Files
gstack/test/plan-count-owned-permission.test.ts
T
Garry Tan 7b534d3e90 v1.90.2.0 perf: halve local free-suite time and preserve coverage (#2972)
* perf: remove repeated test work and preserve AUQ execution budgets

* fix: validate native evaluation fixture evidence at its actual boundaries

* fix: clarify deployment approval and recovery state transitions

* chore: document coverage and release v1.90.2.0

* test: preserve Windows scheduling and native no-change consent

* test: recognize verified reads through fixture symlinks

* test: isolate alias-name installation from runtime assets
2026-09-25 13:27:07 -04:00

63 lines
7.5 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 {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 {classifyPlanCountFrame,createPlanCountPermissionGuard,isPermissionDialogVisible} from './helpers/claude-pty-runner';
import capture from './fixtures/plan-count-owned-permission-v.json';
test('actual V owned-plan Edit pane is a one-time permission, not a review finding',()=>{
expect(capture.events.map(e=>e.block.type)).toEqual(['tool_use','tool_result','tool_use']);
expect(capture.events[0]!.block.id).not.toBe(capture.events[2]!.block.id);
expect(classifyPlanCountFrame(capture.screen)).toBe('permission');
const guard=createPlanCountPermissionGuard();expect(guard(capture.screen,'')).toBe('grant');expect(guard(capture.screen,'')).toBe('handled');
});
test('new permission grammar requires matching file pane and complete native permission footer',()=>{
const panel=capture.screen.slice(capture.screen.indexOf(' Edit file\n'));
for(const screen of [panel.replace(' Edit file\n PLAN.md\n',''),panel.replace(' Edit file\n PLAN.md',' Edit file\n OTHER.md'),panel.replace('Esc to cancel · Tab to amend','Esc to cancel'),panel.replace(' 3. No',' 3. Keep working'),'Example:\n'+panel,'```text\n'+panel,'☐ File policy\n'+panel,panel.replace(' ❯ 1. Yes',' 1. Yes')]){
expect(isPermissionDialogVisible(screen),screen).toBe(false);
expect(createPlanCountPermissionGuard()(screen,''),screen).not.toBe('grant');expect(classifyPlanCountFrame(screen),screen).not.toBe('permission');
}
});
test.skipIf(process.platform==='win32')('real fake CLI binds only report and owned PLAN epochs across redraws and path switches',async()=>{
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'owned-plan-epoch-')),fake=path.join(dir,'fake-claude'),worker=path.join(dir,'worker.ts'),events=path.join(dir,'events.jsonl'),output=path.join(dir,'result.json'),report=path.join(dir,'report.md');fs.writeFileSync(report,'original');
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.OWNED_EPOCH_CASE),sid='owned-main';const log=row=>fs.appendFileSync(item.events,JSON.stringify(row)+'\n');const plan=path.join(process.cwd(),'PLAN.md');
const nativePath=path.join(process.env.CLAUDE_CONFIG_DIR,'projects','owned',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:'Reviewing fixture.'}]);
const settings=JSON.parse(process.argv[process.argv.indexOf('--settings')+1]);if(settings.hooks.PreToolUse[0].matcher!=='^ExitPlanMode$')throw Error('Exit hook changed');
log({type:'start',pid:process.pid,cwd:process.cwd(),hookCount:settings.hooks.PreToolUse.filter(h=>h.matcher==='^(Write|Edit)$').length});
const hook=async(name,id,target)=>{
const event={hook_event_name:name,tool_name:'Edit',session_id:sid,tool_use_id:id,cwd:process.cwd(),transcript_path:nativePath,tool_input:{file_path:target,old_string:'DO_NOT_RETAIN_OLD',new_string:'DO_NOT_RETAIN_NEW'}};
for(const entry of settings.hooks[name]??[]){if(entry.matcher!=='^(Write|Edit)$')continue;const p=Bun.spawn(['bash','-c',entry.hooks[0].command],{stdin:new Blob([JSON.stringify(event)]),stdout:'pipe',stderr:'pipe'});const [code,out,err]=await Promise.all([p.exited,new Response(p.stdout).text(),new Response(p.stderr).text()]);if(code||out||err)throw Error('hook not silent');}
log({type:'hook',name,id,target});
};
let stage='startup';const paint=target=>process.stdout.write('\x1b[2J\x1b[H'+item.screen.replaceAll('PLAN.md',target).replaceAll('\n','\r\n'));const request=async(id,target)=>{await hook('PreToolUse',id,target);stage=id;paint(target);};
process.stdin.setRawMode?.(true);process.stdin.on('data',async data=>{
const input=data.toString();log({type:'input',stage,input});if(stage==='startup'){await request('report1',item.report);return;}
if(stage.startsWith('wait')||stage==='done'){log({type:'unexpected'});return;}if(input!=='1\r')throw Error('one-time input changed');
if(stage==='report1'){await hook('PostToolUse','report1',item.report);await request('plan1',plan);return;}
if(stage==='plan1'){stage='wait-old-plan';await hook('PostToolUse','plan1',plan);paint(plan);setTimeout(async()=>{await hook('PreToolUse','plan1',plan);await hook('PreToolUse','foreign',path.join(process.cwd(),'OTHER.md'));paint(plan);},1000);setTimeout(async()=>{await request('plan2',plan);},3200);return;}
if(stage==='plan2'){stage='wait-old-report';await hook('PostToolUse','plan2',plan);paint(item.report);setTimeout(async()=>{await request('report2',item.report);},3200);return;}
await hook('PostToolUse','report2',item.report);stage='done';const q={header:'Finding',question:'Apply the reviewed fix?',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 args={skillName:'plan-ceo-review',slashCommand:'/plan-ceo-review',followUpPrompt:'Review this owned fixture.',expectedPlanPath:report,reviewCountCeiling:1,timeoutMs:37000,startupReadyMarker:'PTY_READY:'+events,env:{OWNED_EPOCH_CASE:JSON.stringify({events,report,screen:capture.screen})}};
fs.writeFileSync(worker,`import {runPlanSkillCounting} from ${JSON.stringify(pathToFileURL(path.join(import.meta.dir,'helpers/claude-pty-runner.ts')).href)};const result=await runPlanSkillCounting({...${JSON.stringify(args)},isLastStep0AUQ:()=>false,isReviewAUQ:()=>true});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'}),killer=setTimeout(()=>child.kill('SIGKILL'),42000);
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.reviewCount).toBe(1);const rows=fs.readFileSync(events,'utf8').trim().split('\n').map(l=>JSON.parse(l));expect(rows[0].hookCount).toBe(2);expect(rows.filter(r=>r.type==='input').map(r=>[r.stage,r.input])).toEqual([['startup','/plan-ceo-review\r'],['report1','1\r'],['plan1','1\r'],['plan2','1\r'],['report2','1\r']]);expect(rows.some(r=>r.type==='unexpected')).toBe(false);expect(()=>process.kill(rows[0].pid,0)).toThrow();expect(fs.existsSync(rows[0].cwd)).toBe(false);
}finally{
clearTimeout(killer);child.kill('SIGKILL');await child.exited;
if(fs.existsSync(events)){
const first=JSON.parse(fs.readFileSync(events,'utf8').split('\n')[0]!);
try{
const owned=process.platform==='linux'
? fs.readFileSync(`/proc/${first.pid}/cmdline`,'utf8').split('\0').includes(fake)
: Bun.spawnSync(['ps','-p',String(first.pid),'-o','args='],{timeout:1000}).stdout.toString().includes(fake);
if(owned)process.kill(first.pid,'SIGKILL');
}catch{}
}
fs.rmSync(dir,{recursive:true,force:true});
}
},45000);