mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 17:04:27 +01:00
test: isolate model and brain-aware skill renders
This commit is contained in:
1 parent
6009ba4e37
commit
e8470a7e44
3 files changed
+40
-72
No files matched your search
@@ -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 () => {
|
||||
|
||||
@@ -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']");
|
||||
|
||||
@@ -7,14 +7,12 @@
|
||||
* `gbrain put office-hours/<slug>` 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 });
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user