mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 09:55:29 +02:00
* fix: report health failures and coverage accurately * chore: prepare health reporting release 1.87.4.0 * test: restrict routing evaluations to installed project skills * test: stabilize health selection and terminal fixtures
106 lines
9.1 KiB
TypeScript
106 lines
9.1 KiB
TypeScript
import {expect,test} from 'bun:test';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {pathToFileURL} from 'node:url';
|
|
import {classifyPlanCountFrame,createPlanCountPermissionGuard} from './helpers/claude-pty-runner';
|
|
import exact from './fixtures/plan-count-quoted-frame-ak.json';
|
|
import native from './fixtures/plan-count-permission-ac.json';
|
|
import owned from './fixtures/plan-count-owned-permission-v.json';
|
|
import {E2E_TOUCHFILES,selectTests} from './helpers/touchfiles';
|
|
const quote=(s:string)=>s.split('\n').map(row=>'> '+row).join('\n');
|
|
|
|
// A PTY transports bytes, not command-sized stdin events. Share the framing
|
|
// code with the fake CLI so fragmented grants exercise the same receiver.
|
|
function commandBuffer(){
|
|
let pending='';
|
|
return (chunk:string)=>{
|
|
pending+=chunk;const commands:string[]=[];let end:number;
|
|
while((end=pending.indexOf('\r'))!==-1){commands.push(pending.slice(0,end+1));pending=pending.slice(end+1);}
|
|
return commands;
|
|
};
|
|
}
|
|
test('fake CLI preserves command bytes across fragmented and coalesced PTY input',()=>{
|
|
const expected=['/plan-ceo-review\r','1\r'];
|
|
for(const chunks of [expected,['/plan-ceo-review\r','1','\r'],[...expected.join('')],[expected.join('')]]){
|
|
const receive=commandBuffer();expect(chunks.flatMap(receive)).toEqual(expected);
|
|
}
|
|
const receive=commandBuffer();
|
|
expect(receive('1')).toEqual([]);expect(receive('\r2\rtrailing')).toEqual(['1\r','2\r']);
|
|
expect(receive('\r')).toEqual(['trailing\r']); // no unexpected bytes are discarded
|
|
});
|
|
|
|
test('the exact wholly quoted AK pane is handled so the dispatcher sends no fallback',()=>{
|
|
const screen=quote(exact.screen);
|
|
expect(classifyPlanCountFrame(screen)).toBe('permission');
|
|
expect(createPlanCountPermissionGuard()(screen,'',undefined)).toBe('handled');
|
|
});
|
|
test('quoted whole native frames remain inert across redraw, indentation, CRLF and terminal color',()=>{
|
|
for(const source of [exact.screen,native.rows[0]!.screen,owned.screen])for(const transform of [
|
|
(s:string)=>quote(s),(s:string)=>'\n'+quote(s)+'\n',(s:string)=>quote(s).replace(/^>/gm,' >'),
|
|
(s:string)=>quote(s).replaceAll('\n','\r\n'),(s:string)=>'\x1b[31m'+quote(s)+'\x1b[0m',
|
|
]){const s=transform(source),g=createPlanCountPermissionGuard();
|
|
const expected=classifyPlanCountFrame(s)==='permission'?'handled':null;
|
|
expect(g(s,'')).toBe(expected);expect(g(s,'⎿ Wrote 4 lines\n')).toBe(expected);}
|
|
});
|
|
test('quoted history cannot consume or authorize the current native grant',()=>{
|
|
const s=native.rows[0]!.screen,g=createPlanCountPermissionGuard(),q=quote(s);
|
|
expect(g(q,'')).toBe('handled');expect(g(s,q)).toBe('grant');expect(g(q,s)).toBe('handled');expect(g(s,q)).toBe('handled');
|
|
expect(createPlanCountPermissionGuard()(s,'',null)).toBe('handled');
|
|
expect(createPlanCountPermissionGuard()(s,'',{pendingId:'main:first',completedId:null})).toBe('grant');
|
|
expect(createPlanCountPermissionGuard()(q,'',{pendingId:'main:first',completedId:null})).toBe('handled');
|
|
});
|
|
test('unquoted current native frames retain their policy even with quote characters in diff text or history',()=>{
|
|
for(const s of native.rows.map(row=>row.screen))expect(createPlanCountPermissionGuard()(s,quote(s))).toBe('grant');
|
|
expect(createPlanCountPermissionGuard()('> An old note\n'+native.rows[0]!.screen,'')).toBe('grant');
|
|
expect(createPlanCountPermissionGuard()('No current pane',quote(exact.screen))).toBeNull();
|
|
expect(createPlanCountPermissionGuard()('> Ordinary quoted prose, without a permission menu')).toBeNull();
|
|
});
|
|
test('the regression selects exactly the existing permission consumers',()=>{
|
|
for(const dependency of ['test/plan-count-quoted-frame-ak.test.ts','test/fixtures/plan-count-quoted-frame-ak.json'])
|
|
expect(selectTests([dependency],E2E_TOUCHFILES).selected.sort()).toEqual(selectTests(['test/plan-count-permission-ac.test.ts'],E2E_TOUCHFILES).selected.sort());
|
|
});
|
|
|
|
test.skipIf(process.platform==='win32')('real dispatcher ignores quoted pane then grants the fresh owned native request once',async()=>{
|
|
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'count-quoted-frame-')),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}\nconst receive=(${commandBuffer.toString()})();\n`+String.raw`
|
|
import fs from 'node:fs';import path from 'node:path';
|
|
const item=JSON.parse(process.env.QUOTED_FRAME_CASE),sid='quoted-frame-main',log=e=>fs.appendFileSync(item.events,JSON.stringify(e)+'\n');
|
|
const transcript=path.join(process.env.CLAUDE_CONFIG_DIR,'projects','owned',sid+'.jsonl');fs.mkdirSync(path.dirname(transcript),{recursive:true});
|
|
const native=(role,content,extra={})=>fs.appendFileSync(transcript,JSON.stringify({cwd:process.cwd(),sessionId:sid,isSidechain:false,timestamp:new Date().toISOString(),message:{role,content},...extra})+'\n');
|
|
native('assistant',[{type:'text',text:'Reviewing the owned fixture.'}]);log({type:'start',pid:process.pid,cwd:process.cwd()});
|
|
const settings=JSON.parse(process.argv[process.argv.indexOf('--settings')+1]);
|
|
const hook=async name=>{for(const entry of settings.hooks[name]??[]){if(entry.matcher!=='^(Write|Edit)$')continue;
|
|
const event={hook_event_name:name,tool_name:'Edit',session_id:sid,tool_use_id:'current',cwd:process.cwd(),transcript_path:transcript,tool_input:{file_path:item.report}};
|
|
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 failed');}};
|
|
const pane=item.screen.replaceAll('PLAN.md',item.report),paint=s=>process.stdout.write('\x1b[2J\x1b[H'+s.replaceAll('\n','\r\n'));
|
|
let stage='startup';process.stdin.setRawMode?.(true);const dispatch=async input=>{
|
|
log({type:'input',stage,input});
|
|
if(stage==='startup'){stage='quoted';paint(item.quotedScreen);setTimeout(async()=>{await hook('PreToolUse');stage='current';paint(pane);},4200);return;}
|
|
if(stage!=='current'){log({type:'unexpected'});return;}
|
|
if(input!=='1\r')throw Error('One-time grant changed');stage='done';await hook('PostToolUse');
|
|
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'}}});paint('Done.\n');
|
|
};process.stdin.on('data',async data=>{const chunk=data.toString();log({type:'chunk',stage,input:chunk});for(const input of receive(chunk))await dispatch(input);});process.on('SIGINT',()=>process.exit(0));process.stdin.resume();
|
|
`);fs.chmodSync(fake,0o755);
|
|
// Keep every physical terminal row inside the quote; adding a prefix to an
|
|
// already120-column capture would otherwise wrap an unquoted continuation.
|
|
const quotedScreen=exact.screen.split('\n').flatMap(row=>row.trimEnd().match(/.{1,116}/gu)??['']).map(row=>'> '+row).join('\n');
|
|
const args={skillName:'plan-ceo-review',slashCommand:'/plan-ceo-review',followUpPrompt:'Review the disposable fixture.',expectedPlanPath:report,reviewCountCeiling:1,timeoutMs:25000,env:{QUOTED_FRAME_CASE:JSON.stringify({events,report,screen:owned.screen,quotedScreen})}};
|
|
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'}),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')),rows=fs.readFileSync(events,'utf8').trim().split('\n').map(s=>JSON.parse(s));
|
|
expect(result.outcome,JSON.stringify({result,rows})).toBe('ceiling_reached');expect(result.reviewCount).toBe(1);
|
|
const chunks=rows.filter(r=>r.type==='chunk');
|
|
expect(chunks.every(r=>['startup','current'].includes(r.stage))).toBe(true);
|
|
expect(chunks.map(r=>r.input).join('')).toBe('/plan-ceo-review\r1\r');
|
|
expect(rows.filter(r=>r.type==='input').map(r=>[r.stage,r.input])).toEqual([['startup','/plan-ceo-review\r'],['current','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(timer);child.kill('SIGKILL');await child.exited;
|
|
if(fs.existsSync(events)){const first=JSON.parse(fs.readFileSync(events,'utf8').split('\n')[0]!);try{if(fs.readFileSync('/proc/'+first.pid+'/cmdline','utf8').split('\0').includes(fake))process.kill(first.pid,'SIGKILL');}catch{}}
|
|
fs.rmSync(dir,{recursive:true,force:true});}
|
|
},32000);
|