Drive the shutdown path where the requested signal reports a child error without exiting, the bounded marker grace expires, and fallback SIGKILL is accepted.
Assert that both signals are attempted and that close drains the successful fallback exit before preserving the original child error, restoring per-file 100% branch and line coverage for the launcher.
A successful Windows termination request can end the process before Node publishes exitCode or signalCode. If a child error wins the shutdown race, give that accepted exit a bounded observation window before escalating or reporting fallback refusal.
Cover a delayed real exit edge, preserve prompt refusal behavior for a genuinely live child, and document the launcher grace without weakening the complete stdio and parser drain boundary.
Model the requested signal setting a child termination marker before the launcher begins fallback handling. The regression proves close drains inherited stdio and propagates the original process error without sending a redundant SIGKILL.
This complements the post-check fallback-refusal race and restores the launcher's required 100% per-file statement and branch coverage.
Pass the fake descendant the parent process's stdout and stderr stream handles instead of Unix-style numeric file descriptors. This lets Windows duplicate the live ACP and diagnostic pipes so launcher shutdown can prove that inherited handles, buffered frames, and stderr all drain after the parent exits.
Observe the pending update promise before initiating shutdown as well, preventing a missing late frame from becoming a transient unhandled rejection before the assertion reports the fixture failure.
Treat a fallback kill refusal as successful termination when the child already carries an OS exit marker. Windows maps Node's supported signal names to forced termination, so the requested signal can end the process between the launcher error race and its fallback SIGKILL.
Drain inherited stdio, the ACP parser, and in-flight callbacks before propagating the original child error in either exit-race path. Preserve AggregateError reporting only for a refused fallback while the process is still live, and add a deterministic cross-platform regression for that ordering.
Three tests in the shared acp-snapshot package hardcoded POSIX path
separators in their assertions, so they failed on Windows where
node:path.join produces backslash paths:
- childFixturePaths (suite.spec.ts): expected literal '/snap/s/session.1.jsonl'
but join returns '\snap\s\...' on Windows; use join() for the expected value.
- harness.spec.ts (env-forwarding test): substring-matched a JSON-encoded
path against raw stdout text, where backslash escaping makes the compare
byte-fragile; parse the env-probe chunk and compare the structured value.
- harness.spec.ts (harvested-cwd test): substring-matched the raw cwd
against JSONL text where the cwd is JSON-escaped; parse the session line
and compare the cwd field.
These were master's latent bugs (the package's tests never ran on Windows
until the Windows CI lane observed them). Verified green on Windows via
scripts/caohuanqi-private/run-ci.py --windows.
The ACP bridge registers the first real approval answerer: an ask for an
agent it owns becomes session/request_permission attached to the already-
streamed tool call (one-shot allow_once/reject_once only), outcomes map
conservatively (unknown optionId never grants, client cancel → cancelled),
and foreign or call-less requests delegate down the waterfall. The snapshot
harness accepts scripted permissionAnswers (FIFO; an unscripted prompt
answers cancelled, fail closed) so recorded scenarios can drive the wire
keylessly.
A client-callback throw only becomes a JSON-RPC error RESPONSE to the
agent's session/request_permission — runScenario itself kept going, so a
tolerant agent could treat the error as a denial and the scenario would
pass, or worse, record: the impossible click baked into fixture and
golden, green on every replay. The mismatch is now captured as a harness
error while the agent is answered plain cancelled (a well-defined path
it cannot reinterpret), and the step loop rejects the run on it as soon
as the in-flight step settles. The spec asserts the rejection instead of
the agent-side error echo.
InputScript gains an optional permissionAnswers queue, consumed FIFO by
the harness's requestPermission handler. Each entry selects by option
KIND (allow_once, reject_once, …): option ids are agent-issued randoms a
committed script cannot know, while kinds are the ACP-stable vocabulary,
so the client maps kind → the offered optionId at answer time. An absent
or exhausted queue answers cancelled — existing scenarios and goldens
are untouched — and a scripted kind the request never offered throws,
surfacing as a JSON-RPC error on the permission request: the scenario
scripted an impossible click.
This is what lets an approval-flow suite (the sandbox composition) drive
allow/reject round-trips deterministically from input.json, per the
shared-acp-snapshot RFC.
A scripted fake ACP agent bin (tests/fixtures/fake-acp-agent.ts) speaks
real newline JSON-RPC through the REAL runScenario spawn path (tsx
loader, temp cwd, env plumbing); every behavior — prompt outcome,
session/new rejection, persisted logs, filesystem noise — comes from a
behavior.json beside the fixture, so specs script whole subprocess runs
from data. harness.spec.ts drives every step op, both expect-error arms,
the permission-stub default, env forwarding, workspace seeding, and the
harvest ordering/noise/fallback branches. suite.spec.ts runs the factory
for real at collection time: a replay suite over committed synthetic
fixtures and a record suite over a temp copy (write-back never touches
the committed tree; ACP_SNAPSHOT_SPEC_BOOTSTRAP=1 re-bootstraps it),
plus direct cases for the exported pure helpers. The suite factory's
pure helpers (childFixturePaths, fixtureContext, normalizedHeaders,
headerDeltaCount) are exported for those direct specs.
Two branches carry justified v8 ignores, both structurally unreachable:
the waiter in-bounds guard (noUncheckedIndexedAccess) and waitForExit's
already-exited race guard (both call sites sit one synchronous frame
after stdin.end()/kill()). The fake bin substitutes the session/new cwd,
not process.cwd(), into scripted logs — the realpath difference
(/private on darwin) is exactly what the real bin's header carries.
packages/support/acp-snapshot/src is at 100% statements, branches,
functions, and lines under the per-file gate.