You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Two AcpAdapterV2 teardown tests fail on Windows (taskkill one-shot + session poison timeout) #17011
Two teardown tests in apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts fail on Windows:
AcpAdapterV2 > Windows teardown is one-shot explicitly with independent finalizer cleanup — AssertionError: expected 2 to equal 1 at the taskkillCommands.length check after Scope.close (finalizer runs taskkill a second time).
AcpAdapterV2 > poisons the session when hard teardown defects and blocks replacement work — Error: Test timed out in 60000ms.
Repro
From a clean checkout (no local changes):
git checkout 26285ab80
vp i
vp test run apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts -t "Windows teardown is one-shot explicitly with independent finalizer cleanup"
vp test run apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts -t "poisons the session when hard teardown defects and blocks replacement work"
Both fail deterministically, in isolation as well as in the full-file run.
Environment
OS: Microsoft Windows 11 Pro
Node: v24.14.0
Package manager: pnpm v11.10.0 (via vp i)
Evidence it predates recent changes
I verified both failures on two clean checkouts with no modifications:
26285ab80 (Oct 7, upstream main head at the time) — both fail
9e5229b47 (Oct 5) — both fail identically
Between those two commits there are no changes to apps/server/src/provider/acp/AcpSessionRuntime.ts, apps/server/scripts/acp-mock-agent.ts, or the effect version (4.0.1, same patch hash). The test file itself was only touched by #14108 (subagent models, unrelated area) and #16282 (mechanical layer/layerXyz renames) in that window. So this looks broken at least since Oct 5, possibly longer.
Notes
CI (.github/workflows/ci.yml) runs Ubuntu runners only, which likely explains why a Windows-behavioral failure slipped through: both tests exercise the win32 teardown path (processGroupPlatform: "win32", taskkill-based process group termination) with a real spawned child process.
Failure 1 details: the concurrent double-terminateProcessGroup correctly produces 1 taskkill, but Scope.close(runtimeScope, ...) afterwards bumps it to 2 — the scope finalizer does not observe the one-shot guard on this machine.
I can re-run on Windows if a fix needs verification here.
Thanks for the clean repro on two commits. Checked against main at ff3a04a8ebaaf6bb74b8b5b996013b82e044c700. Not reproduced on Windows here; everything below is from reading the code.
Summary: both look like test-only problems. The tests fake processGroupPlatform: "win32" on a Linux host, but parts of the teardown path and the fixtures depend on the real host platform. CI only runs Ubuntu (e.g. ci.yml#L233), and the Windows lane is manual-only and notes that the suite doesn't pass on Windows yet, so neither test has run on a real Windows host in CI. Both tests and the teardown code they exercise arrived with #2829.
1. Windows teardown is one-shot explicitly with independent finalizer cleanup — mechanism confirmed in code
The test routes taskkill to a recording stub and everything else to the real spawner (L12301-L12307), sets processGroupPlatform: "win32" (L12319), calls terminateProcessGroup twice concurrently, then closes the scope and asserts the count is still 1 (L12326-L12331).
The explicit path is one-shot via Effect.cached (L1734-L1736) and uses options.processGroupPlatform to choose taskkill (L1706-L1708).
The scope finalizer is a separate effect that does not go through that cache, and it branches on HostProcessPlatform (the real process.platform by default, hostProcess.ts#L7-L12), not on options.processGroupPlatform (L1737-L1752):
On a Windows host it runs terminateWindowsProcessTreeWithTaskkill(spawner, …), using the same stubbed spawner, so the stub records a second taskkill → expected 2 to equal 1.
On a Linux host (CI) it falls through to signalOwnedProcessGroup("SIGKILL"), which never touches the taskkill stub, so the count stays 1.
So the finalizer runs independently of the one-shot guard by design (that's what the test name says), and the second assertion only holds on a non-Windows host. In production on Windows this means one extra taskkill /T /F on the same PID at scope close after an explicit teardown, with any failure swallowed and logged (L1749-L1750). That matches the "independent finalizer" intent and isn't a functional bug from what the code shows.
2. poisons the session when hard teardown defects and blocks replacement work — timeout cause is a hypothesis
Confirmed from code:
The test enables T3_ACP_EMIT_RUNNING_COMMAND_THEN_HANG with a PID file (L12483, L12486). The mock agent then runs bash -c 'sleep 120 & … printf "%s %s\n" "$$" "$child" > "$1"' via NodeChildProcess.spawn("bash", …), with no error listener (acp-mock-agent.ts#L1352-L1355, L1375-L1377).
The test's first wait spins on that PID file with no timeout (L12534-L12536). Later waits are unbounded too: Deferred.await(teardownStarted) (L12554), Fiber.join(interruptFiber) (L12568), and the session/request_permissionStream.runHead (L12580-L12588). By contrast, waitForProcesses/waitForProcessesToExit are capped at 2s (L277-L289), so a PID mismatch alone would show up as an assertion failure, not a 60s timeout.
On a Windows host the final Scope.close goes through the real taskkill (finalizer above), not the test's injected windowsProcessTreeTerminator (L12475-L12481). The test then expects the bash/sleep PIDs it read to be gone, checked with process.kill(pid, 0) (L12644-L12650).
Hypothesis (not verified on Windows): the fixture assumes a POSIX bash/sleep whose $$/$! are host PIDs. If bash on your PATH is missing or is something else (for example WSL's bash.exe, where a Windows temp path may not resolve), the PID file stays empty and the test spins at L12534 until the 60s timeout. Even with Git Bash, whether its $$/$! match Windows PIDs is unverified, so later assertions might fail too.
Precedent: this file already guards several real-process fixture tests to Linux hosts with if ((yield* HostProcessPlatform) !== "linux") return; (L1722, L1843, L1928).
Possible fixes (for whoever picks this up)
Test 1: make the last assertion host-aware: expect 2 when HostProcessPlatform is win32, since the finalizer is meant to be independent. Alternatively, build the runtime with a pinned HostProcessPlatform and assert on the finalizer path separately.
Test 2: add the same Linux-host guard as the tests above, since it depends on POSIX bash fixtures. Separately, bounding the L12534 wait (e.g. Effect.timeoutOption plus a clear assertion message) would turn a missing fixture into a fast, readable failure on any platform.
Longer term, these belong with the effort to get the suite green on the manual Windows lane.
No open PR touches these lines today.
@arslnysuf since you offered to re-run: could you share the output of where.exe bash and where.exe sleep in the shell you run vp test from? That should confirm or rule out the PID-file hypothesis for test 2.
Activity
juliusmarminge commentedon Oct 8, 2026
Note
Grok responding on behalf of Julius.
Thanks for the clean repro on two commits. Checked against
mainatff3a04a8ebaaf6bb74b8b5b996013b82e044c700. Not reproduced on Windows here; everything below is from reading the code.Summary: both look like test-only problems. The tests fake
processGroupPlatform: "win32"on a Linux host, but parts of the teardown path and the fixtures depend on the real host platform. CI only runs Ubuntu (e.g.ci.yml#L233), and the Windows lane is manual-only and notes that the suite doesn't pass on Windows yet, so neither test has run on a real Windows host in CI. Both tests and the teardown code they exercise arrived with #2829.1.
Windows teardown is one-shot explicitly with independent finalizer cleanup— mechanism confirmed in codetaskkillto a recording stub and everything else to the real spawner (L12301-L12307), setsprocessGroupPlatform: "win32"(L12319), callsterminateProcessGrouptwice concurrently, then closes the scope and asserts the count is still 1 (L12326-L12331).Effect.cached(L1734-L1736) and usesoptions.processGroupPlatformto choose taskkill (L1706-L1708).HostProcessPlatform(the realprocess.platformby default, hostProcess.ts#L7-L12), not onoptions.processGroupPlatform(L1737-L1752):terminateWindowsProcessTreeWithTaskkill(spawner, …), using the same stubbed spawner, so the stub records a secondtaskkill→expected 2 to equal 1.signalOwnedProcessGroup("SIGKILL"), which never touches the taskkill stub, so the count stays 1.So the finalizer runs independently of the one-shot guard by design (that's what the test name says), and the second assertion only holds on a non-Windows host. In production on Windows this means one extra
taskkill /T /Fon the same PID at scope close after an explicit teardown, with any failure swallowed and logged (L1749-L1750). That matches the "independent finalizer" intent and isn't a functional bug from what the code shows.2.
poisons the session when hard teardown defects and blocks replacement work— timeout cause is a hypothesisConfirmed from code:
T3_ACP_EMIT_RUNNING_COMMAND_THEN_HANGwith a PID file (L12483, L12486). The mock agent then runsbash -c 'sleep 120 & … printf "%s %s\n" "$$" "$child" > "$1"'viaNodeChildProcess.spawn("bash", …), with noerrorlistener (acp-mock-agent.ts#L1352-L1355, L1375-L1377).Deferred.await(teardownStarted)(L12554),Fiber.join(interruptFiber)(L12568), and thesession/request_permissionStream.runHead(L12580-L12588). By contrast,waitForProcesses/waitForProcessesToExitare capped at 2s (L277-L289), so a PID mismatch alone would show up as an assertion failure, not a 60s timeout.Scope.closegoes through the realtaskkill(finalizer above), not the test's injectedwindowsProcessTreeTerminator(L12475-L12481). The test then expects the bash/sleep PIDs it read to be gone, checked withprocess.kill(pid, 0)(L12644-L12650).Hypothesis (not verified on Windows): the fixture assumes a POSIX
bash/sleepwhose$$/$!are host PIDs. Ifbashon your PATH is missing or is something else (for example WSL'sbash.exe, where a Windows temp path may not resolve), the PID file stays empty and the test spins at L12534 until the 60s timeout. Even with Git Bash, whether its$$/$!match Windows PIDs is unverified, so later assertions might fail too.Precedent: this file already guards several real-process fixture tests to Linux hosts with
if ((yield* HostProcessPlatform) !== "linux") return;(L1722, L1843, L1928).Possible fixes (for whoever picks this up)
HostProcessPlatformiswin32, since the finalizer is meant to be independent. Alternatively, build the runtime with a pinnedHostProcessPlatformand assert on the finalizer path separately.Effect.timeoutOptionplus a clear assertion message) would turn a missing fixture into a fast, readable failure on any platform.No open PR touches these lines today.
@arslnysuf since you offered to re-run: could you share the output of
where.exe bashandwhere.exe sleepin the shell you runvp testfrom? That should confirm or rule out the PID-file hypothesis for test 2.