Files
gstack/test/plan-count-checkbox.test.ts
T
Garry Tan 35dd014c58 v1.87.5.0 perf: remove idle waits from tests and CI planning (#2897)
* v1.87.5.0 perf: remove idle waits from tests and CI planning

* fix: settle split PTY redraws before routing input

* docs: record final burst-safe test benchmarks

* fix: keep cold-setup snapshot metadata dependency-free

* fix: avoid early-reader pipe races in artifact URL parsing

* fix: preserve safety matches for multiline command payloads

* fix: recognize concurrent CSO publication removal

* test: preload the UI design-review target before invocation

* docs: record validation blocker fixes

* fix: bind plan observer rejection to the invoked command

* fix: count only native design decisions in the UI gate

* docs: clarify UI-positive eval evidence requirements

* test: recognize native UI decisions without weakening finding counts

* test: decouple native UI evidence from question punctuation

* test: recognize concrete native UI decisions independently of prose format

* fix: retain failed eval logs under the hidden CI cache

* test: await telemetry completion instead of racing disk writes
2026-09-21 12:27:25 -04:00

147 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, 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 { capturePlanCountQuestion, planCountQuestionInput, planCountSubmissionInput } from './helpers/claude-pty-runner';
const captured = fs.readFileSync(path.join(import.meta.dir, 'fixtures/ceo-checkbox-l.screen.txt'), 'utf8');
const question = captured.slice(captured.lastIndexOf('←'));
const selected = question.replace(' 1. [ ]', ' 1. [✓]');
function focused(index: number) {
const base = selected.replace(' 1.', ' 1.');
if (index === 5) return base.replace(/^ +Submit$/m, ' Submit');
return base.replace(new RegExp(`^ +${index + 1}\\.`, 'm'), ` ${index + 1}.`);
}
describe('Native checkbox question controls', () => {
test('the captured unchecked menu toggles one choice and does not immediately toggle it off', () => {
expect(planCountSubmissionInput(captured)).toBeNull();
const fp = capturePlanCountQuestion(captured, new Set(), 0, false)!;
expect(planCountQuestionInput(captured, fp, 1)).toBe('1');
expect(planCountSubmissionInput(selected)).toBe('\t');
});
test('selected checkbox rows navigate one focus step at a time; only the actual button submits', () => {
for (let index = 0; index < 5; index++) expect(planCountSubmissionInput(focused(index))).toBe('\t');
expect(planCountSubmissionInput(focused(5))).toBe('\r');
expect(planCountSubmissionInput(focused(5).replace('Submit\n', 'Next\n'))).toBe('\r');
expect(planCountSubmissionInput('```text\nold code\n```\n' + selected)).toBe('\t');
});
test('quoted, incomplete, superseded and unselected panels cannot advance', () => {
for (const text of [
question,
selected.replace(/^ +Submit$/m, ''),
selected.replace(/Enter to select.*$/m, ''),
selected.replace('←', '> ←'),
selected.split('\n').map(line => ' ' + line).join('\n'),
'```text\n' + selected,
'Example panel:\n' + selected,
'Quoted source:\n' + selected,
selected + '\n☐ New question\n 1. Yes\n2. No\nEnter to select · ↑/↓ to navigate · Esc to cancel',
selected.replace(' 2. [ ]', ' 8. [ ]'),
selected.replace(' 1.', ' 1.').replace('6. Chat', ' 6. Chat'),
]) expect(planCountSubmissionInput(text)).toBeNull();
});
test.skipIf(process.platform === 'win32')('real PTY submits one selected checkbox with early or deferred native metadata', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'native-checkbox-'));
const fake = path.join(dir, 'fake-claude');
const worker = path.join(dir, 'worker.ts');
const output = path.join(dir, 'results.json');
const cases = [false, true].map(late => ({ late, log: path.join(dir, `${late}.jsonl`), report: path.join(dir, `${late}.md`) }));
fs.writeFileSync(fake, `#!${process.execPath}\n` + String.raw`
import * as fs from 'node:fs';
import * as path from 'node:path';
const item = JSON.parse(process.env.CHECKBOX_CASE);
const log = event => fs.appendFileSync(item.log, JSON.stringify(event) + '\n');
log({type:'startup',pid:process.pid});
const sessionId = 'checkbox-' + process.pid;
const project = path.join(process.env.CLAUDE_CONFIG_DIR, 'projects', sessionId);
fs.mkdirSync(project, {recursive:true});
const native = (role, content, extra={}) => fs.appendFileSync(path.join(project, sessionId+'.jsonl'), JSON.stringify({cwd:process.cwd(),sessionId,isSidechain:false,timestamp:new Date().toISOString(),message:{role,content},...extra})+'\n');
native('assistant',[{type:'text',text:'Fixture started.'}]);
const q = {header:'Test specification',question:'Which verification should the plan specify? <gstack-qid:checkbox-verification>',multiSelect:true,options:[{label:'Happy path'},{label:'Email failure'},{label:'Nil input'},{label:'SQL injection'}]};
const ask = () => native('assistant',[{type:'tool_use',name:'AskUserQuestion',id:'checkbox',input:{questions:[q]}}]);
let started=false, focus=0, checked=false, stage='question', done=false;
function render() {
let screen;
if(stage==='question') screen='← '+(checked?'☒':'☐')+' Test specification ✔ Submit →\n│ '+q.question+'\n'+[...q.options.map(o=>o.label),'Type something'].map((label,i)=>(focus===i?'':' ')+' '+(i+1)+'. ['+(i===0&&checked?'✓':' ')+'] '+label).join('\n')+'\n'+(focus===5?'':' ')+' Submit\n──────────────────\n 6. Chat about this\nEnter to select · ↑/↓ to navigate · Esc to cancel\n';
else screen='← ☒ Test specification ✔ Submit →\nReview your answers\nHappy path\nReady to submit your answers?\n1.Submit answers\n2.Cancel\n';
process.stdout.write('\x1b[2J\x1b[H'+screen.replace(/\n/g,'\r\n'));
log({type:'screen',stage,focus,checked});
}
process.stdin.setRawMode?.(true);
process.stdin.on('data', async data => {
const input=data.toString(); log({type:'input',input});
if(!started){started=true;if(!item.late)ask();render();return;}
if(done){log({type:'unexpected',input});return;}
if(stage==='confirm'){
if(input!=='\r'||!checked)throw Error('Unanswered confirmation');
if(item.late)ask();
native('user',[{type:'tool_result',tool_use_id:'checkbox',content:'Answered.'}],{toolUseResult:{answers:{[q.question]:q.options[0].label}}});
log({type:'completed',checked});done=true;
await Bun.sleep(10);
fs.writeFileSync(item.report,'# Reviewed plan\n\n## GSTACK REVIEW REPORT\n\n| Review | Status | Findings |\n|---|---|---|\n| Eng | complete | resolved |\n\nVERDICT: READY\n\nNO UNRESOLVED DECISIONS\n');
native('assistant',[{type:'text',text:'Eng review complete.'}]);
process.stdout.write('\x1b[2J\x1b[H## Completion Summary\r\nEng review complete.\r\n');return;
}
for(const key of input){
if(key==='1')checked=!checked;
else if(key==='\t')focus=Math.min(5,focus+1);
else if(key==='\r'){if(focus===5)stage='confirm';else if(focus===0)checked=!checked;}
else throw Error('Unexpected checkbox input '+JSON.stringify(input));
}
render();
});
process.on('SIGINT',()=>process.exit(0));
process.stdin.resume();
process.stdout.write('PTY_READY:'+item.log+'\x1b[2J\x1b[H');
`);
fs.chmodSync(fake, 0o755);
fs.writeFileSync(worker, `import {runPlanSkillCounting} from ${JSON.stringify(pathToFileURL(path.resolve(import.meta.dir, 'helpers/claude-pty-runner.ts')).href)};
const cases=${JSON.stringify(cases)};
const results=await Promise.all(cases.map(async item=>({late:item.late,observation:await runPlanSkillCounting({skillName:'plan-eng-review',slashCommand:'/plan-eng-review',followUpPrompt:'Review this fixture.',isLastStep0AUQ:()=>false,isReviewAUQ:()=>true,reviewCountCeiling:8,timeoutMs:48000,startupReadyMarker:'PTY_READY:'+item.log,expectedPlanPath:item.report,env:{CHECKBOX_CASE:JSON.stringify(item)}})})));
await Bun.write(${JSON.stringify(output)},JSON.stringify(results));`);
const child = Bun.spawn([process.execPath, worker], {
env: { ...process.env, BROWSE_TERMINAL_BINARY: fake, EVALS_HERMETIC: '1' }, stdout: 'pipe', stderr: 'pipe',
});
const watchdog = setTimeout(() => child.kill('SIGKILL'), 48000);
try {
const [code, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
expect(code, stdout + stderr).toBe(0);
const results = JSON.parse(fs.readFileSync(output, 'utf8'));
for (const item of cases) {
const observation = results.find((row: any) => row.late === item.late).observation;
expect(observation.outcome, JSON.stringify(observation)).toBe('completion_summary');
expect(observation.reviewCount).toBe(1);
expect(observation.transcript.calls).toHaveLength(1);
expect(observation.transcript.calls[0].unansweredQuestionIndices).toEqual([]);
expect(Object.values(observation.transcript.calls[0].answers)).toEqual(['Happy path']);
const events = fs.readFileSync(item.log, 'utf8').trim().split('\n').map(line => JSON.parse(line));
expect(events.filter(event => event.type === 'completed')).toHaveLength(1);
expect(events.filter(event => event.type === 'unexpected')).toHaveLength(0);
expect(events.filter(event => event.type === 'input').slice(1).map(event => event.input))
.toEqual(['1', '\t', '\t', '\t', '\t', '\t', '\r', '\r']);
expect(fs.readFileSync(item.report, 'utf8')).toEndWith('NO UNRESOLVED DECISIONS\n');
}
} finally {
clearTimeout(watchdog);
child.kill('SIGKILL');
await child.exited;
for (const item of cases) {
if (!fs.existsSync(item.log)) continue;
const startup = fs.readFileSync(item.log, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line)).find(event => event.type === 'startup');
if (!startup || !Number.isInteger(startup.pid) || startup.pid <= 0) continue;
try {
if (process.platform === 'linux' && !fs.readFileSync(`/proc/${startup.pid}/cmdline`, 'utf8').includes(fake)) continue;
process.kill(startup.pid, 'SIGKILL');
} catch { /* the owned fake child already exited */ }
}
fs.rmSync(dir, { recursive: true, force: true });
}
}, 55000);
});