From 053def75eb05bd808bfe7f15e9c9bf74cd1bfcce Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 20:47:44 +0000 Subject: [PATCH] W7 R4: --model claude renders byte-identical Claude skills Regression contract for passing --model to every Claude render: gen:skill-docs:user --host claude --model claude must equal the render without --model, file for file. --- test/gen-skill-docs-out-dir.test.ts | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/gen-skill-docs-out-dir.test.ts b/test/gen-skill-docs-out-dir.test.ts index 37fbd90a5..3439b7492 100644 --- a/test/gen-skill-docs-out-dir.test.ts +++ b/test/gen-skill-docs-out-dir.test.ts @@ -182,6 +182,50 @@ describe('gen-skill-docs --out-dir (B2 render isolation)', () => { } }); + // W7 regression contract: ./setup now passes --model to every + // Claude render, and the default overlay is `claude`. That is only safe if + // `--model claude` renders exactly what the render without --model did. + test('gen:skill-docs:user --host claude --model claude is byte-identical to the render without --model', () => { + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-home-')); + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-model-claude-')); + const linkRoot = path.join(base, 'render', 'claude'); + const files = (dir: string): Map => { + const out = new Map(); + const walk = (d: string) => { + for (const entry of fs.readdirSync(d, { withFileTypes: true })) { + const p = path.join(d, entry.name); + if (entry.isDirectory()) walk(p); + else out.set(path.relative(dir, p), hashFile(p)); + } + }; + walk(dir); + return out; + }; + try { + fs.writeFileSync( + path.join(tmpHome, 'gbrain-detection.json'), + JSON.stringify({ gbrain_local_status: 'ok', gbrain_version: '9.9.9' }), + ); + const render = (out: string, extra: string[]) => spawnSync( + 'bun', + ['run', 'gen:skill-docs:user', '--host', 'claude', '--out-dir', out, '--link-root', linkRoot, ...extra], + { cwd: ROOT, encoding: 'utf-8', timeout: 120_000, env: { ...process.env, GSTACK_HOME: tmpHome } }, + ); + const plain = render(path.join(base, 'plain'), []); + expect(plain.status, plain.stderr).toBe(0); + const pinned = render(path.join(base, 'model-claude'), ['--model', 'claude']); + expect(pinned.status, pinned.stderr).toBe(0); + const a = files(path.join(base, 'plain')); + const b = files(path.join(base, 'model-claude')); + expect(a.size).toBeGreaterThan(50); + expect([...b.keys()].sort()).toEqual([...a.keys()].sort()); + expect([...a].filter(([rel, hash]) => b.get(rel) !== hash).map(([rel]) => rel)).toEqual([]); + } finally { + fs.rmSync(tmpHome, { recursive: true, force: true }); + fs.rmSync(base, { recursive: true, force: true }); + } + }, 240_000); + // ── External-host out-dir cases ───────────────────────────── // The former tree-mutating tests read codex/factory artifacts from out-dir // renders. That is only sound if an out-dir external render is (a) clean —