From 0778c70b9169e27cc3bdf1dba457a867f29d36fa Mon Sep 17 00:00:00 2001 From: garrytan Date: Wed, 30 Sep 2026 06:49:18 +0000 Subject: [PATCH] test: fold the moved-code touchfile golden into touchfiles.test.ts; fix integration fixture closure and caps --- test/fixtures/module-size-ratchet.json | 2 +- test/gen-skill-docs.test.ts | 5 +- test/test-free-shards.test.ts | 2 +- test/touchfile-moved-code.test.ts | 104 ------------------------- test/touchfiles.test.ts | 95 ++++++++++++++++++++++ 5 files changed, 100 insertions(+), 108 deletions(-) delete mode 100644 test/touchfile-moved-code.test.ts diff --git a/test/fixtures/module-size-ratchet.json b/test/fixtures/module-size-ratchet.json index 904b3950f..e6df12007 100644 --- a/test/fixtures/module-size-ratchet.json +++ b/test/fixtures/module-size-ratchet.json @@ -33,7 +33,7 @@ { "file": "test/helpers/pty/session.ts", "movedFrom": "test/helpers/claude-pty-runner.ts" } ], "residualFiles": [ - { "file": "scripts/test-free-shards.ts", "maxLines": 2356, "reason": "free lane policy after the shard engine took spawn, sandbox, logs, seeds and flags" }, + { "file": "scripts/test-free-shards.ts", "maxLines": 2361, "reason": "free lane policy after the shard engine took spawn, sandbox, logs, seeds and flags" }, { "file": "scripts/test-paid-shards.ts", "maxLines": 1921, "reason": "paid lane policy after the shard engine took sandbox, logs, seeds, verdicts and flags" }, { "file": "browse/src/server.ts", "maxLines": 2224, "reason": "server lifecycle, tunnel filter and fetch entry after the route table took dispatch, auth and handlers" }, { "file": "test/helpers/claude-pty-runner.ts", "maxLines": 30, "reason": "re-export barrel for the 117 importers after the split into test/helpers/pty/*" } diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index e4999aa6a..2f31f243f 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1659,8 +1659,9 @@ describe('SPEC_REVIEW_LOOP resolver', () => { expect(report).toContain('failed mkdir or append stops the review'); expect(report.replace(/\s+/g, ' ')).toContain('Recording the **0H spec-review metrics** is required when writing is permitted, even if the reviewer failed'); expect(report.replace(/\s+/g, ' ')).toContain('If the reviewer fails, report that limit and continue after recording the outcome; if a required save fails, stop before claiming completion'); - expect(report).toContain('mkdir -p ~/.gstack/analytics || exit 1'); - expect(report).toContain('>> ~/.gstack/analytics/spec-review.jsonl || exit 1'); + expect(report).toContain('eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"'); + expect(report).toContain('mkdir -p "$GSTACK_STATE_ROOT/analytics" || exit 1'); + expect(report).toContain('>> "$GSTACK_STATE_ROOT/analytics/spec-review.jsonl" || exit 1'); expect(report).not.toContain('Your doc survived'); }); diff --git a/test/test-free-shards.test.ts b/test/test-free-shards.test.ts index ddce2c8f5..715096b3b 100644 --- a/test/test-free-shards.test.ts +++ b/test/test-free-shards.test.ts @@ -467,7 +467,7 @@ describe('test-free-shards: exclusive host-state phase', () => { let child: ReturnType | undefined; let watchdog: ReturnType | undefined; try { - for (const file of ['scripts/test-free-shards.ts', 'scripts/test-strict-output.ts', 'scripts/lib/shard-engine.ts', + for (const file of ['scripts/test-free-shards.ts', 'scripts/test-strict-output.ts', 'scripts/lib/shard-engine.ts', 'lib/state-root.ts', 'test/helpers/paid-test-set.ts', 'test/helpers/touchfiles.ts', 'test/helpers/touchfiles-data.ts', 'test/helpers/test-selection.ts']) { const target = path.join(directory, file); fs.mkdirSync(path.dirname(target), { recursive: true }); diff --git a/test/touchfile-moved-code.test.ts b/test/touchfile-moved-code.test.ts deleted file mode 100644 index 092c6f9b2..000000000 --- a/test/touchfile-moved-code.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Touchfile coverage for moved code. - * - * Each JSON file in test/fixtures/touchfile-moved-code/ records, before a - * refactor moved code, the paid evals that touching each source file selected - * ("global" when the source was a global touchfile). Touching any module the - * code moved into must select a superset, so a move never silently drops eval - * coverage. A module key ending in "/" covers every .ts file under it. - */ - -import { describe, expect, test } from 'bun:test'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { E2E_TOUCHFILES, GLOBAL_TOUCHFILES, LLM_JUDGE_TOUCHFILES, selectTests } from './helpers/touchfiles'; - -const ROOT = path.resolve(import.meta.dir, '..'); -const GOLDEN_DIR = 'test/fixtures/touchfile-moved-code'; -const LANES = ['e2e', 'llmJudge'] as const; - -type Lane = typeof LANES[number]; -type Selection = 'global' | string[]; -type Maps = Record>; -interface Golden { - workstream: string; - recordedAt: string; - sources: Record>; - modules: Record; -} - -const MAPS: Maps = { e2e: E2E_TOUCHFILES, llmJudge: LLM_JUDGE_TOUCHFILES }; - -function expandModule(key: string): string[] { - if (!key.endsWith('/')) return [key]; - return fs.readdirSync(path.join(ROOT, key), { recursive: true }) - .map(String).filter(file => file.endsWith('.ts')).map(file => key + file.replace(/\\/g, '/')); -} - -export function selectionGaps(golden: Golden, maps: Maps, globals: string[], modulesFor = expandModule): string[] { - const gaps: string[] = []; - for (const [key, sources] of Object.entries(golden.modules)) { - for (const module of modulesFor(key)) { - for (const lane of LANES) { - const result = selectTests([module], maps[lane], globals); - if (result.reason.startsWith('global')) continue; - const selected = new Set(result.selected); - for (const source of sources) { - const recorded = golden.sources[source]?.[lane]; - if (recorded === undefined) { gaps.push(`${module} no recorded ${lane} selection for source ${source}`); continue; } - const missing = recorded === 'global' ? ['(every eval: source was a global touchfile)'] : recorded.filter(name => !selected.has(name)); - if (missing.length) gaps.push(`${module} ${lane} misses ${missing.join(', ')} (selected by ${source} before the move)`); - } - } - } - } - return gaps; -} - -function formatGaps(gaps: string[], file: string): string { - return [ - `Touchfile move coverage: ${gaps.length} gap(s):`, - ...gaps.map(gap => ` ${gap}`), - 'Rule: code moved out of a file keeps that file\'s paid-eval selection, so an edit to the new module still runs the evals the old file ran.', - 'Fix: add the new module (or a directory glob such as \'test/helpers/pty/**\') to every touchfile entry that lists its source file in test/helpers/touchfiles-data.ts (E2E, LLM judge, or GLOBAL_TOUCHFILES).', - `Golden: ${GOLDEN_DIR}/${file}; re-record a source's selection only for a deliberate, reviewed touchfile change.`, - ].join('\n'); -} - -const goldens = fs.readdirSync(path.join(ROOT, GOLDEN_DIR)).filter(file => file.endsWith('.json')).sort(); - -describe('touchfile coverage for moved code', () => { - test('at least one golden is recorded', () => { - expect(goldens.length).toBeGreaterThan(0); - }); - - test.each(goldens)('%s: every moved module exists and selects a superset of its sources', file => { - const golden = JSON.parse(fs.readFileSync(path.join(ROOT, GOLDEN_DIR, file), 'utf8')) as Golden; - for (const key of Object.keys(golden.modules)) { - const modules = expandModule(key); - expect(modules.length, `${key} in ${GOLDEN_DIR}/${file} names no files`).toBeGreaterThan(0); - for (const module of modules) expect(fs.existsSync(path.join(ROOT, module)), `${module} is listed in ${GOLDEN_DIR}/${file} but does not exist`).toBe(true); - } - const gaps = selectionGaps(golden, MAPS, GLOBAL_TOUCHFILES); - if (gaps.length) throw new Error(formatGaps(gaps, file)); - }); - - test('a planted module missing one source entry, or a dropped global source, is reported with the fix', () => { - const golden: Golden = { - workstream: 'planted', recordedAt: 'test', - sources: { - 'src/old.ts': { e2e: ['eval-a', 'eval-b'], llmJudge: [] }, - 'src/global.ts': { e2e: 'global', llmJudge: [] }, - }, - modules: { 'src/new/': ['src/old.ts'], 'src/moved-global.ts': ['src/global.ts'] }, - }; - const maps: Maps = { e2e: { 'eval-a': ['src/old.ts', 'src/new/**'], 'eval-b': ['src/old.ts'] }, llmJudge: {} }; - const gaps = selectionGaps(golden, maps, ['src/global.ts'], key => key.endsWith('/') ? ['src/new/module.ts'] : [key]); - expect(gaps).toEqual([ - 'src/new/module.ts e2e misses eval-b (selected by src/old.ts before the move)', - 'src/moved-global.ts e2e misses (every eval: source was a global touchfile) (selected by src/global.ts before the move)', - ]); - expect(formatGaps(gaps, 'planted.json')).toContain('Fix: add the new module'); - expect(formatGaps(gaps, 'planted.json')).toContain(`Golden: ${GOLDEN_DIR}/planted.json`); - }); -}); diff --git a/test/touchfiles.test.ts b/test/touchfiles.test.ts index 37551d5e4..732a21814 100644 --- a/test/touchfiles.test.ts +++ b/test/touchfiles.test.ts @@ -717,3 +717,98 @@ describe('derived touchfile closure', () => { expect(paid.filter(file => !keyed(file))).toEqual(Object.keys(KEYLESS_PAID).sort()); }); }); + +// Touchfile coverage for moved code. Each JSON file in +// test/fixtures/touchfile-moved-code/ records, before a refactor moved code, +// the paid evals that touching each source file selected ("global" when the +// source was a global touchfile). Touching any module the code moved into must +// select a superset, so a move never silently drops eval coverage. A module +// key ending in "/" covers every .ts file under it. +const GOLDEN_DIR = 'test/fixtures/touchfile-moved-code'; +const LANES = ['e2e', 'llmJudge'] as const; + +type Lane = typeof LANES[number]; +type Selection = 'global' | string[]; +type Maps = Record>; +interface Golden { + workstream: string; + recordedAt: string; + sources: Record>; + modules: Record; +} + +const MAPS: Maps = { e2e: E2E_TOUCHFILES, llmJudge: LLM_JUDGE_TOUCHFILES }; + +function expandModule(key: string): string[] { + if (!key.endsWith('/')) return [key]; + return fs.readdirSync(path.join(ROOT, key), { recursive: true }) + .map(String).filter(file => file.endsWith('.ts')).map(file => key + file.replace(/\\/g, '/')); +} + +function selectionGaps(golden: Golden, maps: Maps, globals: string[], modulesFor = expandModule): string[] { + const gaps: string[] = []; + for (const [key, sources] of Object.entries(golden.modules)) { + for (const module of modulesFor(key)) { + for (const lane of LANES) { + const result = selectTests([module], maps[lane], globals); + if (result.reason.startsWith('global')) continue; + const selected = new Set(result.selected); + for (const source of sources) { + const recorded = golden.sources[source]?.[lane]; + if (recorded === undefined) { gaps.push(`${module} no recorded ${lane} selection for source ${source}`); continue; } + const missing = recorded === 'global' ? ['(every eval: source was a global touchfile)'] : recorded.filter(name => !selected.has(name)); + if (missing.length) gaps.push(`${module} ${lane} misses ${missing.join(', ')} (selected by ${source} before the move)`); + } + } + } + } + return gaps; +} + +function formatGaps(gaps: string[], file: string): string { + return [ + `Touchfile move coverage: ${gaps.length} gap(s):`, + ...gaps.map(gap => ` ${gap}`), + 'Rule: code moved out of a file keeps that file\'s paid-eval selection, so an edit to the new module still runs the evals the old file ran.', + 'Fix: add the new module (or a directory glob such as \'test/helpers/pty/**\') to every touchfile entry that lists its source file in test/helpers/touchfiles-data.ts (E2E, LLM judge, or GLOBAL_TOUCHFILES).', + `Golden: ${GOLDEN_DIR}/${file}; re-record a source's selection only for a deliberate, reviewed touchfile change.`, + ].join('\n'); +} + +const goldens = fs.readdirSync(path.join(ROOT, GOLDEN_DIR)).filter(file => file.endsWith('.json')).sort(); + +describe('touchfile coverage for moved code', () => { + test('at least one golden is recorded', () => { + expect(goldens.length).toBeGreaterThan(0); + }); + + test.each(goldens)('%s: every moved module exists and selects a superset of its sources', file => { + const golden = JSON.parse(fs.readFileSync(path.join(ROOT, GOLDEN_DIR, file), 'utf8')) as Golden; + for (const key of Object.keys(golden.modules)) { + const modules = expandModule(key); + expect(modules.length, `${key} in ${GOLDEN_DIR}/${file} names no files`).toBeGreaterThan(0); + for (const module of modules) expect(fs.existsSync(path.join(ROOT, module)), `${module} is listed in ${GOLDEN_DIR}/${file} but does not exist`).toBe(true); + } + const gaps = selectionGaps(golden, MAPS, GLOBAL_TOUCHFILES); + if (gaps.length) throw new Error(formatGaps(gaps, file)); + }); + + test('a planted module missing one source entry, or a dropped global source, is reported with the fix', () => { + const golden: Golden = { + workstream: 'planted', recordedAt: 'test', + sources: { + 'src/old.ts': { e2e: ['eval-a', 'eval-b'], llmJudge: [] }, + 'src/global.ts': { e2e: 'global', llmJudge: [] }, + }, + modules: { 'src/new/': ['src/old.ts'], 'src/moved-global.ts': ['src/global.ts'] }, + }; + const maps: Maps = { e2e: { 'eval-a': ['src/old.ts', 'src/new/**'], 'eval-b': ['src/old.ts'] }, llmJudge: {} }; + const gaps = selectionGaps(golden, maps, ['src/global.ts'], key => key.endsWith('/') ? ['src/new/module.ts'] : [key]); + expect(gaps).toEqual([ + 'src/new/module.ts e2e misses eval-b (selected by src/old.ts before the move)', + 'src/moved-global.ts e2e misses (every eval: source was a global touchfile) (selected by src/global.ts before the move)', + ]); + expect(formatGaps(gaps, 'planted.json')).toContain('Fix: add the new module'); + expect(formatGaps(gaps, 'planted.json')).toContain(`Golden: ${GOLDEN_DIR}/planted.json`); + }); +});