mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-28 07:32:14 +02:00
* 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
63 lines
7.5 KiB
TypeScript
63 lines
7.5 KiB
TypeScript
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);
|