Skip to content

fix(test-harness): invalidate generated tests when the generator changes - #49

Merged
kinlane merged 2 commits into
mainfrom
fix/harness-cache-invalidation
Aug 3, 2026
Merged

fix(test-harness): invalidate generated tests when the generator changes#49
kinlane merged 2 commits into
mainfrom
fix/harness-cache-invalidation

Conversation

@kinlane

@kinlane kinlane commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Refs #35, #29, #33. main is red; this is the fix.

#48 passed locally and failed in CI. You asked me to verify, I did, and I still missed this — worth being plain about, because the reason I missed it is the interesting part.

What went wrong

The harness generates a .test.js per scenario and bakes the {bin} command into the generated file. Generation is incremental — fileEntryCache tracks the scenario files and the generated outputs, regenerating only scenarios whose inputs changed. Nothing tracked the generator itself.

So #48 changed which command the tests run, no scenario file moved, and CI restored a previously generated tests/ directory alongside a cache declaring it current. The suites kept executing the old binary path:

/bin/sh: 1: /__w/.../packages/cli/binaries/spectral: not found

That is why test-node (18.20.8) regressed from passing to failing — it wasn't Windows-specific at all.

Why local verification didn't catch it

The bug only appears at steady state. After a run with an empty tests/ directory, the next run sees the generated files as newly tracked and regenerates everything regardless. It takes three runs to reach the state CI restores into — and I had been deleting the cache between runs, which is precisely the condition that hides it.

My first attempt at a control run even seemed to disprove the bug, for exactly this reason. I only got a true reading after forcing steady state.

The experiment

control, no fix, steady-state cache, generator changed  ->  dist/index.js   (stale)
with fix,          same cache,       generator changed  ->  dist/FIXED.js   (correct)

Reproduced the CI failure locally first, then fixed it, then re-confirmed. Harness still passes 65/66 — the 66th is help-no-document, whose own faketty.sh uses util-linux script -qfec that BSD script rejects, unrelated and pre-existing.

Two layers, because either alone leaves a hole

generate-tests.ts now tracks __filename alongside the scenarios. A changed generator regenerates every scenario.

The CI cache key now includes hashFiles('test-harness/**') — and so does the restore-keys prefix. That prefix was the real hole: an exact-key miss fell back to test-harness-<os>-<node>-, which cheerfully restored tests built by an older generator. Adding the hash to the key alone would not have helped.

Worth drawing out

This is a cache that could silently serve stale executable content across a semantic change — the same shape as the {{ checksum }} constant-key defect in #29, and adjacent to the reproducibility concern in #15. Caches that key on the wrong thing do not fail loudly; they hand you yesterday's answer.

Given the day's record I would rather this one ran CI before merging.

kinlane and others added 2 commits August 3, 2026 15:50
main is red. #48 passed locally and failed in CI, and this is why.

The harness generates a .test.js per scenario, baking the {bin} command into the
generated file at generation time. Generation is incremental: fileEntryCache
tracks the scenario files and the generated outputs, and only regenerates
scenarios whose inputs changed. Nothing tracked the generator itself.

So #48 changed which command the tests run, no scenario file moved, and CI
restored a previously generated tests/ directory together with the cache that
declared it current. Result: the suites kept executing the old packaged-binary
path, which no longer exists:

    /bin/sh: 1: /__w/.../packages/cli/binaries/spectral: not found

Two layers, because either alone leaves a hole.

generate-tests.ts now tracks __filename alongside the scenarios. If the generator
changed, every scenario is regenerated rather than only those with changed
inputs.

The CI cache key now includes hashFiles('test-harness/**') — and, critically, so
does the restore-keys PREFIX. The prefix was the actual hole: an exact-key miss
fell back to test-harness-<os>-<node>-, which happily restored tests generated by
an older generator.

Verified by reproducing the failure locally before fixing it. The subtlety is
that it only appears at steady state: after a run with an empty tests/ directory,
the next run sees the generated files as newly tracked and regenerates anyway. It
takes three runs to reach the state CI restores into.

  control, no fix, steady-state cache, generator changed  ->  dist/index.js  (stale)
  with fix,          same cache,       generator changed  ->  dist/FIXED.js  (correct)

Harness still passes 65/66 locally. The 66th is help-no-document, whose own
faketty.sh uses util-linux `script -qfec` syntax that BSD script rejects; it
fails identically with a trivial command and is unrelated.

Refs #35, #29, #33

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second Windows attempt, and the first one was wrong for a reason worth recording.

win.ts wraps every command in:

    powershell -Command "& { cd '<cwd>';<command>;echo LASTEXITCODE=$LASTEXITCODE }"

The whole thing is already inside double quotes. So emitting {bin} as

    "C:\Program Files\nodejs\node.exe" "...\dist\index.js"

terminated that wrapper, and PowerShell saw an unquoted path that split on its
space:

    & : The term 'C:\Program' is not recognized as the name of a cmdlet...

The call operator I added was firing correctly; the quoting around it was not
surviving. This never came up before because the old {bin} was a single path with
no spaces (binaries/spectral.exe), so it needed no quoting at all.

Quoting is now platform-aware: PowerShell single quotes are literal and pass
through the wrapper intact, while POSIX shells keep double quotes. win.ts
recognises either quote character when deciding to prefix the call operator.

Also fixes the prettier error the lint job caught on the previous commit.

Local state unchanged: 65/66 on macOS, the 66th being help-no-document's BSD
`script` incompatibility.

Windows remains unverified locally and cannot be verified locally. CI is the only
proof, and this is the second attempt at it — stated plainly rather than dressed
up as confidence.

Refs #35, #29

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kinlane
kinlane merged commit b640c8d into main Aug 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant