Files
gstack/test/plan-count-preview-footer.test.ts
T
garrytan 6415690a18 test: retire the finding-count cluster and trim its helpers (C)
- C0/C1: the five never-green evals (skill-e2e-autoplan-chain and
  skill-e2e-plan-{ceo,eng,design,devex}-finding-count) failed on harness and
  budget, never on skill behavior; delete them, their touchfile/tier ids,
  AUTOPLAN_CHAIN_BUDGET and the dedicated eighth periodic slice (--slices 7).
- C2: delete the helper groups whose only paid consumers were those files
  (11 modules), trim claude-pty-runner and eng-seeded-coverage to the paid
  closure, and delete the free replay tests whose assertions exercised only
  that dead code (89 files, 135 orphaned fixtures). Blocks that used dead code
  only as input for a live subject keep their assertions: the multiSelect
  default moved to plan-review-decisions, runner PTY tests use inline caller
  policies, and the timer-safe budget checks moved to eng-finding-retry-budget.
- The eight production-touching files stay except ceo-current-decision-record
  (its template read only feeds the retired counter).
- CARVE_GUARDS.autoplan is behavioral 'none'; TODOS records the lost chain
  and per-finding cadence coverage with their re-entry tests.
2026-09-29 06:08:49 +00:00

113 lines
7.2 KiB
TypeScript

import {describe, test, expect} 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, nativePlanCallFingerprint, planCountQuestionInput} from './helpers/claude-pty-runner';
import type {NativePlanQuestionCall} from './helpers/plan-count-transcript';
import completed from './fixtures/ceo-preview-u-call.json';
const screen = fs.readFileSync(path.join(import.meta.dir, 'fixtures/ceo-preview-u-screen.txt'), 'utf8');
const designScreen = fs.readFileSync(path.join(import.meta.dir, 'fixtures/design-preview-v-screen.txt'), 'utf8');
// Project an unanswered call for regression execution. The actual U capture
// completed with option A; its 621ms pending state was not retained by observers.
function pending(): NativePlanQuestionCall {
const {answers, answeredAt, unansweredQuestionIndices, ...call} = structuredClone(completed);
return {...call, answered:false, failed:false};
}
describe('native question with preview and notes footer', () => {
test('the actual stalled Design preview requires submission even before native metadata flushes', () => {
const fp = capturePlanCountQuestion(designScreen, new Set(), 0, false)!;
expect(fp.nativeCall).toBeUndefined();
expect(planCountQuestionInput(designScreen, fp, 1)).toBe('1\r');
expect(planCountQuestionInput(designScreen, fp, 2)).toBe('2\r');
});
test('preview submission does not change ordinary native or checkbox shortcuts', () => {
const call = pending();
const fp = nativePlanCallFingerprint(call, 0, true);
const ordinary = screen.replace(' · n to add notes', '');
expect(planCountQuestionInput(ordinary, fp, 2)).toBe('2');
expect(planCountQuestionInput(screen.replace(/Notes:[^\n]*/g, ''), fp, 2)).toBe('2');
expect(planCountQuestionInput(screen.replace(' · Esc to cancel', ''), fp, 2)).toBe('2');
call.questions[0]!.multiSelect = true;
expect(planCountQuestionInput(screen, fp, 2)).toBe('2');
});
test('a preview inside a question packet still submits only its focused choice', () => {
const packet = screen.replace('☐ Approach', '← ☒ Context ☐ Approach ✔ Submit →')
.replace(' · Esc to cancel', ' · Tab to switch questions · Esc to cancel');
const fp = nativePlanCallFingerprint(pending(), 0, true);
expect(planCountQuestionInput(packet, fp, 2)).toBe('2\r');
});
});
test.skipIf(process.platform === 'win32')('real fake CLI focuses B and submits the preview only with Enter', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'count-preview-'));
const fake = path.join(dir, 'fake-claude');
const worker = path.join(dir, 'worker.ts');
const events = path.join(dir, 'events.jsonl');
const output = path.join(dir, 'output.json');
fs.writeFileSync(fake, `#!${process.execPath}\n` + String.raw`
import fs from 'node:fs';import path from 'node:path';
const item=JSON.parse(process.env.PREVIEW_CASE);const log=e=>fs.appendFileSync(item.events,JSON.stringify(e)+'\n');
const sid='preview-'+process.pid;const nativePath=path.join(process.env.CLAUDE_CONFIG_DIR,'projects',sid,sid+'.jsonl');fs.mkdirSync(path.dirname(nativePath),{recursive:true});
const native=(role,content,extra={})=>fs.appendFileSync(nativePath,JSON.stringify({cwd:process.cwd(),sessionId:sid,isSidechain:false,timestamp:new Date().toISOString(),message:{role,content},...extra})+'\n');
native('assistant',[{type:'text',text:'Fixture started.'}]);log({type:'start',pid:process.pid,cwd:process.cwd()});
let stage='startup';let focused=0;
process.stdin.setRawMode?.(true);process.stdin.on('data',data=>{
const input=data.toString();log({type:'input',stage,input});
if(stage==='startup'){
stage='question';native('assistant',[{type:'tool_use',name:'AskUserQuestion',id:'preview',input:{questions:[item.question]}}]);
process.stdout.write('\x1b[2J\x1b[H'+item.screen.replaceAll('\n','\r\n'));return;
}
if(stage!=='question'){log({type:'unexpected',input});return;}
const digit=/[1-9]/.exec(input)?.[0];
if(digit){const next=Number(digit)-1;log({type:'focus-request',next});setTimeout(()=>{focused=next;log({type:'focus',focused});},25);}
if(!input.includes('\r'))return;
stage='done';
const choice=item.question.options[focused]?.label;
native('user',[{type:'tool_result',tool_use_id:'preview',content:'Answered'}],{toolUseResult:{answers:{[item.question.question]:choice}}});
log({type:'choice',choice,input});
const q={header:'Finding',question:'Apply the repair?',options:[{label:'Fix'},{label:'Keep'}]};
native('assistant',[{type:'tool_use',name:'AskUserQuestion',id:'finding',input:{questions:[q]}}]);
native('user',[{type:'tool_result',tool_use_id:'finding',content:'Answered'}],{toolUseResult:{answers:{[q.question]:'Fix'}}});
process.stdout.write('\x1b[2J\x1b[HDone.\r\n');
});process.on('SIGINT',()=>process.exit(0));process.stdin.resume();
process.stdout.write('PTY_READY:'+item.events+'\x1b[2J\x1b[H');
`);
fs.chmodSync(fake, 0o755);
const runner=pathToFileURL(path.join(import.meta.dir,'helpers/claude-pty-runner.ts')).href;
fs.writeFileSync(worker, `import {runPlanSkillCounting} from ${JSON.stringify(runner)};
const result=await runPlanSkillCounting({skillName:'plan-ceo-review',slashCommand:'/plan-ceo-review',followUpPrompt:'Review this fixture.',isLastStep0AUQ:()=>true,defaultPick:2,reviewCountCeiling:1,timeoutMs:26000,startupReadyMarker:${JSON.stringify('PTY_READY:' + events)},env:{PREVIEW_CASE:${JSON.stringify(JSON.stringify({events, screen, question:completed.questions[0]}))}}});await Bun.write(${JSON.stringify(output)},JSON.stringify(result));`);
const child=Bun.spawn([process.execPath,worker], {env:{...process.env,BROWSE_TERMINAL_BINARY:fake,EVALS_HERMETIC:'1'},stdout:'pipe',stderr:'pipe'});
const timer=setTimeout(()=>child.kill('SIGKILL'),30000);
try {
const [code,out,err]=await Promise.all([child.exited,new Response(child.stdout).text(),new Response(child.stderr).text()]);
expect(code,out+err).toBe(0);
const result=JSON.parse(fs.readFileSync(output,'utf8'));
expect(result.outcome,JSON.stringify(result)).toBe('ceiling_reached');
expect(result.step0Count).toBe(1);
expect(result.reviewCount).toBe(1);
const rows=fs.readFileSync(events,'utf8').trim().split('\n').map(line=>JSON.parse(line));
expect(rows.filter(row=>row.type==='input').map(row=>row.input)).toEqual(['/plan-ceo-review\r','2','\r']);
expect(rows.find(row=>row.type==='choice').choice).toBe(completed.questions[0]!.options[1]!.label);
expect(rows.some(row=>row.type==='unexpected')).toBe(false);
expect(()=>process.kill(rows[0].pid,0)).toThrow();
expect(fs.existsSync(rows[0].cwd)).toBe(false);
} finally {
clearTimeout(timer);child.kill('SIGKILL');await child.exited;
if(fs.existsSync(events)) {
const first=JSON.parse(fs.readFileSync(events,'utf8').split('\n')[0]!);
try {
const argv = process.platform === 'linux'
? fs.readFileSync('/proc/' + first.pid + '/cmdline', 'utf8').split('\0')
: Bun.spawnSync(['ps', '-p', String(first.pid), '-o', 'command='], { timeout: 1_000 }).stdout.toString().trim().split(/\s+/);
if (argv.includes(fake)) process.kill(first.pid, 'SIGKILL');
} catch { /* owned child already closed */ }
}
fs.rmSync(dir,{recursive:true,force:true});
}
},35000);