From ecd3caa2d62fe395d9897de1e69bdb781e16e60b Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 3 Nov 2026 09:59:11 +0000 Subject: [PATCH] fix(harness-version): a pin ahead of the manifest version counts as drift (GST-1084) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- scripts/bump-harness-version.ts | 4 ++-- test/harness-version.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/scripts/bump-harness-version.ts b/scripts/bump-harness-version.ts index d047fabbc..7f7d7d36a 100644 --- a/scripts/bump-harness-version.ts +++ b/scripts/bump-harness-version.ts @@ -66,11 +66,11 @@ export function workingHarnessFiles(root: string, patterns: readonly string[]): return tracked.map((entry, index) => ({ file: entry.file, blob: blobs[index]! })); } -/** Harness files whose blob differs from, is missing from, or no longer matches an entry in the manifest. */ +/** Harness files whose blob differs from, is missing from, or no longer matches an entry in the manifest; a pin ahead of the manifest version (a bump that failed partway) counts as changed. */ export function harnessDrift(manifest: HarnessManifest, files: TreeEntry[]): Drift { const current = new Map(files.map(entry => [entry.file, entry.blob])); return { - changed: files.filter(entry => manifest.files[entry.file] && manifest.files[entry.file]!.blob !== entry.blob).map(entry => entry.file).sort(), + changed: files.filter(entry => manifest.files[entry.file] && (manifest.files[entry.file]!.blob !== entry.blob || manifest.files[entry.file]!.since > manifest.version)).map(entry => entry.file).sort(), added: files.filter(entry => !manifest.files[entry.file]).map(entry => entry.file).sort(), removed: Object.keys(manifest.files).filter(file => !current.has(file)).sort(), }; diff --git a/test/harness-version.test.ts b/test/harness-version.test.ts index 5ccd013cd..5cb23c9c4 100644 --- a/test/harness-version.test.ts +++ b/test/harness-version.test.ts @@ -105,6 +105,31 @@ describe('HARNESS_VERSION guard', () => { } }); + // Value: protects=a --bump that fails partway (pins written, manifest version not) cannot pass the guard with behavior pooled into the + // old series; fails_when=a pin ahead of the manifest version reads as no drift; seam=writeManifest failing on a real write, then harnessDrift + test('a bump that fails partway leaves a pin ahead of the manifest version, and the guard reports it (GST-1084)', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-partial-')); + try { + copyManifest(dir); + const live = readManifest(dir); + const current = workingHarnessFiles(ROOT, live.patterns); + const [first, second] = current.map(entry => entry.file); + const blocked = path.join(dir, PINS_DIR, `${second}.json`); + const original = fs.readFileSync(blocked, 'utf8'); + fs.rmSync(blocked); + fs.mkdirSync(blocked); + expect(() => writeManifest(dir, decide(live, current, 'bump', 'behavior changed in the runner'))).toThrow(); + fs.rmdirSync(blocked); + fs.writeFileSync(blocked, original); + const after = readManifest(dir); + expect(after.version).toBe(live.version); + expect(after.files[first!]!.since).toBe(live.version + 1); + expect(harnessDrift(after, current).changed).toEqual([first]); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + // Value: protects=PRs that record decisions for different harness files merge without a conflict (the one-line-per-file manifest // conflicted on adjacent lines and on the trailing comma); fails_when=two non-behavioral decisions on adjacent pins conflict in a // real merge, or two decisions on one pin merge silently; seam=writeManifest and git merge-tree --write-tree on two branches