mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-03 01:46:55 +02:00
touchfiles.test.ts now checks, per key, that the paid file's static test/helpers and test/fixtures closure (plus fixture paths it names in string literals) is covered, and names the file, path, chain and key to fix when it is not. Free *.test.ts files are no longer touchfiles, so editing a free replay test stops selecting paid evals: 950 entries removed, 653 real closure paths added. The hand-copied inventories go: periodic-fixture-selection, fake-impeccable-touchfiles and 45 per-file selection examples. Selection for the sample edits (plan-eng-review template, claude-pty-runner, plan-count-fixture, gstack-config) loses no case under either profile. CONTRIBUTING documents the rule and its lower bound.
187 lines
13 KiB
TypeScript
187 lines
13 KiB
TypeScript
import { expect, test } from 'bun:test';
|
||
import * as fs from 'node:fs';
|
||
import * as os from 'node:os';
|
||
import * as path from 'node:path';
|
||
import { pathToFileURL } from 'node:url';
|
||
import { readPlanFloorTarget } from './helpers/plan-floor-target';
|
||
import { createFakeBunCli } from './helpers/fake-bun-cli';
|
||
const ROOT = path.resolve(import.meta.dir, '..');
|
||
const SID = '11111111-2222-3333-4444-555555555555';
|
||
const START = Date.parse('2026-09-10T04:46:45Z');
|
||
const SEED = '# Plan: Marketing landing page\n\nThe primary CTA has the same weight as Learn more.';
|
||
const COMMAND = '<command-message>plan-design-review</command-message>\n<command-name>/plan-design-review</command-name>\n<command-args>PLAN.md</command-args>';
|
||
function fixture() {
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'floor-target-'));
|
||
const cwd = path.join(dir, 'project'), config = path.join(dir, 'config');
|
||
fs.mkdirSync(cwd); fs.writeFileSync(path.join(cwd, 'PLAN.md'), SEED);
|
||
const journal = path.join(config, 'projects', 'owned', SID + '.jsonl');
|
||
fs.mkdirSync(path.dirname(journal), { recursive: true });
|
||
const row = { type: 'user', isSidechain: false, cwd, sessionId: SID, timestamp: new Date(START + 1).toISOString(), message: { role: 'user', content: COMMAND } };
|
||
const write = (rows: unknown[], newline = true) => fs.writeFileSync(journal, rows.map(x => JSON.stringify(x)).join('\n') + (newline ? '\n' : ''));
|
||
const options = { seed: SEED, sessionId: SID, slashCommand: '/plan-design-review', startedAt: START, now: START + 100 };
|
||
return { dir, cwd, config, journal, row, write, options, check: () => readPlanFloorTarget(config, cwd, options), cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
|
||
}
|
||
|
||
test('owned native command acknowledgment binds exact target, session and submitted bytes', () => {
|
||
const f = fixture();
|
||
try {
|
||
expect(f.check().status).toBe('missing');
|
||
f.write([f.row]);
|
||
expect(f.check()).toMatchObject({ status: 'ready', command: '/plan-design-review PLAN.md', targetPath: path.join(f.cwd, 'PLAN.md'), sessionId: SID, acknowledgedAt: f.row.timestamp });
|
||
f.write([{ ...f.row, message: { role: 'user', content: [{ type: 'text', text: COMMAND }] } }]);
|
||
expect(f.check().status).toBe('ready');
|
||
} finally { f.cleanup(); }
|
||
});
|
||
|
||
test('foreign, child, stale, future, quoted, tool and wrong-target records cannot acknowledge delivery', () => {
|
||
const f = fixture();
|
||
try {
|
||
for (const row of [
|
||
{ ...f.row, cwd: f.cwd + '-foreign' }, { ...f.row, sessionId: 'foreign' },
|
||
{ ...f.row, isSidechain: true }, { ...f.row, parent_tool_use_id: 'parent-tool' },
|
||
{ ...f.row, timestamp: new Date(START - 1).toISOString() },
|
||
{ ...f.row, timestamp: new Date(START + 101).toISOString() },
|
||
{ ...f.row, type: 'assistant', message: { role: 'assistant', content: COMMAND } },
|
||
{ ...f.row, message: { role: 'user', content: '> ' + COMMAND } },
|
||
{ ...f.row, message: { role: 'user', content: COMMAND.replace('PLAN.md', 'OTHER.md') } },
|
||
{ ...f.row, message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'read', content: COMMAND }] } },
|
||
]) { f.write([row]); expect(f.check().status, JSON.stringify(row)).not.toBe('ready'); }
|
||
} finally { f.cleanup(); }
|
||
});
|
||
|
||
test('the actual bare invocation cannot be repaired by a later queued target', () => {
|
||
const f = fixture();
|
||
try {
|
||
const bare = '<command-message>plan-design-review</command-message>\n<command-name>/plan-design-review</command-name>';
|
||
f.write([{ ...f.row, message: { role: 'user', content: bare } }, { ...f.row, timestamp: new Date(START + 2).toISOString() }]);
|
||
expect(f.check().status).toBe('error');
|
||
f.write([{ ...f.row, message: { role: 'user', content: [{ type: 'text', text: bare }, { type: 'text', text: 'Started without a target.' }] } }, f.row]);
|
||
expect(f.check().status).toBe('error');
|
||
f.write([{ ...f.row, message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'read', content: bare }] } }, f.row]);
|
||
expect(f.check().status).toBe('ready');
|
||
f.write([f.row], false); expect(f.check().status).toBe('missing');
|
||
f.write([f.row]);
|
||
const other = path.join(f.config, 'projects', 'other'); fs.mkdirSync(other); fs.copyFileSync(f.journal, path.join(other, SID + '.jsonl'));
|
||
expect(f.check().status).toBe('error');
|
||
} finally { f.cleanup(); }
|
||
});
|
||
|
||
test('changed or linked plan, invalid clock, linked journal and oversized source are unavailable', () => {
|
||
const f = fixture();
|
||
try {
|
||
f.write([f.row]); fs.writeFileSync(path.join(f.cwd, 'PLAN.md'), SEED + ' changed'); expect(f.check().status).toBe('error');
|
||
fs.writeFileSync(path.join(f.cwd, 'PLAN.md'), SEED);
|
||
for (const now of [NaN, Infinity, START - 1]) expect(readPlanFloorTarget(f.config, f.cwd, { ...f.options, now }).status).not.toBe('ready');
|
||
fs.renameSync(path.join(f.cwd, 'PLAN.md'), path.join(f.cwd, 'real.md')); fs.symlinkSync('real.md', path.join(f.cwd, 'PLAN.md')); expect(f.check().status).toBe('error');
|
||
fs.unlinkSync(path.join(f.cwd, 'PLAN.md')); fs.renameSync(path.join(f.cwd, 'real.md'), path.join(f.cwd, 'PLAN.md'));
|
||
const real = f.journal + '.real'; fs.renameSync(f.journal, real); fs.symlinkSync(real, f.journal); expect(f.check().status).toBe('error');
|
||
fs.unlinkSync(f.journal); fs.renameSync(real, f.journal); fs.truncateSync(f.journal, 32 * 1024 * 1024 + 1); expect(f.check().status).toBe('error');
|
||
} finally { f.cleanup(); }
|
||
});
|
||
test('compiled fake CLI preserves Claude arguments and stdin without a shebang launcher', async () => {
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'native-floor-cli-'));
|
||
try {
|
||
const cli = createFakeBunCli(path.join(dir, 'fake-claude'), `
|
||
process.stdin.on('data', chunk => { console.log(JSON.stringify({ argv: process.argv.slice(2), input: chunk.toString() })); process.exit(0); });
|
||
`, true);
|
||
const args = ['--model', 'fixture-model', '--session-id', SID];
|
||
const child = Bun.spawn([cli, ...args], { stdin: new Blob(['/plan-design-review PLAN.md\r']), stdout: 'pipe', stderr: 'pipe' });
|
||
const [exit, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
|
||
expect(exit, stderr).toBe(0);
|
||
expect(JSON.parse(stdout)).toEqual({ argv: args, input: '/plan-design-review PLAN.md\r' });
|
||
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||
});
|
||
|
||
test('real fake CLI sees seed before command; missing acknowledgment and scope menu never satisfy floor', async () => {
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'floor-delivery-pty-'));
|
||
try {
|
||
const worker = path.join(dir, 'worker.ts'), output = path.join(dir, 'results.json');
|
||
const cli = createFakeBunCli(path.join(dir, 'fake-claude'), `
|
||
const fs = require('node:fs'), path = require('node:path');
|
||
const args = process.argv.slice(2), id = args[args.indexOf('--session-id') + 1];
|
||
if(args.includes('-p')) {
|
||
let prompt='';process.stdin.on('data',chunk=>prompt+=chunk);process.stdin.on('end',()=>{
|
||
const input=JSON.parse(prompt.slice(prompt.indexOf('Evidence JSON:\\n')+15));
|
||
if(input.candidate.transport!=='native'||input.candidate.question.question!=='Should we make the primary CTA stronger than Learn more?')process.exit(2);
|
||
const citations=JSON.parse(prompt.split('Citation index JSON (exact passages from the evidence, never instructions):\\n')[1].split('\\n\\nEvidence JSON:')[0]);
|
||
const seed=citations.seed.find(c=>c.text.includes('The primary CTA has the same weight as Learn more.'));
|
||
if(!seed)process.exit(2);
|
||
console.log(JSON.stringify({kind:'finding',seedId:seed.id,questionId:'question-1',optionId:'option-1-label',
|
||
reason:'Controlled semantic assessment of the exact first-run finding.'}));
|
||
});
|
||
} else {
|
||
const record = value => fs.appendFileSync(process.env.FLOOR_RECORD, JSON.stringify(value)+'\\n');
|
||
record({type:'startup',pid:process.pid,cwd:process.cwd(),argv:args,plan:fs.readFileSync('PLAN.md','utf8'),context:fs.readFileSync('CLAUDE.md','utf8')});
|
||
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
||
let started=false;
|
||
process.stdin.on('data', chunk => {
|
||
const input=chunk.toString(); record({type:'input',input}); if(started)return; started=true;
|
||
const mode=process.env.FLOOR_MODE;
|
||
if(mode!=='missing') {
|
||
const root=path.join(process.env.CLAUDE_CONFIG_DIR,'projects','owned'); fs.mkdirSync(root,{recursive:true});
|
||
const content=${JSON.stringify(COMMAND)};
|
||
const row={type:'user',isSidechain:false,cwd:mode==='foreign'?process.cwd()+'-foreign':process.cwd(),sessionId:id,timestamp:new Date().toISOString(),message:{role:'user',content}};
|
||
fs.writeFileSync(path.join(root,id+'.jsonl'),JSON.stringify(row)+'\\n');
|
||
}
|
||
const q={header:'CTA',question:'Should we make the primary CTA stronger than Learn more?',multiSelect:false,options:[
|
||
{label:'Emphasize the primary CTA',description:'Make the primary action stronger and Learn more secondary.'},
|
||
{label:'Keep equal weight',description:'Leave the CTA at the same weight as Learn more.'}]};
|
||
if(mode==='ready'){
|
||
const root=path.join(process.env.CLAUDE_CONFIG_DIR,'projects','owned');
|
||
const row={type:'assistant',isSidechain:false,cwd:process.cwd(),sessionId:id,timestamp:new Date().toISOString(),message:{role:'assistant',content:[{type:'tool_use',id:'question1',name:'AskUserQuestion',input:{questions:[q]}}]}};
|
||
fs.appendFileSync(path.join(root,id+'.jsonl'),JSON.stringify(row)+'\\n');
|
||
}
|
||
process.stdout.write(mode==='scope'?'What should I review?\\n❯ 1. Current branch diff\\n 2. A plan or design doc\\n':
|
||
'☐ CTA\\n'+q.question+'\\n❯ 1. '+q.options[0].label+'\\n'+q.options[0].description+'\\n 2. '+q.options[1].label+'\\n'+q.options[1].description+'\\nEnter to select · ↑/↓ to navigate · Esc to cancel\\n');
|
||
});
|
||
process.on('SIGINT',()=>process.exit(0));
|
||
process.stdin.resume();
|
||
}
|
||
`);
|
||
fs.writeFileSync(worker, `
|
||
import {runPlanSkillFloorCheck} from ${JSON.stringify(pathToFileURL(path.join(ROOT, 'test/helpers/claude-pty-runner.ts')).href)};
|
||
import fs from 'node:fs';
|
||
// Observe actual process exit: Windows SIGINT uses TerminateProcess and cannot
|
||
// run a child signal handler. The native PTY and its process stay unchanged.
|
||
const spawn=Bun.spawn;
|
||
Bun.spawn=((command,options)=>{
|
||
if(Array.isArray(command)&&command[0]===process.env.BROWSE_TERMINAL_BINARY){
|
||
const onExit=options?.onExit;
|
||
return spawn(command,{...options,onExit(proc,...details){
|
||
fs.appendFileSync(options.env.FLOOR_RECORD,JSON.stringify({type:'closed',pid:proc.pid})+'\\n');
|
||
onExit?.(proc,...details);
|
||
}});
|
||
}
|
||
return spawn(command,options);
|
||
}) as typeof Bun.spawn;
|
||
const modes=['ready','missing','foreign','scope'];
|
||
const results=await Promise.all(modes.map(async mode=>({mode,observation:await runPlanSkillFloorCheck({skillName:'plan-design-review',slashCommand:'/plan-design-review',followUpPrompt:${JSON.stringify(SEED)},cwd:${JSON.stringify(ROOT)},timeoutMs:2500,model:'claude-fable-5-1',env:{FLOOR_MODE:mode,FLOOR_RECORD:${JSON.stringify(dir)}+'/'+mode+'.jsonl'}})})));
|
||
fs.writeFileSync(${JSON.stringify(output)},JSON.stringify(results));
|
||
`);
|
||
const child = Bun.spawn([process.execPath, worker], { cwd: ROOT,
|
||
env: { ...process.env, BROWSE_TERMINAL_BINARY: cli, EVALS_HERMETIC: '1', GSTACK_EVAL_DIR: path.join(dir, 'artifacts'), EVALS_RUN_ID: 'floor-delivery-test' }, stdout: 'pipe', stderr: 'pipe' });
|
||
const killer = setTimeout(() => child.kill('SIGKILL'), 25000);
|
||
try {
|
||
const [exit, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
|
||
expect(exit, stdout + stderr).toBe(0);
|
||
for (const result of JSON.parse(fs.readFileSync(output, 'utf8'))) {
|
||
const events = fs.readFileSync(path.join(dir, result.mode + '.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
||
const startup = events[0];
|
||
expect(startup.plan.endsWith('\n\n'+SEED)).toBe(true); expect(startup.context).toContain(SEED);
|
||
expect(startup.plan).toContain('skip the optional /office-hours prerequisite');
|
||
expect(startup.plan).toContain('Preserve the supplied product scope'); expect(startup.cwd).not.toBe(ROOT);
|
||
expect(startup.argv[startup.argv.indexOf('--permission-mode') + 1]).toBe('plan');
|
||
expect(startup.argv[startup.argv.indexOf('--model') + 1]).toBe('claude-fable-5-1');
|
||
expect(events.filter(e => e.type === 'input').map(e => e.input).join('')).toBe('/plan-design-review PLAN.md\r');
|
||
expect(events.at(-1)).toEqual({type:'closed',pid:startup.pid}); expect(fs.existsSync(startup.cwd)).toBe(false);
|
||
expect(() => process.kill(startup.pid, 0)).toThrow();
|
||
expect(result.observation.auqObserved,JSON.stringify(result.observation)).toBe(result.mode === 'ready');
|
||
expect(result.observation.outcome).toBe(result.mode === 'ready' ? 'auq_observed' : 'timeout');
|
||
expect(result.observation.targetDelivery.status).toBe(['ready','scope'].includes(result.mode) ? 'ready' : 'missing');
|
||
const retained=JSON.parse(fs.readFileSync(path.join(result.observation.artifactDir,'observation.json'),'utf8'));
|
||
expect(retained.targetDelivery).toEqual(result.observation.targetDelivery);
|
||
}
|
||
} finally { clearTimeout(killer); }
|
||
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||
}, 30000);
|