Skip to content

flaky: live Git Bash background job resolves with no handle on windows-latest #504

Description

@SUaDtL

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:

  1. 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.
  2. 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.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions