The flake
test/background-jobs.test.ts > session-local background job state > launches and disposes a real Git Bash background job fails intermittently on windows-latest:
AssertionError: expected undefined to be defined
at background-jobs.test.ts:603
Observed on PR #503, Pi 0.80.10 / windows-latest. A re-run of the identical commit passed, so it is intermittent rather than a regression. The PR that surfaced it changed only Markdown, CHANGELOGs and version strings — no executable code — so nothing in the diff can reach a test that spawns a real Git Bash process.
Why this is NOT the same bug as #495
Worth separating, because the obvious reading is wrong and would lead to the wrong fix.
#495 was a deadline problem: a 5s vitest timeout could not cover a cold CPython launch, and the symptom was Test timed out in 5000ms.
This one is not a timeout at all. The assertion that fails is:
const job = await awaitLiveCondition(
runtime.launch({ ... }),
"the live Git Bash launch",
() => "no job handle - the contained launch never returned",
);
expect(job).toBeDefined(); // <- line 603, this is what fails
awaitLiveCondition rejects with a descriptive did not settle within Nms error when it times out. That error was not raised. So the helper resolved — with undefined — comfortably inside its 60s WINDOWS_LIVE_TEST_TIMEOUT_MS budget.
In other words: runtime.launch(...) returned no job handle, promptly and without erroring. Raising the timeout would change nothing.
What that points at
A launch that resolves to undefined rather than throwing is a spawn that did not produce a tracked job. Candidates worth checking, in rough order:
shellPath resolution. The test resolves a real Git Bash. If that lookup returns a path that is momentarily unusable on the runner image, a launch could fail soft.
createBackgroundJobRuntime(...) returning a runtime whose launch can resolve empty — note the ! on its construction; if the failure is upstream of launch, the handle is simply never registered.
- A race between process start and job-table registration — the handle is read before the tracker has recorded it, so the read is legitimately empty rather than late.
(3) would be the most concerning of the three, because it means the runtime can report success while owning nothing — and this is the module that tracks background jobs for cleanup, so an untracked job is a leaked process.
Why it matters beyond a re-run
[GATE ] | [REPO] | Merge readiness aggregates this check, and the release preflight requires that gate green for the exact commit being tagged (#385). A flake here blocks the release lane for that SHA, and it trains readers to re-run a red gate without reading it — the habit the gate exists to prevent. It cost one re-run cycle on #503.
Acceptance criteria
- AC-1: the root cause is identified — specifically, whether
launch can resolve undefined on a successful spawn (a tracking race) or only on a failed one.
- AC-2: if a launch cannot produce a handle, it fails loudly rather than resolving empty;
expect(job).toBeDefined() should never be the thing that discovers it.
- AC-3: if it is a registration race, the job table is proven to contain the job before
launch resolves — an untracked background job is a leaked process, which is what this module exists to prevent.
- AC-4: the test passes across repeated
windows-latest runs without a re-run.
Relevant sources
The flake
test/background-jobs.test.ts > session-local background job state > launches and disposes a real Git Bash background jobfails intermittently onwindows-latest:Observed on PR #503, Pi 0.80.10 / windows-latest. A re-run of the identical commit passed, so it is intermittent rather than a regression. The PR that surfaced it changed only Markdown, CHANGELOGs and version strings — no executable code — so nothing in the diff can reach a test that spawns a real Git Bash process.
Why this is NOT the same bug as #495
Worth separating, because the obvious reading is wrong and would lead to the wrong fix.
#495 was a deadline problem: a 5s vitest timeout could not cover a cold CPython launch, and the symptom was
Test timed out in 5000ms.This one is not a timeout at all. The assertion that fails is:
awaitLiveConditionrejects with a descriptivedid not settle within Nmserror when it times out. That error was not raised. So the helper resolved — withundefined— comfortably inside its 60sWINDOWS_LIVE_TEST_TIMEOUT_MSbudget.In other words:
runtime.launch(...)returned no job handle, promptly and without erroring. Raising the timeout would change nothing.What that points at
A launch that resolves to
undefinedrather than throwing is a spawn that did not produce a tracked job. Candidates worth checking, in rough order:shellPathresolution. The test resolves a real Git Bash. If that lookup returns a path that is momentarily unusable on the runner image, a launch could fail soft.createBackgroundJobRuntime(...)returning a runtime whoselaunchcan resolve empty — note the!on its construction; if the failure is upstream oflaunch, the handle is simply never registered.(3) would be the most concerning of the three, because it means the runtime can report success while owning nothing — and this is the module that tracks background jobs for cleanup, so an untracked job is a leaked process.
Why it matters beyond a re-run
[GATE ] | [REPO] | Merge readinessaggregates this check, and the release preflight requires that gate green for the exact commit being tagged (#385). A flake here blocks the release lane for that SHA, and it trains readers to re-run a red gate without reading it — the habit the gate exists to prevent. It cost one re-run cycle on #503.Acceptance criteria
launchcan resolveundefinedon a successful spawn (a tracking race) or only on a failed one.expect(job).toBeDefined()should never be the thing that discovers it.launchresolves — an untracked background job is a leaked process, which is what this module exists to prevent.windows-latestruns without a re-run.Relevant sources
plugins/ca-pi/tools/test/background-jobs.test.ts:588-606(runLiveGitBashJob,awaitLiveCondition)createBackgroundJobRuntime/launchimplementation underplugins/ca-pi/tools/src/