test: serialize client-metro and harden the unit suite against ambient daemon env#1365
Conversation
…lakes src/__tests__/client-metro.test.ts stubs npx and the package managers on PATH and spawns a real Metro dev server per case, so each case waits real subprocess time (~570-910ms measured). Run in the unit-core project at ~7x file parallelism it contends for CPU with every other stub-spawning file; a starved spawn pushes production down a generic failure path that returns a different error than the assertion expects. This is the same contention flake #1362 serialized runtime-hints.test.ts and apple/core/index.test.ts for, but this file was not included in that batch (observed failing a full test:unit run while passing 20/20 in isolation and green on a re-run). Add it to SUBPROCESS_STUB_TESTS so it joins the fileParallelism:false, maxWorkers:1 subprocess-stub project, so at most one real-stub-spawning file spawns at a time. Injecting the spawn budget is not the lever here: the fake dev server becomes ready fast (no waited-out timeout to inject), the per-case cost is a genuine subprocess spawn, and each case is already under the 2.5s slow-test budget so the gate never flags it. Removing the cost would mean mocking the very spawn/args/package-manager-detection path the file exists to verify, and an exec-options DI seam is forbidden by the CI gate (AGENTS.md). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGCchHyrPhTqtXniWuERYK
Size Report
Startup median (7 runs, lower is better):
Top changed chunks: no changes in the largest emitted chunks. |
A machine actually running agent-device — including this repo's own remote dev
containers — exports AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN
pointing at a live daemon. Production flag-default resolution folds those into
every command's input and connection config (resolveConfigBackedFlagDefaults ->
readEnvFlagDefaults, plus the daemon client's own env fallbacks in
daemon-client-lifecycle), so a configured host silently diverges from CI, which
runs with them unset:
- command-tools and cloud-connect-profile assert exact command input / profile
shapes and gain phantom daemonBaseUrl/daemonAuthToken keys.
- daemon-client and daemon-client-lifecycle take the remote-daemon path
("Remote daemon is unavailable") instead of the local one they exercise.
28 tests across those four files fail deterministically on such a host — fast,
in isolation, not contention — while passing on CI.
Add a shared setup file that deletes the two ambient daemon-connection vars
before each test file, wired into every vitest project alongside the existing
process-memo reset, so a configured host matches CI. Tests that need these set
assign their own value or pass an explicit env object, which runs after this
setup module loads and is therefore unaffected.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGCchHyrPhTqtXniWuERYK
516aa46 to
c2be423
Compare
|
Review of |
CI runs with AGENT_DEVICE_DAEMON_* unset, so it can never exercise the scrub that hermetic-env-setup.ts performs — deleting the setup file or forgetting to wire a project would leave the whole suite green. Add a guard that closes that gap two ways: - A static check that every configured vitest project lists the setup file in its setupFiles (catches an unwired project). - A real vitest child, launched with both daemon vars set, running a probe fixture that proves a wired project sees them scrubbed — plus a negative control (same vars, setup disabled) proving the probe actually detects the leak, so a green result is the setup working, not a no-op. A control var the setup must not touch proves the child inherited the injected environment. The two children run concurrently to stay within the unit wall-clock budget, and the file joins the serialized subprocess-stub project since it spawns real vitest processes. Ignore the fixture config's default export in fallow — vitest loads it by path rather than importing it, the same class as the other tool-config default exports already listed there. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGCchHyrPhTqtXniWuERYK
|
Addressed in Regression (
Scope — the summary now describes all three commits explicitly (serialize client-metro; ambient-daemon-env hermeticity; this regression). Kept on one branch rather than split, per the note to push follow-ups here — happy to split into a separate PR if you'd prefer the env work reviewed on its own. Generated by Claude Code |
|
Re-review of |
AGENTS.md hard rule: TypeScript process execution must go through src/utils/exec.ts, never raw spawn/spawnSync. The guard's runProbe spawned the vitest child with node:child_process spawn directly, bypassing the shared timeout, process-tree cleanup, normalized failure, and diagnostics behavior. Replace it with runCmd invoking the repo-local vitest CLI (node node_modules/vitest/vitest.mjs) with allowFailure: true and a bounded timeoutMs, asserting the returned exitCode. allowFailure surfaces the negative control's non-zero exit as data instead of throwing, so that assertion keeps working; the two probes still run concurrently to stay under the unit budget. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGCchHyrPhTqtXniWuERYK
|
Fixed in Local: guard passes at ~0.7–1.2s (under the unit budget, no slow-gate entry); Generated by Claude Code |
|
Blocking finding — the new hermetic-env guard still cannot guarantee timely process-tree cleanup. |
…plete Follow-up to review. Two process-tree cleanup gaps in the guard's runProbe: - It gave the nested vitest a 60s timeout while the enclosing test used vitest's default (5s) timeout, so a hung probe would time the parent test out first and leave the child running (orphaned). - runCmd only process-group-kills when detached:true; without it a timeout kills only the direct child, so vitest worker descendants could survive. Run the probe detached so a timeout kills the whole process group (the vitest child and its workers), and set the child timeout (20s) strictly below an explicit enclosing test timeout (40s) so a hang is reaped by runCmd before vitest abandons the parent test. The repo-local node/vitest invocation, allowFailure, exit-code assertions, and the concurrent positive/negative probes are unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGCchHyrPhTqtXniWuERYK
|
Fixed in
Unchanged per your note: repo-local Local: guard passes ~0.7s (under budget, no slow-gate entry); Generated by Claude Code |
|
Re-review clean at |
… test The previous guard spawned a real vitest child to exercise the setup through vitest's setupFiles machinery, which dragged in process-group cleanup, timeout ordering, a fixture config + probe, and a fallow suppression — a pile of scaffolding around a scrub that is two delete statements. Prove the same contract in-process instead: - Behavior: set the daemon vars, vi.resetModules(), re-import hermetic-env-setup, and assert they are gone. This exercises the real import-time scrub on any host (CI included, where the vars are otherwise absent) and fails if it regresses. - Wiring: assert every configured vitest project lists the setup in setupFiles. Runs in ~5ms in unit-core with no subprocess, so it also drops out of SUBPROCESS_STUB_TESTS and removes the fixtures and the fallow ignore entry. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGCchHyrPhTqtXniWuERYK
|
Re-review clean at |
|
Summary
Three test-infrastructure changes:
Serialize
client-metro.test.ts(a1897c1) — it stubsnpx/package managers onPATHand spawns a real Metro dev server per case: the same real-subprocess contention profile Generalize subprocess stub tests to support multiple platforms #1362 serializedruntime-hints/apple-core-indexfor, but it was missed in that batch. Moves it into thesubprocess-stubproject (fileParallelism:false, maxWorkers:1). Injecting a budget isn't the lever here — the fake server becomes ready fast (no waited-out timeout), each case is already under the 2.5s slow-test budget, and removing the spawn would delete the behavior under test; an exec-options DI seam is CI-forbidden.Make the vitest suite hermetic against ambient daemon env (
c2be423) — a machine actually running agent-device (including this repo's own remote dev containers) exportsAGENT_DEVICE_DAEMON_BASE_URL/AGENT_DEVICE_DAEMON_AUTH_TOKEN. Production flag-default resolution folds those into command inputs and connection config (resolveConfigBackedFlagDefaults→readEnvFlagDefaults, plus the daemon client's own env fallbacks), so on a configured hostcommand-tools,cloud-connect-profile,daemon-client, anddaemon-client-lifecycle(28 tests) fail deterministically — fast, in isolation, not contention — while passing on CI, which runs with the vars unset. A sharedsrc/__tests__/hermetic-env-setup.ts, wired into every project alongside the existing memo-reset, deletes those two vars before each test file so a configured host matches CI. No production behavior changes.Regression test for the hermetic-env setup (
32f5546) — CI starts clean, so it can't catch a regression of (2): deleting the setup or forgetting to wire a project would leave the suite green.hermetic-env-setup.test.tsproves both in-process, no subprocess:vi.resetModules(), re-importhermetic-env-setup.ts, assert they are scrubbed. This exercises the real import-time scrub on any host (CI included, where the vars are otherwise absent) and fails if it regresses (verified by neutering the setup locally).setupFiles.An earlier revision spawned a real vitest child to exercise the setupFiles machinery end-to-end; that dragged in process-group cleanup, timeout ordering, a fixture config/probe, and a fallow suppression for a two-line scrub, so it was replaced with the in-process test above (~5ms in
unit-core).Validation
pnpm test:unitgreen ×2 (490 files / 4338 tests, exit 0); on a configured host the pre-change suite failed 28 tests across the four files in (2).client-metroruns serialized and passes; the hermetic-env test runs in-process in ~5ms (unit lane, no slow-gate entry).format,lint,typecheck,fallow(diff scope), andlayeringall pass locally.Device evidence: N/A — test-only, no production code touched.
https://claude.ai/code/session_01YGCchHyrPhTqtXniWuERYK