Skip to content

test(spec): harden the build-schemas check-mode fixture repos against the merge-queue lifetime race - #9104

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9068-fixture-repo-race-hardening
Aug 16, 2026
Merged

test(spec): harden the build-schemas check-mode fixture repos against the merge-queue lifetime race#9104
os-zhuang merged 1 commit into
mainfrom
claude/issue-9068-fixture-repo-race-hardening

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Fixes#9068

Test-harness hardening only. No *.zod.ts, no strictness-ledger file, no behaviour
change to build-schemas.ts, and all 60 assertions in the file are preserved verbatim —
what changes is how the fixture repositories are built and insulated, never what is
asserted.

What the harness actually looked like (measured, not assumed)

Three of the card's premises were checked against origin/main before anything was edited:

  • Tmpdir collision between concurrent test files — falsified. All four fixture repos in
    this file already use fs.mkdtempSync (build-schemas-check-, and three box prefixes),
    packages/spec has exactly one vitest project, and vitest isolates test files into
    separate processes. No concurrently running file can name one of these directories.
  • "The git helper shells out per fixture" — confirmed, and that was the hole. It passed
    cwd and two -c user.* args and inherited everything else: the ambient global and
    system git config, init.templateDir, and every GIT_* environment variable. So did the
    generator runs, and build-schemas.ts shells out to git itself (merge-base, cat-file,
    a --depth=1 fetch).
  • Serializing the three git-topology blocks (the card's fallback ③) is a no-op. They are
    already serial: vitest runs one test at a time within a file. Not implemented, and the
    reason is recorded in the file.

The mechanism, reproduced

Three describe blocks (#5370, #5847, #6452 — exactly the three that failed) deliberately
write .git/shallow into the shared sandbox, because that is the environment they
model: CI checks out --depth=1. Inside that window every commit behind the graft point is
unreachable by definition, so a git gc arriving there prunes the fixture's own history
and leaves the refs pointing at objects that are gone. On a five-commit fixture in exactly
that state:

$ git gc --quiet # ambient [gc] pruneExpire/reflogExpire = now
$ git merge-base HEAD 538d7ec0… # a commit made moments earlier
fatal: Not a valid commit name 538d7ec0ac80c958841f3606d567dee02bdbc1b1

That is the reported signature. The control — same commits, same gc, no.git/shallow
— keeps every object.

The fix

Two layers, because they fail differently:

  1. HERMETIC_ENV on every git this file runs and on the generator spawns, so ambient
    config and leaked GIT_* pointers (GIT_DIR, GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY,
    GIT_ALTERNATE_OBJECT_DIRECTORIES, …) cannot reach a fixture or its children.
  2. initFixtureRepo writes gc.auto=0, gc.autoDetach=false, maintenance.auto=false,
    gc.pruneExpire=never, gc.reflogExpire*=never, core.logAllRefUpdates=true into each
    repo's own config. This is the layer that survives a gc this file never launched:
    local config outranks global. Measured A/B — identical outside git gc with a hostile
    global config, in the shallow window: unhardened repo loses the commit, hardened repo
    keeps it.

Plus the card's preferred ①, applied where it is load-bearing: the three git-topology blocks
each take their own fixture repo (createSandbox / mountSandbox / releaseSandbox).
They fork branches, reset them every beforeEach, leave merges uncommitted and truncate
history; sharing one repo is why a single lost object read as ten failures spread over three
blocks, with the block that caused it invisible. A scripts/ copy is 1.8 MB and the
generation cost is per run, not per sandbox.

Finally, a fixture git failure now names the repo and its state (HEAD, main,
origin/main, shallow, and whether main's object still exists) instead of a bare sha —
the whole first triage lap on this card went into establishing that the sha belonged to no
tree of this repository.

Verification — every run below at c0740f996, the head of this branch

Stability, since a hardening PR should demonstrate it rather than assert it:

runscoperesultduration
1whole file60 passed (60)278.7 s
2whole file60 passed (60)274.7 s
3a / 3bthe three git-topology blocks, two instances concurrently13 passed each60.3 s / 60.6 s

Run 3 is the parallel-pressure reading: two processes driving the same fixture-building code
at the same moment, which is the shape the merge queue applies.

Reverse verification, direction predicted (red) before it was run. Same subset
(-t 'a shallow checkout re-anchors', 5 tests), same hostile environment — GIT_DIR aimed at
a decoy repository plus a global config carrying gc.pruneExpire/reflogExpire = now:

  • origin/main's harness → 5 failed / 5
  • this branch's harness → 5 passed / 5

Gates. Re-derived from the actual changed path with node scripts/pm/dispatch-gates.mjs;
it named exactly the dispatched set, nothing beyond. Green here: check:merge-driver,
check:type-source-resolution, check:query-options-erasure, check:type-check-coverage,
check:engine-double-contract, check:where-matcher, check:nul-bytes, and
check:scripts-typecheck (the zero-error-budget program this file belongs to).

Two are not measured locally, deliberately: check:type-check-debt and
check-dev-prereqs.mjs both refuse on a worktree with no built closure — the ratchet says so
itself ("measuring now would not fail, it would silently measure a DIFFERENT WORLD"). Their
population is src/**/*.test.ts and workspace dist/, neither of which this diff touches, so
a full 55-package build here buys no information the CI job will not produce. CI is the
authority on both.

skip-changeset: tests-only, nothing published.


Generated by Claude Code

…per-block (#9068)
A merge-queue full-suite run lost ten tests in this file to one root error —
`fatal: Not a valid commit name <sha>` on a commit the fixture had created
itself minutes earlier. The fixture repos inherited ambient git config and any
leaked GIT_* pointer, and three describe blocks share one repo that they mark
shallow; a gc arriving in that window prunes everything behind the graft point.
Reproduced and closed, without changing what any test asserts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 16, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 16, 2026 10:46
@os-zhuang
os-zhuang added this pull request to the merge queueAug 16, 2026
Merged via the queue into main with commit 2901d2fAug 16, 2026
34 of 35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9068-fixture-repo-race-hardening branch August 16, 2026 11:04
os-zhuang pushed a commit that referenced this pull request Aug 16, 2026
…es hermetic
Applies the #9068/#9104 hermetic-git pattern to the two harnesses issue #9109
names: the spec sharded-artifacts fixture repos, and the create-objectstack
repo reads. No assertion changes — only what the git invocations inherit.
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…es hermetic (objectstack-ai#9168)
Applies the objectstack-ai#9068/objectstack-ai#9104 hermetic-git pattern to the two harnesses issue objectstack-ai#9109
names: the spec sharded-artifacts fixture repos, and the create-objectstack
repo reads. No assertion changes — only what the git invocations inherit.
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gateteststooling

Projects

None yet

2 participants

@os-zhuang@claude