Files
gstack/test/plan-count-file-permission.test.ts
T
garrytan 7cbb5e04e4 test: derive paid touchfiles from each eval's static closure (E)
touchfiles.test.ts now checks, per key, that the paid file's static
test/helpers and test/fixtures closure (plus fixture paths it names in string
literals) is covered, and names the file, path, chain and key to fix when it is
not. Free *.test.ts files are no longer touchfiles, so editing a free replay
test stops selecting paid evals: 950 entries removed, 653 real closure paths
added. The hand-copied inventories go: periodic-fixture-selection,
fake-impeccable-touchfiles and 45 per-file selection examples. Selection for
the sample edits (plan-eng-review template, claude-pty-runner,
plan-count-fixture, gstack-config) loses no case under either profile.
CONTRIBUTING documents the rule and its lower bound.
2026-09-29 07:00:28 +00:00

866 lines
65 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,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 {createFilePermissionRecorder,recordFilePermission,currentFilePermissionEpoch} from './helpers/plan-count-file-permission';
import {createPlanCountPermissionGuard,classifyPlanCountFrame} from './helpers/claude-pty-runner';
import captured from './fixtures/plan-count-edit-permission-t.json';
import capturedAc from './fixtures/plan-count-permission-ac.json';
import largeCeo from './fixtures/ceo-report-permission-fb10.json';
function fixture() {
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'count-file-epoch-'));const cwd=path.join(dir,'cwd');fs.mkdirSync(cwd);
const config=path.join(dir,'.claude');const expected=path.join(dir,'report.md');fs.writeFileSync(expected,'original');
const recorder=createFilePermissionRecorder(cwd,config,expected)!;const startedAt=Date.now()-1000;
const screen=captured.screen.replaceAll(captured.expectedPath,expected).replace('../gstack-e2e-plan-ceo-paired-2Rv5Bi/gstack-test-plan-ceo-paired.md','../report.md').replaceAll('gstack-test-plan-ceo-paired.md','report.md');
const transcript:any={status:'ready',calls:[],assistantMessages:[{sessionId:'main',text:'Reviewing',timestamp:new Date().toISOString()}]};
const event=(name:string,id:string,extra={})=>({hook_event_name:name,tool_name:'Edit',session_id:'main',tool_use_id:id,cwd,transcript_path:path.join(config,'projects','owned','main.jsonl'),tool_input:{file_path:expected,old_string:'NEVER_SAVE_OLD',new_string:'NEVER_SAVE_NEW'},...extra});
const record=(name:string,id:string,extra={})=>recordFilePermission(JSON.stringify(event(name,id,extra)),recorder.file,cwd,config,expected);
const read=()=>currentFilePermissionEpoch(recorder.file,expected,cwd,config,startedAt,transcript,screen);
return {dir,cwd,config,expected,recorder,startedAt,screen,transcript,event,record,read,close(){recorder.dispose();fs.rmSync(dir,{recursive:true,force:true});}};
}
describe('native repeated report permission identity',()=>{
test('captured prompt is recognized but unchanged visible-only dedup cannot release its second Edit',()=>{
expect(classifyPlanCountFrame(captured.screen)).toBe('permission');
const guard=createPlanCountPermissionGuard();const history='⎿ Wrote32lines\n';
expect(guard(captured.screen,history)).toBe('grant');
expect(guard(captured.screen,history)).toBe('handled');
expect(captured.pendingEditId).toBeNull();
});
test('late success and old pane redraw do not release until a distinct current request',()=>{
const f=fixture();try{
const guard=createPlanCountPermissionGuard();const input=()=>guard(f.screen,'',f.read());
expect(input()).toBe('handled');
f.record('PreToolUse','first');expect(input()).toBe('grant');expect(input()).toBe('handled');
f.record('PostToolUse','first');expect(input()).toBe('handled');
f.record('PreToolUse','first');expect(input()).toBe('handled');
f.record('PreToolUse','second');expect(input()).toBe('grant');expect(input()).toBe('handled');
f.record('PostToolUse','first');expect(input()).toBe('handled');
f.record('PostToolUse','second');expect(input()).toBe('handled');
f.record('PreToolUse','first');expect(input()).toBe('handled');
expect(fs.readFileSync(f.recorder.file,'utf8')).not.toContain('NEVER_SAVE');
}finally{f.close();}
});
test('failed, missing-result, foreign session/path, mismatched tool and replay cannot release',()=>{
for(const kind of ['failed','no-result','foreign','other-path','other-tool','sidechain','wrong-cwd']){
const f=fixture();try{
const guard=createPlanCountPermissionGuard();const input=()=>guard(f.screen,'',f.read());
f.record('PreToolUse','first');expect(input()).toBe('grant');
if(kind==='failed')f.record('PostToolUseFailure','first');
else if(kind!=='no-result')f.record('PostToolUse','first',kind==='foreign'?{session_id:'foreign'}:kind==='other-path'?{tool_input:{file_path:path.join(f.dir,'other')}}:kind==='other-tool'?{tool_name:'Bash'}:kind==='sidechain'?{agent_id:'child'}:{cwd:'/other'});
f.record('PreToolUse','second');expect(input(),kind).toBe('handled');
}finally{f.close();}
}
});
test('scoped current records and viewport are required; other permission policy stays unchanged',()=>{
const f=fixture();try{
f.record('PreToolUse','first');const valid=fs.readFileSync(f.recorder.file,'utf8');
for(const delta of [{sessionId:'foreign'},{cwd:'/other'},{expected:'/other'},{transcriptPath:'/outside/main.jsonl'},{timestamp:new Date(f.startedAt-1).toISOString()},{timestamp:new Date(Date.now()+60000).toISOString()},{pendingId:'main:../escape'}]){
fs.writeFileSync(f.recorder.file,JSON.stringify({...JSON.parse(valid),...delta}));expect(f.read()).toBeNull();
}
fs.writeFileSync(f.recorder.file,valid);
expect(currentFilePermissionEpoch(f.recorder.file,f.expected,f.cwd,f.config,f.startedAt,f.transcript,'Do you want to create OTHER.md?')).toBeUndefined();
const guard=createPlanCountPermissionGuard();expect(guard('Do you want to create OTHER.md?\n❯1.Yes\n2.Yes, and switch to accept edits (auto-approve file edits and common file commands) for this session (shift+tab)\n3.No\nEsc to cancel · Tab to amend')).toBe('grant');
fs.unlinkSync(f.recorder.file);fs.symlinkSync(f.expected,f.recorder.file);expect(f.read()).toBeNull();
expect(createFilePermissionRecorder(f.cwd,f.config,path.parse(f.dir).root+'not-disposable.md')).toBeUndefined();
}finally{f.close();}
});
});
test('large report crop verifies only a complete bounded source line and the current owned epoch',()=>{
const f=fixture();try{
const screen=largeCeo.viewport.replaceAll(path.dirname(largeCeo.expectedPath),path.dirname(f.expected))
.replaceAll(path.basename(largeCeo.expectedPath),path.basename(f.expected));
const prefix=Array.from({length:largeCeo.sourceLine-1},(_,i)=>`preceding line ${i+1}\n`).join('');
const complete=prefix+largeCeo.priorLine+'\n';
const report=complete+'tail\n'.repeat(Math.ceil((largeCeo.originalReportBytes-Buffer.byteLength(complete))/5));
expect(Buffer.byteLength(report)).toBeGreaterThan(64*1024);
fs.writeFileSync(f.expected,report);f.record('PreToolUse','first');
const check=()=>currentFilePermissionEpoch(f.recorder.file,f.expected,f.cwd,f.config,f.startedAt,f.transcript,screen);
expect(check()?.pendingId).toBe('main:first');
const guard=createPlanCountPermissionGuard();expect(guard(screen,'',check())).toBe('grant');
expect(guard(screen,'',check())).toBe('handled');
const valid=fs.readFileSync(f.recorder.file,'utf8');
for(const delta of [{cwd:'/foreign'},{expected:'/foreign/report.md'},{sessionId:'foreign'},{pendingId:null},{timestamp:new Date(f.startedAt-1).toISOString()}]){
fs.writeFileSync(f.recorder.file,JSON.stringify({...JSON.parse(valid),...delta}));expect(check()).toBeNull();
}
fs.writeFileSync(f.recorder.file,valid);
for(const content of [
report.replace(largeCeo.priorLine,'different prior line'),
'no requested line\n'.repeat(2),
prefix+'x'.repeat(64*1024)+largeCeo.priorLine+'\n',
prefix+'x'.repeat(64*1024-Buffer.byteLength(prefix)-Buffer.byteLength(largeCeo.priorLine))+largeCeo.priorLine+'\n',
]){fs.writeFileSync(f.expected,content);expect(check()).toBeNull();}
fs.writeFileSync(f.expected,report);
const target=f.expected+'.real';fs.renameSync(f.expected,target);fs.symlinkSync(target,f.expected);
expect(check()).toBeNull();
}finally{f.close();}
});
for (const variant of ['basic', 'intervening', 'cropped', 'same-basename', 'path-cropped']) test.skipIf(process.platform==='win32')(`real fake CLI grants each current request once: ${variant}`,async()=>{
const intervening = variant === 'intervening';
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'ce-'));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');const expected=path.join(dir,variant==='same-basename'?'PLAN.md':'report.md');fs.writeFileSync(expected,'original');
const cropped=capturedAc.rows.find(row=>row.job===5)!;
let screen=variant==='path-cropped' ? capturedPath.screen.replace(capturedPath.screen.split('\n')[0]!,expected).replaceAll(path.dirname(capturedPath.expected),path.dirname(expected)).replaceAll(path.basename(capturedPath.expected),'report.md')
: variant==='cropped' ? cropped.screen.replaceAll(path.dirname(cropped.hook.expected),path.dirname(expected)).replaceAll(path.basename(cropped.hook.expected),'report.md')
: captured.screen.replaceAll(captured.expectedPath,expected).replace('../gstack-e2e-plan-ceo-paired-2Rv5Bi/gstack-test-plan-ceo-paired.md',expected).replaceAll('gstack-test-plan-ceo-paired.md','report.md');
if(variant==='same-basename')screen=screen.replaceAll(expected,'__ACTIVE_PLAN_PATH__').replaceAll('report.md','PLAN.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.FILE_EPOCH_CASE);const log=e=>fs.appendFileSync(item.events,JSON.stringify(e)+'\n');
const sid='epoch-main';const nativePath=path.join(process.env.CLAUDE_CONFIG_DIR,'projects','epoch',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.'}]);log({type:'start',pid:process.pid,cwd:process.cwd()});
const settings=JSON.parse(process.argv[process.argv.indexOf('--settings')+1]);
if(settings.hooks.PreToolUse[0].matcher!=='^ExitPlanMode$')throw Error('Exit recorder changed');
const hook=async(name,id)=>{
const entries=(settings.hooks[name]??[]).filter(h=>h.matcher==='^(Write|Edit)$');
for(const entry of entries){
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:item.activePlan?path.join(process.cwd(),'PLAN.md'):item.expected,old_string:'old',new_string:'new'}};
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 was not silent');log({type:'hook',name,id});
}
};
let stage='startup';const paint=()=>process.stdout.write('\x1b[2J\x1b[H'+item.screen.replaceAll('__ACTIVE_PLAN_PATH__',path.join(process.cwd(),'PLAN.md')).replaceAll('\n','\r\n'));
process.stdin.setRawMode?.(true);process.stdin.on('data',async data=>{
const input=data.toString();log({type:'input',stage,input});
if(stage==='startup'){stage='first';await hook('PreToolUse','first');paint();return;}
if(stage==='old-pane'||stage==='done'){log({type:'unexpected'});return;}
if(input!=='1\r')throw Error('default permission input changed');
if(stage==='first'){stage='old-pane';await hook('PostToolUse','first');if(item.intervening){await hook('PreToolUse','automatic');await hook('PostToolUse','automatic');}paint();setTimeout(async()=>{await hook('PreToolUse','second');stage='second';paint();},3200);return;}
stage='done';await hook('PostToolUse','second');
const q={header:'Finding',question:'Apply this 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);
fs.writeFileSync(worker,`import {runPlanSkillCounting} from ${JSON.stringify(pathToFileURL(path.join(import.meta.dir,'helpers/claude-pty-runner.ts')).href)};const o=await runPlanSkillCounting({skillName:'plan-ceo-review',slashCommand:'/plan-ceo-review',followUpPrompt:'Review the disposable fixture.',expectedPlanPath:${JSON.stringify(expected)},isLastStep0AUQ:()=>false,isReviewAUQ:()=>true,reviewCountCeiling:1,timeoutMs:28000,startupReadyMarker:${JSON.stringify('PTY_READY:'+events)},env:{FILE_EPOCH_CASE:${JSON.stringify(JSON.stringify({events,expected,screen,intervening,activePlan:variant==='same-basename'}))}}});await Bun.write(${JSON.stringify(output)},JSON.stringify(o));`);
const child=Bun.spawn([process.execPath,worker],{env:{...process.env,BROWSE_TERMINAL_BINARY:fake,EVALS_HERMETIC:'1'},stdout:'pipe',stderr:'pipe'});const killer=setTimeout(()=>child.kill('SIGKILL'),33000);
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 o=JSON.parse(fs.readFileSync(output,'utf8'));expect(o.outcome,JSON.stringify(o)).toBe('ceiling_reached');expect(o.reviewCount).toBe(1);
const rows=fs.readFileSync(events,'utf8').trim().split('\n').map(l=>JSON.parse(l));expect(rows.filter(e=>e.type==='input').map(e=>e.input)).toEqual(['/plan-ceo-review\r','1\r','1\r']);expect(rows.some(e=>e.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');if(fs.existsSync(events)){const first=JSON.parse(fs.readFileSync(events,'utf8').split('\n')[0]!);try{process.kill(first.pid,'SIGKILL');}catch{}}fs.rmSync(dir,{recursive:true,force:true});}
},35000);
import capturedPath from './fixtures/plan-count-permission-target-ad-v2.json';
function pathCroppedFixture(){
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'count-path-cropped-'));
// Retain the captured relative layout inside this run's disposable temp root.
const relocate=(value:string)=>value.replaceAll(path.dirname(capturedPath.cwd),dir);
const cwd=relocate(capturedPath.cwd),config=relocate(capturedPath.config),expected=relocate(capturedPath.expected);
const screen=relocate(capturedPath.screen),sessionId=capturedPath.sessionId;
const recorder=createFilePermissionRecorder(cwd,config,expected)!;const startedAt=Date.now()-1000;
const transcript:any={status:'ready',calls:[],assistantMessages:[{sessionId,text:'Reviewing',timestamp:new Date().toISOString()}]};
// The public screen/identity are retained; these hook events are synthetic.
const record=(name:string,id:string)=>recordFilePermission(JSON.stringify({hook_event_name:name,tool_name:'Edit',session_id:sessionId,tool_use_id:id,cwd,transcript_path:path.join(config,'projects','owned',sessionId+'.jsonl'),tool_input:{file_path:expected}}),recorder.file,cwd,config,expected);
const read=(viewport=screen)=>currentFilePermissionEpoch(recorder.file,expected,cwd,config,startedAt,transcript,viewport);
return {recorder,startedAt,transcript,record,read,screen,expected,close(){recorder.dispose();fs.rmSync(dir,{recursive:true,force:true});}};
}
test('AD v2 retained path-only heading needs the exact current owned epoch',()=>{
const f=pathCroppedFixture();try{
expect(capturedPath.provenance.actualOutcome).toBe('timeout');
expect(classifyPlanCountFrame(f.screen)).toBe('permission');
expect(f.read()).toBeNull();f.record('PreToolUse','first');
expect(f.read()?.pendingId).toBe(capturedPath.sessionId+':first');
const guard=createPlanCountPermissionGuard();expect(guard(f.screen,'',f.read())).toBe('grant');
expect(guard(f.screen,'',f.read())).toBe('handled');
}finally{f.close();}
});
test('AD v2 path-only target retains native success and replay boundaries',()=>{
const f=pathCroppedFixture();try{
const guard=createPlanCountPermissionGuard();const read=()=>guard(f.screen,'',f.read());
f.record('PreToolUse','first');expect(read()).toBe('grant');
f.record('PostToolUseFailure','first');f.record('PreToolUse','second');expect(read()).toBe('handled');
f.record('PostToolUse','second');f.record('PreToolUse','third');expect(read()).toBe('handled');
}finally{f.close();}
const g=pathCroppedFixture();try{
const guard=createPlanCountPermissionGuard();g.record('PreToolUse','first');expect(guard(g.screen,'',g.read())).toBe('grant');
g.record('PostToolUse','first');expect(guard(g.screen,'',g.read())).toBe('handled');
g.record('PreToolUse','second');expect(guard(g.screen,'',g.read())).toBe('grant');
g.record('PostToolUse','second');g.record('PreToolUse','first');expect(guard(g.screen,'',g.read())).toBe('handled');
}finally{g.close();}
});
test('AD v2 path-only heading must agree with full menu target and current metadata',()=>{
const f=pathCroppedFixture();try{
f.record('PreToolUse','first');const screen=f.screen,lines=screen.split('\n');
expect(f.read([f.expected,...lines.slice(1)].join('\n'))?.pendingId).toBe(capturedPath.sessionId+':first');
const wrong=[
[' ../sibling/'+path.basename(f.expected),...lines.slice(1)].join('\n'),
[' '+f.expected+' extra',...lines.slice(1)].join('\n'),
[lines[0],'/tmp/other/'+path.basename(f.expected),...lines.slice(1)].join('\n'),
[lines[0],...lines.slice(2)].join('\n'),
'Example:\n'+screen,'```text\n'+screen,screen+'\nContinue with a different action.',
screen.replace('❯ 1. Yes','❯ 2. Yes'),screen.replace('Esc to cancel · Tab to amend','Esc to cancel'),
screen.replace('always allow access to '+path.dirname(f.expected),'always allow access to /tmp/sibling'),
];
for(const altered of wrong){expect(altered).not.toBe(screen);expect(f.read(altered)).toBeNull();}
const valid=JSON.parse(fs.readFileSync(f.recorder.file,'utf8'));
for(const delta of [{timestamp:new Date(f.startedAt-1).toISOString()},{sessionId:'foreign'},{pendingId:valid.completedId},{cwd:'/foreign'},{expected:'/foreign'}]){
fs.writeFileSync(f.recorder.file,JSON.stringify({...valid,...delta}));expect(f.read()).toBeNull();
}
}finally{f.close();}
});
import {E2E_TOUCHFILES,selectTests} from './helpers/touchfiles';
import fs_batching_permission_at from 'node:fs';
import os_batching_permission_at from 'node:os';
import path_batching_permission_at from 'node:path';
import captured_batching_permission_at from './fixtures/batching-permission-at.json';
import fixture_design_crop_gutter_ap from './fixtures/design-crop-gutter-ap.json';
import previous_design_crop_gutter_ap from './fixtures/plan-count-crop-ak.json';
import capturedAd_plan_count_permission_ac from './fixtures/plan-count-permission-ad.json';
import capturedAe_plan_count_permission_ac from './fixtures/plan-count-permission-ae.json';
import capturedAh_plan_count_permission_ac from './fixtures/plan-count-permission-ah.json';
import { currentFilePermissionBinding } from './helpers/plan-count-file-permission';
import exact_plan_count_quoted_frame_ak from './fixtures/plan-count-quoted-frame-ak.json';
import owned_plan_count_quoted_frame_ak from './fixtures/plan-count-owned-permission-v.json';
describe('batching-permission-at', () => {
const fs = fs_batching_permission_at;
const os = os_batching_permission_at;
const path = path_batching_permission_at;
const captured = captured_batching_permission_at;
function renderPermissionScreen(expected: string, paths: Pick<typeof path, 'dirname' | 'basename'> = path): string {
// The capture is already laid out at the runner's 120 columns. Replacing its
// path must reflow that menu line, otherwise the PTY hard-wraps words in half.
return captured.screen.split('\n').map(original => {
const line = original.replaceAll(path.posix.dirname(captured.expectedPath), paths.dirname(expected))
.replaceAll(path.posix.basename(captured.expectedPath), paths.basename(expected));
if (line === original || line.length <= 120) return line;
const indent = /^ */.exec(line)![0], lines: string[] = []; let current = indent;
for (const word of line.trim().split(/\s+/)) {
if (indent.length + word.length > 120) throw Error('Fixture path exceeds the permission panel width');
if (current.length > indent.length && current.length + 1 + word.length > 120) { lines.push(current); current = indent; }
current += (current.length > indent.length ? ' ' : '') + word;
}
return [...lines, current].join('\n');
}).join('\n');
}
function fixture(){
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'batch-permission-')),cwd=path.join(dir,'cwd'),config=path.join(dir,'.claude'),expected=path.join(dir,'report.md');fs.mkdirSync(cwd);fs.writeFileSync(expected,'original');
const recorder=createFilePermissionRecorder(cwd,config,expected)!;const startedAt=Date.now()-1000;
const screen=renderPermissionScreen(expected);
const transcript:any={status:'ready',calls:[],assistantMessages:[{sessionId:'synthetic-epoch',text:'Reviewing',timestamp:new Date().toISOString()}]};
const record=(name:string,id:string,extra={})=>recordFilePermission(JSON.stringify({hook_event_name:name,tool_name:'Edit',session_id:'synthetic-epoch',tool_use_id:id,cwd,transcript_path:path.join(config,'projects','owned','synthetic-epoch.jsonl'),tool_input:{file_path:expected},...extra}),recorder.file,cwd,config,expected);
const read=()=>currentFilePermissionEpoch(recorder.file,expected,cwd,config,startedAt,transcript,screen);
return{dir,cwd,config,expected,recorder,screen,transcript,record,read,close(){recorder.dispose();fs.rmSync(dir,{recursive:true,force:true})}};
}
test('retained retry has a valid permission panel and real previous completion without a pending ID',()=>{
expect(classifyPlanCountFrame(captured.screen)).toBe('permission');expect(captured.priorCompletedEdit[0]!.name).toBe('Edit');expect(captured.priorCompletedEdit[1]!.isError).toBe(false);
expect(captured.pendingEditId).toBeNull();expect(captured.provenance.originalOutcome).toBe('timeout');expect(captured.provenance.paidOutcomeReclassified).toBe(false);
const guard=createPlanCountPermissionGuard();expect(guard(captured.screen,captured.lastMatchedDisplayCompletion)).toBe('grant');expect(guard(captured.screen,captured.lastMatchedDisplayCompletion)).toBe('handled');
});
test('a substituted long fixture path reflows the menu without splitting permission words', () => {
const prefix = ' always allow access to ', suffix = ' for this ';
const directory = '/' + 'x'.repeat(120 - prefix.length - suffix.length - 3 - 1);
const rawLine = `${prefix}${directory}${suffix}session`;
expect(`${rawLine.slice(0, 120)}\n${rawLine.slice(120)}`).toContain('ses\nsion');
for (const paths of [path.posix, path.win32]) {
const expected = paths.join(directory, 'report.md');
const screen = renderPermissionScreen(expected, paths);
const menu = screen.slice(screen.indexOf(' Do you want to make this edit'));
expect(menu.split('\n').every(line => line.length <= 120)).toBe(true);
expect(menu).toContain(paths.dirname(expected));
expect(menu).toContain('edit to report.md?');
expect(menu).toMatch(/1\. Yes[\s\S]+2\. Yes,[\s\S]+3\. No/);
expect(createPlanCountPermissionGuard()(screen, captured.lastMatchedDisplayCompletion)).toBe('grant');
}
});
test('synthetic hook epochs release only the later exact request after its predecessor succeeds',()=>{
const f=fixture();try{const guard=createPlanCountPermissionGuard(),input=()=>guard(f.screen,captured.lastMatchedDisplayCompletion,f.read());
expect(input()).toBe('handled');f.record('PreToolUse','first');expect(input()).toBe('grant');expect(input()).toBe('handled');
f.record('PostToolUse','first');expect(input()).toBe('handled');f.record('PreToolUse','first');expect(input()).toBe('handled');
f.record('PreToolUse','second');expect(input()).toBe('grant');expect(input()).toBe('handled');f.record('PostToolUse','first');expect(input()).toBe('handled');
}finally{f.close()}
});
for(const reason of ['failed','no-result','foreign-session','foreign-path','other-tool','sidechain'])test(`a later matching menu cannot replace ${reason} predecessor evidence`,()=>{
const f=fixture();try{const guard=createPlanCountPermissionGuard(),input=()=>guard(f.screen,'',f.read());f.record('PreToolUse','first');expect(input()).toBe('grant');
if(reason==='failed')f.record('PostToolUseFailure','first');else if(reason!=='no-result')f.record('PostToolUse','first',reason==='foreign-session'?{session_id:'foreign'}:reason==='foreign-path'?{tool_input:{file_path:path.join(f.dir,'foreign','report.md')}}:reason==='other-tool'?{tool_name:'Read'}:{agent_id:'child'});
f.record('PreToolUse','second');expect(input()).toBe('handled');
}finally{f.close()}
});
test.skipIf(process.platform==='win32')('real fake CLI observes two file epochs without imposing terminal report validation',async()=>{
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'batch-permission-pty-')),fake=path.join(dir,'fake-claude'),worker=path.join(dir,'worker.ts'),events=path.join(dir,'events.jsonl'),output=path.join(dir,'output.json'),expected=path.join(dir,'report.md');fs.writeFileSync(expected,'original');
const screen=renderPermissionScreen(expected);
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.FILE_EPOCH_CASE);const log=e=>fs.appendFileSync(item.events,JSON.stringify(e)+'\n');
const sid='epoch-main';const nativePath=path.join(process.env.CLAUDE_CONFIG_DIR,'projects','epoch',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.'}]);log({type:'start',pid:process.pid,cwd:process.cwd()});
const settings=JSON.parse(process.argv[process.argv.indexOf('--settings')+1]);
if(settings.hooks.PreToolUse[0].matcher!=='^ExitPlanMode$')throw Error('Exit recorder changed');
const hook=async(name,id)=>{
const entries=(settings.hooks[name]??[]).filter(h=>h.matcher==='^(Write|Edit)$');
if(entries.length!==1)throw Error('Expected exactly one caller-owned file recorder');
for(const entry of entries){
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:item.activePlan?path.join(process.cwd(),'PLAN.md'):item.expected,old_string:'old',new_string:'new'}};
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 was not silent');log({type:'hook',name,id});
}
};
let stage='startup';const paint=()=>process.stdout.write('\x1b[2J\x1b[H'+item.screen.replaceAll('__ACTIVE_PLAN_PATH__',path.join(process.cwd(),'PLAN.md')).replaceAll('\n','\r\n'));
process.stdin.setRawMode?.(true);process.stdin.on('data',async data=>{
const input=data.toString();log({type:'input',stage,input});
if(stage==='startup'){stage='first';await hook('PreToolUse','first');paint();return;}
if(stage==='old-pane'||stage==='done'){log({type:'unexpected'});return;}
if(input!=='1\r')throw Error('default permission input changed');
if(stage==='first'){stage='old-pane';await hook('PostToolUse','first');if(item.intervening){await hook('PreToolUse','automatic');await hook('PostToolUse','automatic');}paint();setTimeout(async()=>{await hook('PreToolUse','second');stage='second';paint();},3200);return;}
stage='done';await hook('PostToolUse','second');
const q={header:'Finding',question:'Apply this 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[HCompletion summary\r\n');
});process.on('SIGINT',()=>process.exit(0));process.stdin.resume();process.stdout.write('FILE_EPOCH_READY\r\n');
`);fs.chmodSync(fake,0o755);
fs.writeFileSync(worker,`import {runPlanSkillCounting} from ${JSON.stringify(pathToFileURL(path.join(import.meta.dir,'helpers/claude-pty-runner.ts')).href)};const o=await runPlanSkillCounting({skillName:'plan-eng-review',slashCommand:'/plan-eng-review',followUpPrompt:'Review this disposable batching fixture.',permissionPlanPath:${JSON.stringify(expected)},startupReadyMarker:'FILE_EPOCH_READY',isLastStep0AUQ:()=>false,isReviewAUQ:()=>true,reviewCountCeiling:2,timeoutMs:28000,env:{FILE_EPOCH_CASE:${JSON.stringify(JSON.stringify({events,expected,screen}))}}});await Bun.write(${JSON.stringify(output)},JSON.stringify(o));`);
const child=Bun.spawn([process.execPath,worker],{env:{...process.env,BROWSE_TERMINAL_BINARY:fake,EVALS_HERMETIC:'1'},stdout:'pipe',stderr:'pipe'});const killer=setTimeout(()=>child.kill('SIGKILL'),33000);
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 o=JSON.parse(fs.readFileSync(output,'utf8'));expect(o.outcome,JSON.stringify(o)).toBe('completion_summary');expect(o.reviewCount).toBe(1);expect(fs.readFileSync(expected,'utf8')).toBe('original');
const rows=fs.readFileSync(events,'utf8').trim().split('\n').map(l=>JSON.parse(l));expect(rows.filter(e=>e.type==='input').map(e=>e.input)).toEqual(['/plan-eng-review\r','1\r','1\r']);expect(rows.some(e=>e.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');if(fs.existsSync(events)){const first=JSON.parse(fs.readFileSync(events,'utf8').split('\n')[0]!);try{process.kill(first.pid,'SIGKILL');}catch{}}fs.rmSync(dir,{recursive:true,force:true});}
},35000);
});
describe('design-crop-gutter-ap', () => {
const fs = fs_batching_permission_at;
const os = os_batching_permission_at;
const path = path_batching_permission_at;
const fixture = fixture_design_crop_gutter_ap;
const previous = previous_design_crop_gutter_ap;
function replay(change:(f:any)=>void=()=>{},input:any=fixture){
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'design-gutter-ap-'));
const expected=path.join(dir,'report.md'),record=path.join(dir,'record.json');
const f:any={expected,record,cwd:input.cwd,config:input.config,startedAt:input.startedAt,
screen:input.screen.replaceAll(path.dirname(input.hook.expected),dir).replaceAll(path.basename(input.hook.expected),'report.md'),
state:{...structuredClone(input.hook),expected},before:input.ownedBefore,transcript:structuredClone(input.transcript),fileKind:'file'};
try{
change(f);fs.writeFileSync(record,JSON.stringify(f.state));
if(f.fileKind==='file')fs.writeFileSync(expected,f.before);
if(f.fileKind==='directory')fs.mkdirSync(expected);
if(f.fileKind==='symlink'){const target=path.join(dir,'other.md');fs.writeFileSync(target,f.before);fs.symlinkSync(target,expected);}
const epoch=currentFilePermissionEpoch(record,f.expected,f.cwd,f.config,f.startedAt,f.transcript,f.screen);
const guard=createPlanCountPermissionGuard();
return {epoch,first:guard(f.screen,'',epoch),second:guard(f.screen,'',epoch)};
}finally{fs.rmSync(dir,{recursive:true,force:true});}
}
test('exact five-column current gutter supplies one owned Edit epoch and one grant',()=>{
const lines=fixture.screen.split('\n');expect(lines[0]).toMatch(/^ {5}\S/);expect(lines[1]).toBe(' 62 ');
expect(fixture.ownedBefore.split('\n')[60]!.endsWith(lines[0]!.slice(5))).toBe(true);
const r=replay();expect(r.epoch).toEqual({pendingId:fixture.hook.pendingId,completedId:fixture.hook.completedId,completedIds:fixture.hook.completedIds});
expect(r.first).toBe('grant');expect(r.second).toBe('handled');
});
test('prior six-column public crop remains exact and one-time',()=>{
expect(previous.screen.split('\n')[0]).toMatch(/^ {6}\S/);expect(previous.screen.split('\n')[1]).toBe(' 82 ');
const r=replay(()=>{},previous);expect(r.epoch?.pendingId).toBe(previous.hook.pendingId);expect(r.first).toBe('grant');expect(r.second).toBe('handled');
// Keep the existing six-space acceptance even when the adjacent numeric
// row has a different padding; the unchanged original-line guard remains.
expect(replay(f=>{f.screen=' '+f.screen;}).epoch?.pendingId).toBe(fixture.hook.pendingId);
});
test('padding and line-number width derive the continuation column together',()=>{
const padded=replay(f=>{f.screen=' '+f.screen;f.screen=f.screen.replace(/^ 62 $/m,' 62 ');});
expect(padded.epoch?.pendingId).toBe(fixture.hook.pendingId);
const relocated=replay(f=>{
f.before='Earlier unchanged line\n'.repeat(38)+f.before;
f.screen=' '+f.screen;
f.screen=f.screen.replace(/^ ([1-9]\d*)( | [+-])/gm,(_:string,n:string,g:string)=>' '+(Number(n)+38)+g);
});
expect(relocated.epoch?.pendingId).toBe(fixture.hook.pendingId);
});
test.each([0,3])('a native numbered-row padding of %d derives a matching non-six gutter',padding=>{
const r=replay(f=>{
f.screen=' '.repeat(padding+4)+f.screen.slice(5);
f.screen=f.screen.replace(/^ 62 $/m,' '.repeat(padding)+'62 ');
});
expect(r.epoch?.pendingId).toBe(fixture.hook.pendingId);expect(r.first).toBe('grant');
});
test('an ordinary numbered unchanged row remains a numbered row, not a wrapped continuation',()=>{
const r=replay(f=>{
f.screen=' 61 '+f.before.split('\n')[60]+'\n'+f.screen.slice(f.screen.indexOf('\n')+1);
});
expect(r.epoch?.pendingId).toBe(fixture.hook.pendingId);expect(r.first).toBe('grant');
});
const negatives:Array<[string,(f:any)=>void]>=[
['four-space gutter with five-column numbered row',f=>{f.screen=f.screen.slice(1);}],
['seven-space gutter with five-column numbered row',f=>{f.screen=' '+f.screen;}],
['tab cannot substitute for a native space gutter',f=>{f.screen='\t'+f.screen.slice(1);}],
['wrong preceding file line',f=>{f.screen=f.screen.replace(/^ 62 $/m,' 63 ');}],
['changed continuation content',f=>{f.screen=f.screen.replace('f2 with icon','foreign with icon');}],
['stale before-file bytes',f=>{f.before=f.before.replace('f2 with icon','changed with icon');}],
['quoted continuation',f=>{f.screen=f.screen.replace(/^ {5}/,' > ');}],
['two unnumbered continuation rows',f=>{f.screen=f.screen.split('\n')[0]+'\n'+f.screen;}],
['next row is an addition, not unchanged context',f=>{f.screen=f.screen.replace(/^ 62 $/m,' 62 +');}],
['zero next line',f=>{f.screen=f.screen.replace(/^ 62 $/m,' 00 ');}],
['missing current file',f=>{f.fileKind='missing';}],
['directory instead of current file',f=>{f.fileKind='directory';}],
['required source line beyond the bounded prefix',f=>{f.before='x'.repeat(65537)+f.before;}],
['foreign displayed directory',f=>{f.screen=f.screen.replace(path.dirname(f.expected)+' for this session',path.join(path.dirname(f.expected),'foreign')+' for this session');}],
['foreign hook target',f=>{f.state.expected+='.foreign';}],
['foreign hook cwd',f=>{f.state.cwd+='.foreign';}],
['foreign native session',f=>{f.transcript={status:'ready',calls:[],assistantMessages:[{sessionId:'foreign',text:'Current review',timestamp:new Date(f.startedAt).toISOString()}]};}],
['missing pending request',f=>{f.state.pendingId=null;}],
['completed request cannot reopen',f=>{f.state.completedId=f.state.pendingId;}],
['stale request timestamp',f=>{f.state.timestamp=new Date(f.startedAt-1).toISOString();}],
['missing menu footer',f=>{f.screen=f.screen.replace('Esc to cancel · Tab to amend','');}],
['one-time action changed',f=>{f.screen=f.screen.replace('❯ 1. Yes','❯ 1. Yes, always allow');}],
];
test.each(negatives)('%s cannot obtain a grant',(_,change)=>{
const r=replay(change);expect(r.epoch).not.toBeTruthy();expect(r.first).not.toBe('grant');
});
test.skipIf(process.platform==='win32')('symlink cannot provide the original line',()=>{expect(replay(f=>{f.fileKind='symlink';}).epoch).toBeNull();});
});
describe('plan-count-crop-ak', () => {
const fs = fs_batching_permission_at;
const os = os_batching_permission_at;
const path = path_batching_permission_at;
const fixture = previous_design_crop_gutter_ap;
function replay(change: (f: any) => void = () => {}) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-crop-ak-'));
const expected = path.join(dir, 'report.md'), record = path.join(dir, 'record.json');
const f: any = { expected, record, screen: fixture.screen.replaceAll(path.dirname(fixture.hook.expected), dir)
.replaceAll(path.basename(fixture.hook.expected), 'report.md'), state: { ...structuredClone(fixture.hook), expected },
before: fixture.ownedBefore, cwd: fixture.cwd, config: fixture.config, startedAt: fixture.startedAt,
transcript: structuredClone(fixture.transcript), fileKind: 'file' };
try {
change(f); fs.writeFileSync(record, JSON.stringify(f.state));
if (f.fileKind === 'file') fs.writeFileSync(expected, f.before);
if (f.fileKind === 'directory') fs.mkdirSync(expected);
if (f.fileKind === 'symlink') { const other = path.join(dir, 'other.md'); fs.writeFileSync(other, f.before); fs.symlinkSync(other, expected); }
const epoch = currentFilePermissionEpoch(record, f.expected, f.cwd, f.config, f.startedAt, f.transcript, f.screen);
return { epoch, screen: f.screen, guard: createPlanCountPermissionGuard(), run: () => createPlanCountPermissionGuard()(f.screen, '', epoch) };
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
}
test('actual owned bare continuation uses the preceding original line and grants once', () => {
const r = replay();
expect(r.epoch?.pendingId).toBe(fixture.hook.pendingId);
expect(r.guard(r.screen, '', r.epoch)).toBe('grant');
expect(r.guard(r.screen, '', r.epoch)).toBe('handled');
});
test('harmless preceding line number relocation keeps the exact content binding', () => {
const r = replay(f => {
f.before = 'Added unchanged line\n' + f.before;
f.screen = f.screen.replace(/^ {0,3}82 +$/m, ' 83 ');
});
expect(r.epoch?.pendingId).toBe(fixture.hook.pendingId);
});
const negatives: Array<[string, (f: any) => void]> = [
['wrong preceding line', f => { f.screen = f.screen.replace(/^ {0,3}82 +$/m, ' 83 '); }],
['unrelated prose continuation', f => { f.screen = f.screen.replace(/^.*\n/, ' Apply this edit now\n'); }],
['source quotation continuation', f => { f.screen = f.screen.replace(/^.*\n/, ' > Example: apply this edit now\n'); }],
['foreign continuation suffix', f => { f.screen = f.screen.replace('f2 (~7.6:1)', 'foreign (~7.6:1)'); }],
['five-space unnumbered gutter', f => { f.screen = f.screen.slice(1); }],
['seven-space unnumbered gutter', f => { f.screen = ' ' + f.screen; }],
['no following unchanged numbered row', f => { f.screen = f.screen.replace(/^ {0,3}82 +$/m, ' 82 +'); }],
['two arbitrary continuation rows', f => { f.screen = f.screen.split('\n')[0] + '\n' + f.screen; }],
['missing original file', f => { f.fileKind = 'missing'; }],
['directory in place of original file', f => { f.fileKind = 'directory'; }],
['required original line beyond the bounded prefix', f => { f.before = 'x'.repeat(65537) + f.before; }],
['foreign displayed directory', f => { f.screen = f.screen.replace(path.dirname(f.expected) + ' for this session', path.join(path.dirname(f.expected), 'foreign') + ' for this session'); }],
['foreign hook expected path', f => { f.state.expected += '.foreign'; }],
['no current native request', f => { f.state.pendingId = null; }],
['completed native request', f => { f.state.completedId = f.state.pendingId; }],
['stale native timestamp', f => { f.state.timestamp = new Date(f.startedAt - 1).toISOString(); }],
['future native timestamp', f => { f.state.timestamp = new Date(Date.now() + 60000).toISOString(); }],
['foreign session transcript', f => { f.transcript.assistantMessages[0].sessionId = 'foreign'; }],
['unavailable native transcript', f => { f.transcript.status = 'error'; }],
['missing complete menu footer', f => { f.screen = f.screen.replace('Esc to cancel · Tab to amend', ''); }],
['selected policy change', f => { f.screen = f.screen.replace('❯ 1. Yes', '❯ 1. Yes, always allow'); }],
['unrelated prompt prepended', f => { f.screen = '☐ Review this example\n' + f.screen; }],
['historical pane is not current viewport', f => { f.screen = 'Waiting for current tool'; }],
];
test.each(negatives)('%s cannot supply an owned epoch', (_, change) => {
const r = replay(change); expect(r.epoch).not.toBeTruthy(); expect(r.run()).not.toBe('grant');
});
test.skipIf(process.platform === 'win32')('a symlink cannot supply current original content', () => {
expect(replay(f => { f.fileKind = 'symlink'; }).epoch).toBeNull();
});
});
describe('plan-count-permission-ac', () => {
const captured = capturedAc;
const capturedAd = capturedAd_plan_count_permission_ac;
const capturedAe = capturedAe_plan_count_permission_ac;
const capturedAh = capturedAh_plan_count_permission_ac;
function fixture() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'count-permission-ac-'));
const cwd = path.join(dir, 'cwd'), config = path.join(dir, '.claude');
const expected = path.join(dir, 'report.md'), file = path.join(dir, 'state.json');
fs.mkdirSync(cwd); const startedAt = Date.now() - 1000;
const screen = captured.rows[0]!.screen
.replace('../gstack-e2e-plan-design-3vPM9g/gstack-test-plan-design.md', expected)
.replaceAll('gstack-test-plan-design.md', 'report.md');
const transcript: any = { status: 'ready', calls: [], assistantMessages: [{ sessionId: 'main', text: 'Reviewing' }] };
const record = (name: string, id: string, delta: object = {}) => recordFilePermission(JSON.stringify({
hook_event_name: name, tool_name: 'Edit', session_id: 'main', tool_use_id: id, cwd,
transcript_path: path.join(config, 'projects', 'owned', 'main.jsonl'),
tool_input: { file_path: expected, old_string: 'PRIVATE_OLD', new_string: 'PRIVATE_NEW' }, ...delta,
}), file, cwd, config, expected);
const epoch = () => currentFilePermissionEpoch(file, expected, cwd, config, startedAt, transcript, screen);
return { dir, cwd, config, expected, file, screen, record, epoch,
close: () => fs.rmSync(dir, { recursive: true, force: true }) };
}
test('all five actual stalled screens already identify an edit permission', () => {
for (const row of captured.rows) {
expect(classifyPlanCountFrame(row.screen), row.attempt).toBe('permission');
expect(createPlanCountPermissionGuard()(row.screen), row.attempt).toBe('grant');
expect(row.hook.pendingId).not.toBeNull();
expect(row.transcriptSessions).toEqual([row.hook.sessionId]);
}
});
test('an intervening successful edit does not erase the exact previous grant completion', () => {
const f = fixture(); try {
const guard = createPlanCountPermissionGuard(), input = () => guard(f.screen, '', f.epoch());
f.record('PreToolUse', 'granted'); expect(input()).toBe('grant');
f.record('PostToolUse', 'granted'); expect(input()).toBe('handled');
f.record('PreToolUse', 'automatic'); f.record('PostToolUse', 'automatic');
// Old pane is still inert, even after two successful results.
expect(input()).toBe('handled');
f.record('PreToolUse', 'next'); expect(input()).toBe('grant'); expect(input()).toBe('handled');
expect(fs.readFileSync(f.file, 'utf8')).not.toContain('PRIVATE_');
} finally { f.close(); }
});
test('an unrelated success cannot substitute for failed or missing prior approval completion', () => {
for (const outcome of ['PostToolUseFailure', 'missing', 'foreign', 'other-path', 'sidechain']) {
const f = fixture(); try {
const guard = createPlanCountPermissionGuard(), input = () => guard(f.screen, '', f.epoch());
f.record('PreToolUse', 'granted'); expect(input()).toBe('grant');
if (outcome === 'PostToolUseFailure') f.record(outcome, 'granted');
else if (outcome !== 'missing') f.record('PostToolUse', 'granted', outcome === 'foreign'
? { session_id: 'foreign' } : outcome === 'sidechain' ? { agent_id: 'child' }
: { tool_input: { file_path: path.join(f.dir, 'other.md') } });
f.record('PreToolUse', 'automatic'); f.record('PostToolUse', 'automatic');
f.record('PreToolUse', 'next'); expect(input(), outcome).toBe('handled');
f.record('PreToolUse', 'granted'); expect(input(), outcome).toBe('handled');
} finally { f.close(); }
}
});
test('success history rejects malformed, foreign, replayed, and pending IDs', () => {
const f = fixture(); try {
f.record('PreToolUse', 'first'); f.record('PostToolUse', 'first'); f.record('PreToolUse', 'next');
const original = JSON.parse(fs.readFileSync(f.file, 'utf8'));
for (const completedIds of [['foreign:first'], ['main:../escape'], ['main:next'],
['main:first', 'main:first'], Array(129).fill('main:first'), ['main:unseen'], 'main:first']) {
fs.writeFileSync(f.file, JSON.stringify({ ...original, completedIds }));
expect(f.epoch()).toBeNull();
}
} finally { f.close(); }
});
test('success history is bounded by the existing 128-request recorder limit', () => {
const f = fixture(); try {
for (let i = 0; i < 127; i++) { f.record('PreToolUse', `id${i}`); f.record('PostToolUse', `id${i}`); }
f.record('PreToolUse', 'last'); expect(f.epoch()?.completedIds?.length).toBe(127);
expect(fs.statSync(f.file).size).toBeLessThan(64 * 1024);
f.record('PreToolUse', 'overflow'); expect(f.epoch()).toBeNull();
} finally { f.close(); }
});
test('cropped actual panes bind their full directory and basename to the current native epoch', () => {
const rows = captured.rows.filter(row => [4, 5].includes(row.job) && !/^ {0,3}Edit file$/m.test(row.screen));
expect(rows).toHaveLength(2);
for (const row of rows) {
const f = fixture(); try {
const screen = row.screen.replaceAll(path.dirname(row.hook.expected), path.dirname(f.expected))
.replaceAll(path.basename(row.hook.expected), path.basename(f.expected));
const read = (value = screen) => currentFilePermissionEpoch(f.file, f.expected, f.cwd, f.config,
0, { status: 'ready', calls: [], assistantMessages: [{ sessionId: 'main', text: 'Reviewing', timestamp: new Date().toISOString() }] }, value);
f.record('PreToolUse', 'current');
expect(read()?.pendingId, row.attempt).toBe('main:current');
const guard = createPlanCountPermissionGuard();
expect(guard(screen, '', read())).toBe('grant');
expect(guard(screen, '', read())).toBe('handled');
for (const [name, changed] of [
['foreign', screen.replace(path.dirname(f.expected), path.join(f.dir, 'foreign'))],
['remedy', screen.replace(/always\s+allow\s+access\s+to/, 'remove files from')],
['footer', screen.replace('Esc to cancel · Tab to amend', '')],
['yes policy', screen.replace(/❯\s*1\.\s*Yes/, '❯ 1. Yes, change policy')],
['AUQ', '☐ Finding\n' + screen],
['quoted', '> Example:\n' + screen],
['wrapped path', screen.replace(path.dirname(f.expected), path.dirname(f.expected) + '\n/other')],
['path spaces', screen.replace(path.dirname(f.expected), path.dirname(f.expected) + ' space')],
]) {
expect(changed, name).not.toBe(screen);
expect(read(changed), name).toBeNull();
}
expect(read(screen.replace(path.dirname(f.expected), path.join(f.dir, 'foreign')))).toBeNull();
f.record('PostToolUse', 'current'); expect(read()).toBeNull();
f.record('PreToolUse', 'current'); expect(read()).toBeNull();
} finally { f.close(); }
}
});
test('a later exact owned binding wins over an earlier same-basename block', () => {
const f = fixture(); try {
f.record('PreToolUse', 'current');
const transcript: any = {status:'ready', calls:[], assistantMessages:[{sessionId:'main', text:'Reviewing'}]};
const foreign = {file:path.join(f.dir,'foreign-state.json'), expected:path.join(f.dir,'other','report.md')};
const owned = {file:f.file, expected:f.expected};
for (const bindings of [[foreign, owned], [owned, foreign]]) {
const selected = currentFilePermissionBinding(bindings, f.cwd, f.config, 0, transcript, f.screen);
expect(selected?.binding).toBe(owned);
expect(selected?.epoch.pendingId).toBe('main:current');
}
const blocked = currentFilePermissionBinding([foreign, {...foreign, expected:path.join(f.dir,'another','report.md')}],
f.cwd, f.config, 0, transcript, f.screen);
expect(blocked).toBeNull();
expect(createPlanCountPermissionGuard()(f.screen, '', blocked)).toBe('handled');
const otherScreen = f.screen.replaceAll('report.md', 'OTHER.md');
const unrelated = currentFilePermissionBinding([foreign, owned], f.cwd, f.config, 0, transcript, otherScreen);
expect(unrelated).toBeUndefined();
expect(createPlanCountPermissionGuard()(otherScreen, '', unrelated)).toBe('grant');
} finally { f.close(); }
});
// Exact current screens plus content-free native identity from full AD/AE runs.
// The replay projections do not assert these pending writes ever completed.
const cases = [...capturedAd.rows, capturedAe, capturedAh].map(row => ({
p: row, screen: row.screen, binding: {expected: row.state.expected, state: row.state},
observation: {transcript: {status: row.transcriptStatus, calls: [],
assistantMessages: row.transcriptSessions.map(sessionId => ({sessionId}))}},
}));
function adEpoch(c: any, screen = c.screen, state = c.binding.state, delta: any = {}) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'permission-crop-replay-'));
const file = path.join(dir, 'state.json');
try {
// Captures contain POSIX paths. Project their filesystem identity onto the
// replay host without changing the captured fixture or its menu rendering.
const nativeState = { ...state, cwd: path.resolve(state.cwd), expected: path.resolve(state.expected),
transcriptPath: path.resolve(state.transcriptPath) };
const replayScreen = screen.replaceAll(path.posix.dirname(c.binding.expected),
path.dirname(path.resolve(c.binding.expected)));
fs.writeFileSync(file, JSON.stringify(nativeState));
return currentFilePermissionEpoch(file, path.resolve(delta.expected ?? c.binding.expected), path.resolve(delta.cwd ?? c.p.cwd),
path.resolve(delta.config ?? c.p.config), delta.startedAt ?? c.p.startUnix * 1000,
delta.transcript ?? c.observation.transcript, replayScreen);
} finally { fs.rmSync(dir, {recursive:true, force:true}); }
}
for (const c of cases) {
test(`actual AD captured current permission ${c.p.pid} returns its exact pending epoch`, () => {
expect(classifyPlanCountFrame(c.screen)).toBe('permission');
expect(adEpoch(c)).toEqual({pendingId:c.binding.state.pendingId, completedId:c.binding.state.completedId,
completedIds:c.binding.state.completedIds});
const guard = createPlanCountPermissionGuard();
expect(guard(c.screen, '', adEpoch(c))).toBe('grant');
expect(guard(c.screen, '', adEpoch(c))).toBe('handled');
});
test(`AD isolating the rejected rendering guard ${c.p.pid} preserves native identity`, () => {
// These are explicitly normalized controls; the actual captured screen is unchanged above.
const normalized = c.p.pid === capturedAe.pid ? c.screen.replace(/^[╌─━]{3,}[ \t]*\n/, '')
: c.p.pid === 1332470 ? c.screen.replace('3. Nohift+tab)', '3. No') : c.screen.replace(/^ {4,5}\+/, ' 99 +');
expect(normalized).not.toBe(c.screen);
expect(adEpoch(c, normalized)?.pendingId).toBe(c.binding.state.pendingId);
});
}
for (const c of cases) {
test(`AD crop ${c.p.pid} rejects foreign, quoted, incomplete and policy-changing menus`, () => {
const directory = path.dirname(c.binding.expected);
const changes: [string, string][] = [
['foreign directory', c.screen.replace(directory, path.join(directory, 'foreign'))],
['split directory', c.screen.replace(directory, directory + '\n/foreign')],
['quoted', '> Example:\n' + c.screen],
['AUQ', '☐ Review\n' + c.screen],
['code fence', '```\n' + c.screen],
['missing footer', c.screen.replace('Esc to cancel · Tab to amend', '')],
['policy on selected Yes', c.screen.replace('❯ 1. Yes', '❯ 1. Yes, always allow')],
['selected No', c.screen.replace('❯ 1. Yes', ' 1. Yes').replace(' 3. No', ' ❯ 3. No')],
['arbitrary No suffix', c.screen.replace(/3\. No(?:hift\+tab\))?/, '3. No; run another command')],
['another hint', c.screen.replace(/3\. No(?:hift\+tab\))?/, '3. No(shift+enter)')],
['foreign option action', c.screen.replace(/always\s+allow\s+access\s+to/, 'delete files from')],
['unrecognized cropped prose', c.screen.replace(/^.*\n/, 'arbitrary text\n')],
];
for (const [name, screen] of changes) {
expect(screen, name).not.toBe(c.screen);
expect(adEpoch(c, screen), name).not.toBeTruthy();
expect(createPlanCountPermissionGuard()(screen, '', adEpoch(c, screen)), name).not.toBe('grant');
}
});
test(`AD crop ${c.p.pid} leaves unrelated-basename permission policy unchanged`, () => {
const screen = c.screen.replace(path.basename(c.binding.expected), 'OTHER.md');
expect(adEpoch(c, screen)).toBeUndefined();
// This is intentionally the existing caller policy for unrelated fixture permissions.
expect(createPlanCountPermissionGuard()(screen, '', adEpoch(c, screen))).toBe('grant');
});
test(`AD crop ${c.p.pid} cannot replace missing, stale, completed or foreign native identity`, () => {
const original = c.binding.state;
for (const [name, state, delta] of [
['foreign session', {...original, sessionId:'foreign'}, {}],
['wrong native transcript', {...original, transcriptPath:path.join(c.p.config, 'projects', 'foreign', 'other.jsonl')}, {}],
['completed request', {...original, completedId:original.pendingId}, {}],
['no pending request', {...original, pendingId:null}, {}],
['unseen pending request', {...original, pendingId:original.sessionId + ':other'}, {}],
['stale timestamp', {...original, timestamp:new Date(c.p.startUnix * 1000 - 1).toISOString()}, {}],
['future timestamp', {...original, timestamp:new Date(Date.now() + 60_000).toISOString()}, {}],
['mixed sessions', original, {transcript:{status:'ready', calls:[], assistantMessages:[{sessionId:original.sessionId},{sessionId:'foreign'}]}}],
['unready transcript', original, {transcript:{...c.observation.transcript,status:'unavailable'}}],
] as const) {
expect(adEpoch(c, c.screen, state, delta), name).toBeNull();
}
});
}
test('AE crop admits one native divider only', () => {
const c = cases.find(item => item.p.pid === capturedAe.pid)!;
const firstLine = c.screen.slice(0, c.screen.indexOf('\n') + 1);
for (const screen of [firstLine + c.screen, 'unrelated prose\n' + c.screen,
firstLine + 'Example:\n' + c.screen.slice(firstLine.length),
c.screen.replace(firstLine, firstLine.trimEnd() + ' extra action\n')]) {
expect(adEpoch(c, screen)).toBeNull();
expect(createPlanCountPermissionGuard()(screen, '', adEpoch(c, screen))).not.toBe('grant');
}
});
test('AH wrapped crop admits four or five spaces with the same owned native epoch', () => {
const c = cases.find(item => item.p.pid === capturedAh.pid)!;
expect(c.screen.startsWith(' + ')).toBe(true);
for (const screen of [c.screen, ` ${c.screen}`, c.screen.replace(/^ \+/, ' -')]) {
expect(adEpoch(c, screen)?.pendingId).toBe(c.binding.state.pendingId);
const guard = createPlanCountPermissionGuard();
expect(guard(screen, '', adEpoch(c, screen))).toBe('grant');
expect(guard(screen, '', adEpoch(c, screen))).toBe('handled');
}
// Cleaning the unselected No paint residue does not establish missing identity.
const noOnly = c.screen.replace('3. Nohift+tab)', '3. No');
expect(noOnly).not.toBe(c.screen);
expect(adEpoch(c, noOnly)?.pendingId).toBe(c.binding.state.pendingId);
});
test('AH continuation crop rejects prose, unsupported gutters and malformed numbered context', () => {
const c = cases.find(item => item.p.pid === capturedAh.pid)!;
for (const [name, screen] of [
['four-space prose', c.screen.replace(/^.*\n/, ' Apply this edit now\n')],
['four-space quoted prose', c.screen.replace(/^.*\n/, ' > Example\n')],
['three-space gutter', c.screen.slice(1)],
['six-space gutter', ` ${c.screen}`],
['no numbered rows', c.screen.replace(/^\s*\d+\s+(?=[+\- ])/gm, ' +')],
['one numbered row', c.screen.replace(/^(\s*\d+\s+)(?=[+\- ])/gm,
(prefix, _group, offset) => offset === c.screen.indexOf(' 79 ') ? prefix : ' +')],
]) {
expect(screen, name).not.toBe(c.screen);
expect(adEpoch(c, screen), name).toBeNull();
expect(createPlanCountPermissionGuard()(screen, '', adEpoch(c, screen)), name).not.toBe('grant');
}
expect(selectTests(['test/fixtures/plan-count-permission-ah.json'], E2E_TOUCHFILES).selected.sort()).toEqual(
selectTests(['test/fixtures/plan-count-permission-ae.json'], E2E_TOUCHFILES).selected.sort());
});
});
describe('plan-count-quoted-frame-ak', () => {
const fs = fs_batching_permission_at;
const os = os_batching_permission_at;
const path = path_batching_permission_at;
const exact = exact_plan_count_quoted_frame_ak;
const native = capturedAc;
const owned = owned_plan_count_quoted_frame_ak;
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.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();
process.stdout.write('PTY_READY:'+item.events+'\x1b[2J\x1b[H');
`);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,startupReadyMarker:'PTY_READY:'+events,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);
});