Skip to content

test(rstest): pool policy from the #576 audit — worker-root teardown, stale-dist guard, isolation and timeouts in every pool, adapter libId, membership moves - #587

Merged
ScriptedAlchemy merged 7 commits into
mainfrom
test/576-rstest-config
Sep 5, 2026
Merged

test(rstest): pool policy from the #576 audit — worker-root teardown, stale-dist guard, isolation and timeouts in every pool, adapter libId, membership moves#587
ScriptedAlchemy merged 7 commits into
mainfrom
test/576-rstest-config

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Config-level items from the Rstest audit, #576 (§1 config surface, §3 adapter/stale dist, the pool-membership half of §2). The test-level flake fixes are #584; the CI shape landed in #583. No test body changed except one import line (item 8).

Items

# #576 finding Resolution
1 P1 /tmp/ab-rstest-* worker roots never removed on the pnpm test* path rstest.global-setup.ts (setup tags the invocation with AGENT_BUNDLE_RSTEST_RUN_ID, teardown removes exactly that run's roots via removeRunRstestWorkerRoots, scripts/rstest-worker-roots.mjs:120); rstest.worker-isolation.ts:40-44 records the id in the owner marker. Wired into every pool through workspaceGlobalSetup (rstest.pools.ts:19). Rstest runs global setup in a forked process and relays its env changes to the workers, verified for isolate true and false, on a failing run, and with an outer-owned id. removeOwnedRstestWorkerRoots (local-ci) is unchanged; #583's PLAYWRIGHT_BROWSERS_PATH pin lives outside the roots and is never swept.
2 P1 stale dist consumed with no freshness guard scripts/dist-freshness.mjs — newest-mtime of each package's build inputs (src, build config, package.json, tsconfig chain, lockfile; the workbench sources for agent-bundle, whose build copies ../workbench/dist) vs newest mtime of its dist, for the five outputs pnpm build writes. rstest.dist-freshness.setup.ts runs it first in every pool's globalSetup; scripts/check-dist-fresh.mjs runs ahead of pnpm typecheck (package.json:24). Fails fast with the offending input vs output and run `pnpm build` ; 50–70 ms. ensureRuntimeExamplePayload (packages/workbench/tests/helpers/runtime-example-payload.ts:32) now rebuilds a payload older than the example's inputs, which include packages/rsc-runtime/dist — so the first process-pool run after a pnpm build rebuilds the example once, in the orchestrator. No bypass variable: every flow that runs a pool builds first.
3 P2 route-unit / projection never load rstest.setup.ts rstest.route-unit.config.ts:28, rstest.projection.config.ts:29: setupFiles: [...workspaceSetupFiles, ...helper.setupFiles] — isolation before the generated route registry. Measured: those pools now create (and remove) 9 and 13 worker roots per run; before, 0 (nothing was isolated).
4 P2 thin rstest.config.ts with no caps or timeouts rstest.config.ts:47-54: the process pool's worker cap (processPoolMaxWorkers, 1..4), AGENT_BUNDLE_TEST_TIME_SCALE, 30 s test/hook floors, plus the shared hooks. The cap/scale formula moved out of rstest.integration.config.ts into rstest.pools.ts:67-86 and both configs import it.
5 P2 unit pool at the 5 s default Explicit testTimeout/hookTimeout per pool (table below); poolTimeouts (rstest.pools.ts:43) keeps hookTimeout = testTimeout.
6 P2 builds / native opt-ins in the unit pool Own commit. prebuilt-payload.test.ts (build() ×3) and test-browser-rstest.test.ts (compileMcpApps → Rsbuild) → integrationTestFiles. host-adapters.native.test.ts (0 unconditional tests, 29 nativeIt) → new nativeHostTestFiles + rstest.native-host.config.ts (1 worker, 60 s) + pnpm test:native-host. verify-rstest-shards: 92 files, 46/46, partition exact. Unit-pool comment reworded to "no builds, no long-lived processes" (rstest.unit.config.ts:20-26): the 11 spawning files reap short-lived children (node -e, git --version, one-shot Flight worker — dispatcher 3.35 s, runtime-routes 2.5 s) and stay. Not moved: native-claude-contract, native-codex-contract, host-contract, eval-codex-home — each carries 3–18 unconditional tests next to one gated nativeIt/smokeIt, and native-host-smoke.yml now runs them by file through pnpm test:unit; moving the files would drop 43 per-PR tests and silently detach the smokes. Splitting the gated cases into *.native.test.ts files is a test-level follow-up.
7 P2 adapter without libId packages/agent-bundle/rslib.config.ts:56,61 id: 'esm-node' (naming only: Rsbuild environment label, --lib selector, build-cache key — one cold cache after this lands, no emitted file changes); rstest.rslib.ts:123-141 passes it, drops the name the adapter derives so pools keep the default project name; mapped-vs-ignored table verified against @rstest/adapter-rslib@0.11.12 source (rstest.rslib.ts:57-112). rstest-rslib-adapter.test.ts (10 tests) pins id parity, the silent-{} fallback, ESM/node, the define, tsconfig repoint, plugin/tools filtering, hygiene.
8 P2 projection/mcp-lineage.test.ts imports agent-bundle/test from dist import { openInMemoryMcpServer } from '../../src/test/mcp.ts' — one line.
9 Optional — unit collect 45.8 % Findings below; no config change.

rstest-pool-configs.test.ts (33 tests) resolves all nine configs in a child Node process and pins items 1–5 at the resolved-config level (hooks, setup order, timeouts, caps, hygiene on the helper-built pools).

Timeouts

pool before (test / hook) after (test = hook) basis (#576 measurements)
unit 5 s / 10 s default 15 s inspect-state 2.3–4.5 s per case, 5 s timeout in 1 of 3 runs; longer runCli files carry per-test timeouts
route-unit 5 / 10 30 s lifecycle-replay 12.3 s of tests
projection 5 / 10 30 s contract-matrix ≈5.5 s per case, slowest 8.3 s
integration 30 / 10 30 s unchanged test floor; hook floor raised
rstest.config.ts 5 / 10 30 s dev-host-install 30 s per host variant
packed 5 / 10 120 s pack + install per case; files set up to 300 s themselves
evidence 30 / 10 30 s hook floor raised
mcp-conformance 180 / 10 180 s hook floor raised
native-host (new) 60 s 2× the 30–32 s real-host install variants; file sets no per-case timeout

Worker roots

  • Baseline on this host: 53,297 ab-rstest-* roots at start (grew to 55,059 by the time the gates ran — other agents' runs).
  • Per run with the teardown (AGENT_BUNDLE_RSTEST_DEBUG_ROOTS=1): unit removed 95/95, route-unit 9/9, projection 13/13, integration shards 46/46 and 46/46, packed 12/12 — 0 retained, 0 left behind by any pool. Before: the unit pool leaked exactly 95 per run (one per worker; measured with a tagged TMPDIR), route-unit/projection leaked 0 only because nothing isolated them.
  • Legacy roots are never claimed by a teardown (no runId in their marker); the owned-roots sweep result is below.

Teardown cost: Rstest's global-setup fork ≈1.5 s per pool run; the freshness check ≈0.3 s; the root sweep scales with the number of ab-rstest-* directories in /tmp (≈3 s on this host with 55k legacy roots, ≈0 on a clean runner). To stop paying that forever, the teardown also reclaims untagged roots from the same checkout once their worker has exited (removeRunRstestWorkerRoots({ reclaimUntaggedFrom }), verified end-to-end with a planted legacy root). Roots from other, deleted worktrees are the owned-roots sweep's job — run once after merge; count below.

OWNED_SWEEP

Pool wall times (this host, 96 cores, load 30–60, single runs after the change vs lane medians of 3 before)

pool before after tests
unit 44.4 s 48.2 s 3758 passed / 6 skipped
route-unit 16.0 s 21.2 s (A/B back-to-back without the hooks: 17.7–18.6 s; with: 23.1–23.7 s) 83
projection 121.5 s 127.1 s 172
integration shard 1/2 · 2/2 316 s (whole pool, 4 workers) 228 s · 138 s 903 + 176 passed / 4 skipped (was 33 skipped: the 29 native skips left the pool)

The route-unit/projection deltas are the cost of the setup file, the global-setup fork and the freshness stat pass (≈1 s), inside run-to-run noise on this shared box.

Optional item 9 — collect analysis (findings only, no config change)

Compared the unit pool's default against output.bundleDependencies: false and true (Rstest 0.11, isolate: false) on dev-services.test.ts and the effect-boundary canary, single-file and 15-file, at default and 2 workers. false is the node default and measured identically; true bundles a 19.8 MB vendors chunk (effect, typescript, MCP SDK, rsbuild/rslib, zod), adds 0.4–0.6 s per build and never lowers collect (2.06 s vs 1.42 s single-file; 16.4 vs 16.0 s aggregate at 15 workers; 6.25 vs 3.77 s at 2 workers). The canary passed under every variant, so module identity is not the blocker — the numbers are. The pool's collect cost is not the intrinsic graph (0.28 s for effect-boundary alone, 1.42 s for dev-services) but 95 cold workers with no reuse: Rstest drops every non-runtime chunk from a worker's cache after each file even with isolate: false, so only natively imported externals are reused across files, and bundling forfeits that too. Aggregate collect for 15 files fell 16.0 s → 3.8 s going from 15 to 2 workers with only +0.7 s Duration. The lever worth measuring on the full pool is files-per-worker (pool.maxWorkers), not bundling.

Behaviour changes to know about

  • Any pool started over a dist older than its inputs — or without one — fails in the orchestrator with the rebuild instruction instead of testing stale code. A git checkout that touches sources flags staleness even when content is unchanged; that is the safe direction.
  • pnpm typecheck refuses a stale dist for the same reason (it types against @agent-bundle/runtime's dist .d.ts). Consequently pnpm test now builds once up front and runs the integration pool prebuilt, native-host-smoke.yml builds before its unit-pool smoke, and the AGENTS.md pre-push gate starts with pnpm build.
  • AGENT_BUNDLE_RSTEST_RUN_ID may be set by an outer runner to own the id; concurrently running pools then need distinct ids.
  • pnpm test:native-host is new; native-host-smoke.yml does not run it (the NATIVE_HOST_CONTRACTS gate ran nowhere in CI before either) — a CI follow-up.
  • Not in this PR: docs/local-ci.md §worker roots still describes only the local-CI sweep (that file was ci: split Verify into a fast leg + 2 integration shards behind the required gate; pin the browser; retry setup; nightly pools (#576) #583's); a one-line mention of the pool teardown can ride the next CI docs change.

Changeset

skip-changeset: only root rstest*.ts, scripts/**, root package.json scripts, tests and test helpers change. The one file under a publishable package, packages/agent-bundle/rslib.config.ts, gains a lib id (naming only; no emitted file changes) — nothing user-facing.

Self-review

Reviewer: change-risk-reviewer on gpt-5.6-sol-medium, against git diff origin/main...HEAD, two passes.

Pass 1 — 5 findings:

  1. blocker native-host-smoke.yml ran pnpm test:unit before pnpm build; with the freshness guard a fresh runner has no dist and the unit smoke would fail. Fixed: build first, comments updated.
  2. should-fix root pnpm test began with test:unit and failed on a clean checkout. Fixed: pnpm build && test:unit && test:route-unit && test:projection && test:integration:run (one build, no double build).
  3. should-fix teardown cannot sweep Windows worker roots (agent-bundle-rstest-w* under tmpdir()). Dismissed: parity with the pre-existing local-CI sweep, which has the same non-Windows scheme; no Windows CI leg or supported Windows dev path; Windows roots are still created and isolated exactly as before.
  4. should-fix newest-output masking: a dist whose newest file postdates the inputs while other files are older (build failed mid-way, hand-touched file) reads fresh. Accepted bound, now documented in the scripts/dist-freshness.mjs header — pnpm build rewrites each dist whole and a failing build reports itself; a per-file manifest is the alternative if this ever bites.
  5. should-fix AGENTS.md pre-push gate (pnpm typecheck && pnpm lint && pnpm test:unit, build only for rsc-runtime changes) now fails on any source-modified tree. Fixed: pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit, with the reason.

Checked and fine by the reviewer: required CI legs, nightly, release, package-preview, local-CI and packed flows all build first; docs.yml uses the website's own typecheck; rstest list (shard guard) runs after CI's build and does not run global setup; integration worker cap / time scale identical to origin/main; moved files not named by native-host-smoke.yml; run-id cleanup retains live pids and rejects non-directories; PLAYWRIGHT_BROWSERS_PATH is captured before XDG isolation; the Rslib id does not alter published files, so skip-changeset holds.

Pass 2 (after fixes) — no findings; "clear to merge, subject to green CI".

…uard, isolation setup in every pool, explicit timeouts, adapter libId (#576)

- rstest.global-setup.ts: tag each invocation with AGENT_BUNDLE_RSTEST_RUN_ID
  (relayed to workers), record it in the worker-root owner marker, and
  remove exactly that run's /tmp/ab-rstest-* roots in teardown.
- scripts/dist-freshness.mjs + rstest.dist-freshness.setup.ts +
  scripts/check-dist-fresh.mjs: newest-mtime src-vs-dist guard for every
  `pnpm build` output, run in every pool's globalSetup and ahead of
  `pnpm typecheck`; ensureRuntimeExamplePayload rebuilds a stale payload.
- rstest.pools.ts: shared globalSetup chains, setup file, poolTimeouts
  (hookTimeout = testTimeout), process-pool worker cap and time scale;
  route-unit/projection load rstest.setup.ts before the generated registry;
  rstest.config.ts gets the process-pool cap and 30 s floors.
- rstest.rslib.ts passes the lib's id as libId; mapped vs ignored adapter
  fields documented; rstest-rslib-adapter.test.ts and
  rstest-pool-configs.test.ts pin the resolved configs.
- projection/mcp-lineage.test.ts imports agent-bundle/test from src.
…pool and the installed-host contract proofs to their own pool (#576)

prebuilt-payload.test.ts and test-browser-rstest.test.ts run real
builds; host-adapters.native.test.ts asserts nothing without
AGENT_BUNDLE_NATIVE_HOST_CONTRACTS=1 and now runs on one worker through
rstest.native-host.config.ts (`pnpm test:native-host`) instead of as 29
skips per integration run. Shards still partition the pool (46/46).
@ScriptedAlchemy ScriptedAlchemy added the skip-changeset PR changes a publishable package but ships no observable change; changeset not required label Sep 5, 2026
@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d8d8e32

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T03:54:56.478351Z ed0b7fa PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 5, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@587
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@587
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@587
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@587

commit: d8d8e32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed0b7fa220

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rstest.unit.config.ts
*/
export default defineConfig({
extends: withAgentBundleRslibConfig(),
globalSetup: [...workspaceGlobalSetup],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Build before running the unit-pool freshness guard

In .github/workflows/native-host-smoke.yml, the source signed-in smoke deliberately runs pnpm test:unit <files> immediately after a fresh checkout and only runs pnpm build afterward (lines 64–75). Adding workspaceGlobalSetup here makes that first test command call assertFreshDist while all five untracked dist trees are still absent, so every Native host smoke dispatch exits during global setup before exercising either host; move the workflow's build ahead of this step or defer the freshness guard for this source-only invocation.

Useful? React with 👍 / 👎.

@ScriptedAlchemy ScriptedAlchemy Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2247394 (third commit, "build before every pool in the test chain and the native smoke"): pnpm build now runs before the unit-pool smoke in native-host-smoke.yml, and root pnpm test builds first as well. Same finding as pass 1 / item 1 of the Self-review section.

export default defineConfig({
lib: [
{
id: agentBundleLibId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required package changeset

This modifies a non-test file in the publishable packages/agent-bundle package, but the commit contains no .changeset/*.md; the repository requires exactly one changeset for such package changes, so the Changeset-present gate will reject this commit. Add the required patch changeset.

AGENTS.md reference: AGENTS.md:L127-L138

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR carries the skip-changeset label (the Changeset present check is green). The only change under a publishable package is the lib entry's id: 'esm-node' in rslib.config.ts, which names the Rsbuild environment for --lib and the build cache; it emits no different file, so there is nothing user-facing to release-note. Justification is in the PR body under "Changeset".

…e smoke; document the freshness bound (#576 self-review)

Every pool now refuses a missing or stale dist, so `pnpm test` builds
once up front and runs the integration pool prebuilt, native-host-smoke.yml
builds before its unit-pool smoke, and the AGENTS.md gate starts with
`pnpm build`. dist-freshness.mjs names the one false negative the
newest-mtime comparison admits.
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 5, 2026 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR changes a publishable package but ships no observable change; changeset not required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant