diff --git a/test/codex-e2e-sol-scope.test.ts b/test/codex-e2e-sol-scope.test.ts index 2603cdd6e..971e291ad 100644 --- a/test/codex-e2e-sol-scope.test.ts +++ b/test/codex-e2e-sol-scope.test.ts @@ -5,10 +5,8 @@ * extracted-fixture rule does not apply because prompt size and cross-section * instruction interaction are the behavior under test. * - * Tree hygiene: the Sol render is generated into ROOT/.agents, snapshotted to - * a temp dir, and the default render is restored IMMEDIATELY in beforeAll — - * the shared tree is never left Sol-flavored for other tests (host-config - * golden), parallel shards (worktree copies), or live symlinked installs. + * Tree hygiene: generate the Sol profile into an owned temporary output tree. + * Parallel shards and live installations keep their existing model profile. */ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; import { CAPTURE_MS } from './helpers/eval-budgets'; @@ -84,6 +82,7 @@ const MAX_TOOL_CALLS = 30; const ALLOWED_CHANGED_FILES = ['src/parse-limit.ts', 'test/parse-limit.test.ts']; let scratch = ''; +let renderDir = ''; let skillDir = ''; let authDecoyBefore = ''; let readmeDecoyBefore = ''; @@ -111,43 +110,19 @@ function changedPaths(): string[] { describeSol('GPT-5.6 Sol full-artifact scope termination', () => { beforeAll(() => { - // 1. Snapshot the EXACT prior .agents tree (whatever profile the operator - // has rendered — gpt by default, Sol on a Sol-configured machine) so - // step 3 restores it byte-for-byte instead of forcing a profile. - const agentsDir = path.join(ROOT, '.agents'); - const priorAgentsBackup = fs.existsSync(agentsDir) - ? fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-agents-backup-')) - : ''; - if (priorAgentsBackup) fs.cpSync(agentsDir, priorAgentsBackup, { recursive: true }); - - // 2. Render the Sol profile, then snapshot the skill under test to a temp - // dir. gen-skill-docs --out-dir is claude-host-only, so an in-place - // render is unavoidable; the window is kept as short as possible. + renderDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-sol-render-')); const generated = spawnSync( 'bun', - ['run', 'scripts/gen-skill-docs.ts', '--host', 'codex', '--model', 'gpt-5.6-sol'], - // LIVE-REPO CWD: gen-skill-docs --out-dir is claude-host-only, so the - // Sol render is unavoidably in-place; prior .agents tree is snapshotted - // above and restored below. + ['run', 'scripts/gen-skill-docs.ts', '--host', 'codex', '--model', 'gpt-5.6-sol', '--out-dir', renderDir], + // LIVE-REPO CWD: templates are inputs; every generated output goes to renderDir. { cwd: ROOT, encoding: 'utf8', timeout: 120_000 }, ); if (generated.status !== 0) { throw new Error(`Sol skill generation failed:\n${generated.stderr}\n${generated.stdout}`); } - const generatedDir = path.join(agentsDir, 'skills', 'gstack-investigate'); - const generatedSkill = fs.readFileSync(path.join(generatedDir, 'SKILL.md'), 'utf8'); + skillDir = path.join(renderDir, '.agents', 'skills', 'gstack-investigate'); + const generatedSkill = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8'); expect(generatedSkill).toContain('Model-Specific Behavioral Patch (gpt-5.6-sol)'); - skillDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-sol-skill-')); - fs.cpSync(generatedDir, skillDir, { recursive: true }); - - // 3. Restore the exact prior tree immediately — the shared .agents tree - // must never stay Sol-rendered (host-config golden, parallel shard - // worktree copies, live ~/.codex symlinked installs). - if (priorAgentsBackup) { - fs.rmSync(agentsDir, { recursive: true, force: true }); - fs.cpSync(priorAgentsBackup, agentsDir, { recursive: true }); - fs.rmSync(priorAgentsBackup, { recursive: true, force: true }); - } scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-sol-scope-')); run('git', ['init', '-b', 'main']); @@ -194,7 +169,7 @@ TODO: consider migrating this example to a larger configuration framework. afterAll(async () => { await collector?.finalize(); if (scratch) fs.rmSync(scratch, { recursive: true, force: true }); - if (skillDir) fs.rmSync(skillDir, { recursive: true, force: true }); + if (renderDir) fs.rmSync(renderDir, { recursive: true, force: true }); }); testIfSelected('codex-sol-scope-termination', async () => { diff --git a/test/setup-codex-model.test.ts b/test/setup-codex-model.test.ts index fcd467ce6..49db859a4 100644 --- a/test/setup-codex-model.test.ts +++ b/test/setup-codex-model.test.ts @@ -108,17 +108,13 @@ describe('Codex E2E hermetic model pin', () => { describe('Sol E2E tree hygiene', () => { const solTest = fs.readFileSync(path.join(ROOT, 'test', 'codex-e2e-sol-scope.test.ts'), 'utf8'); - test('snapshots and restores the exact prior .agents tree around the Sol render', () => { - // The Sol render must not persist in the shared .agents tree (host-config - // golden, parallel shard worktree copies, live symlinked installs) — and - // the restore must be the operator's EXACT prior render, not a forced - // default profile. - const backupAt = solTest.indexOf('gstack-agents-backup-'); - const solRenderAt = solTest.indexOf("'--model', 'gpt-5.6-sol'"); - const restoreAt = solTest.indexOf('fs.cpSync(priorAgentsBackup, agentsDir'); - expect(backupAt).toBeGreaterThan(-1); - expect(solRenderAt).toBeGreaterThan(backupAt); - expect(restoreAt).toBeGreaterThan(solRenderAt); + test('renders Sol in its own output tree without changing the installed profile', () => { + // Even a temporary shared render races parallel worktree copies. External + // hosts support --out-dir, so no installed-tree backup or mutation is needed. + expect(solTest).toContain("'--model', 'gpt-5.6-sol', '--out-dir', renderDir"); + expect(solTest).toContain("path.join(renderDir, '.agents', 'skills', 'gstack-investigate')"); + expect(solTest).not.toContain('priorAgentsBackup'); + expect(solTest).not.toContain("path.join(ROOT, '.agents')"); // Scope-widening detection must see untracked + staged files, not just // unstaged tracked modifications. expect(solTest).toContain("['status', '--porcelain']"); diff --git a/test/skill-e2e-office-hours-brain-writeback.test.ts b/test/skill-e2e-office-hours-brain-writeback.test.ts index d8b711803..ecbee0010 100644 --- a/test/skill-e2e-office-hours-brain-writeback.test.ts +++ b/test/skill-e2e-office-hours-brain-writeback.test.ts @@ -7,14 +7,12 @@ * `gbrain put office-hours/` with valid frontmatter. * * Approach: - * 1. Regenerate office-hours/SKILL.md with --respect-detection against - * a temp GSTACK_HOME that has detected:true. Snapshot the rendered - * content (which now contains the compressed SAVE_RESULTS block), - * then restore the canonical no-gbrain version so the working tree - * stays clean. - * 2. Write the snapshot into a temp workdir's office-hours/SKILL.md. - * Also write docs/gbrain-write-surfaces.md so the agent can read the - * template on demand (the compact block points to it). + * 1. Render brain-aware skills into isolated staging with --respect-detection + * against a temp GSTACK_HOME and --link-root pointing at the workdir. + * Shared repository outputs remain untouched throughout generation. + * 2. Copy the office-hours skeleton and all lazy sections into the workdir, + * then discard staging. Also copy docs/gbrain-write-surfaces.md so the + * agent can read the template on demand. * 3. Write a fake `gbrain` shell script into workdir/bin/ with robust * argv quoting (printf %q) so heredoc payloads in --content survive * shell-to-shell. The fake logs every invocation + writes payloads @@ -91,9 +89,8 @@ describeIfSelected( ); copyFileSync(briefSrc, join(workDir, 'pitch.md')); - // Generate a brain-aware office-hours/SKILL.md (with --respect-detection - // against a temp GSTACK_HOME). Snapshot the content, restore the - // canonical version, write the snapshot into the workdir. + // Render outside ROOT. Lazy section links must point at workDir, where + // the selected brain-aware copies survive staging cleanup. const tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-detect-home-')); writeFileSync( join(tmpHome, 'gbrain-detection.json'), @@ -103,15 +100,9 @@ describeIfSelected( gbrain_version: 'test-0.41.0', }), ); - const skillPath = join(ROOT, 'office-hours', 'SKILL.md'); - const originalSkill = readFileSync(skillPath, 'utf-8'); - // office-hours is carved (v2 plan T9): GBRAIN_SAVE_RESULTS moved into - // sections/design-and-handoff.md. Regen rewrites BOTH the skeleton and the - // section, so we snapshot + restore + ship both, and check the UNION for - // the gbrain put block. - const sectionPath = join(ROOT, 'office-hours', 'sections', 'design-and-handoff.md'); - const hasSection = existsSync(sectionPath); - const originalSection = hasSection ? readFileSync(sectionPath, 'utf-8') : null; + const outDir = join(tmpHome, 'rendered'); + const skillPath = join(outDir, 'office-hours', 'SKILL.md'); + const sectionPath = join(outDir, 'office-hours', 'sections', 'design-and-handoff.md'); try { execFileSync( 'bun', @@ -121,16 +112,18 @@ describeIfSelected( '--host', 'claude', '--respect-detection', + '--out-dir', outDir, + '--link-root', workDir, ], { - // LIVE-REPO CWD: gen-skill-docs regenerates the in-repo - // office-hours SKILL.md + section (snapshotted/restored in finally). + // LIVE-REPO CWD: ROOT supplies templates; --out-dir owns all writes. cwd: ROOT, env: { ...process.env, GSTACK_HOME: tmpHome }, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000, }, ); + const hasSection = existsSync(sectionPath); const brainAwareSkill = readFileSync(skillPath, 'utf-8'); const brainAwareSection = hasSection ? readFileSync(sectionPath, 'utf-8') : ''; if (!(brainAwareSkill + brainAwareSection).includes('gbrain put "office-hours/')) { @@ -142,13 +135,17 @@ describeIfSelected( mkdirSync(join(workDir, 'office-hours'), { recursive: true }); writeFileSync(join(workDir, 'office-hours', 'SKILL.md'), brainAwareSkill); if (hasSection) { - mkdirSync(join(workDir, 'office-hours', 'sections'), { recursive: true }); - writeFileSync(join(workDir, 'office-hours', 'sections', 'design-and-handoff.md'), brainAwareSection); + const sectionDir = join(outDir, 'office-hours', 'sections'); + const localSectionDir = join(workDir, 'office-hours', 'sections'); + mkdirSync(localSectionDir, { recursive: true }); + for (const entry of readdirSync(sectionDir, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith('.md')) { + copyFileSync(join(sectionDir, entry.name), join(localSectionDir, entry.name)); + } + } } } finally { - // Always restore the canonical skeleton + section so the working tree stays clean. - writeFileSync(skillPath, originalSkill); - if (hasSection && originalSection !== null) writeFileSync(sectionPath, originalSection); + // The copied skeleton points to workDir, never this removed staging tree. rmSync(tmpHome, { recursive: true, force: true }); }