mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-03 01:46:55 +02:00
The targeted batching rerun on Claude Code 2.1.284 left its first report Write unanswered for 1,372 s and timed out: the viewport began at the pane's relative file row and rule, with the 'Create file' title cropped above, so the preview parser rejected the file row as foreign. That row must now resolve to the owned path and is skipped before the unchanged line-by-line preview match. Replay controls reject another file, another directory and an edited preview row.
152 lines
10 KiB
TypeScript
152 lines
10 KiB
TypeScript
import {describe, expect, test} from 'bun:test';
|
||
import * as fs from 'node:fs';
|
||
import * as os from 'node:os';
|
||
import * as path from 'node:path';
|
||
import {createFilePermissionRecorder, recordFilePermission, currentFilePermissionEpoch} from './helpers/plan-count-file-permission';
|
||
import {readPlanCountTranscript} from './helpers/plan-count-transcript';
|
||
import {createPlanCountPermissionGuard} from './helpers/claude-pty-runner';
|
||
import captures from './fixtures/plan-create-combined-permission-70b.json';
|
||
import croppedTitle from './fixtures/plan-create-cropped-title-batching.json';
|
||
|
||
function fixture(captured: typeof captures[number]) {
|
||
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'create-combined-'));
|
||
const cwd=path.join(dir,'owned report directory'), config=path.join(dir,'config');fs.mkdirSync(cwd);
|
||
const expected=path.join(cwd,path.basename(captured.event.input.file_path));
|
||
const sessionId=captured.event.sessionId, id=captured.event.toolUseId;
|
||
const journal=path.join(config,'projects','owned',sessionId+'.jsonl');fs.mkdirSync(path.dirname(journal),{recursive:true});
|
||
const recorder=createFilePermissionRecorder(cwd,config,expected)!;
|
||
const startedAt=Date.now()-1000, timestamp=new Date().toISOString();
|
||
// Only fixture ownership/time are rebound. Source content and viewport remain
|
||
// literal; replace the original directory in the native footer, not its preview.
|
||
const promptAt=captured.screen.indexOf('Do you want to create ');
|
||
const screen=captured.screen.slice(0,promptAt)+captured.screen.slice(promptAt)
|
||
.replaceAll(path.dirname(captured.event.input.file_path),cwd);
|
||
const input={...captured.event.input,file_path:expected};
|
||
const block={type:'tool_use',id,name:'Write',input};
|
||
const native=(role:string,content:any[])=>({cwd,sessionId,isSidechain:false,timestamp,message:{role,content}});
|
||
const original=[native('assistant',[{type:'text',text:'Preparing the review.'},block])];
|
||
const write=(rows:any[])=>fs.writeFileSync(journal,rows.map(r=>JSON.stringify(r)).join('\n')+'\n');
|
||
write(original);
|
||
recordFilePermission(JSON.stringify({hook_event_name:'PreToolUse',tool_name:'Write',session_id:sessionId,
|
||
tool_use_id:id,cwd,transcript_path:journal,tool_input:input}),recorder.file,cwd,config,expected);
|
||
const read=(s=screen)=>currentFilePermissionEpoch(recorder.file,expected,cwd,config,startedAt,
|
||
readPlanCountTranscript(config,cwd),s);
|
||
return {dir,cwd,screen,config,expected,sessionId,id,journal,recorder,input,block,native,original,write,read,
|
||
close(){recorder.dispose();fs.rmSync(dir,{recursive:true,force:true});}};
|
||
}
|
||
|
||
for(const captured of captures) {
|
||
test(`${captured.case}: actual combined Create grants the owned pending Write once`,()=>{
|
||
const f=fixture(captured);try {
|
||
const epoch=f.read();expect(epoch?.pendingId).toBe(`${f.sessionId}:${f.id}`);
|
||
const guard=createPlanCountPermissionGuard();
|
||
expect(guard(f.screen,'',epoch)).toBe('grant');
|
||
expect(guard(f.screen,'',epoch)).toBe('handled');
|
||
expect(fs.readFileSync(f.recorder.file,'utf8')).not.toContain(f.input.content);
|
||
}finally{f.close();}
|
||
});
|
||
|
||
for(const variant of ['single-line','different-wrap','no-key-hint','standalone'])
|
||
test(`${captured.case}: native option layout ${variant} preserves one-time action`,()=>{
|
||
const f=fixture(captured);try {
|
||
const at=f.screen.indexOf(' 2.');const prefix=f.screen.slice(0,at);
|
||
let footer=f.screen.slice(at);
|
||
if(variant==='single-line')footer=footer.replace(/;\s+Yes,\s+and\s+always\s+allow\s+access\s+to\s+/,'; Yes, and always allow access to ');
|
||
if(variant==='different-wrap')footer=footer.replace(/;\s+Yes,\s+and\s+always\s+allow\s+access\s+to\s+/,';\n Yes, and always\n allow access to ');
|
||
if(variant==='no-key-hint')footer=footer.replace(/\s*\(shift\+tab\)/,'');
|
||
if(variant==='standalone')footer=footer.replace(/;\s+Yes,\s+and\s+always\s+allow\s+access\s+to\s+[^\r\n]+?\s+for\s+this\s+session/,'');
|
||
expect(f.read(prefix+footer)?.pendingId).toBe(`${f.sessionId}:${f.id}`);
|
||
}finally{f.close();}
|
||
});
|
||
|
||
for(const variant of ['foreign-directory','ancestor','relative','traversal','quoted-directory','extra-directory',
|
||
'selected-2','selected-no','missing-no','changed-action','permanent-access','quoted','auq','missing-footer'])
|
||
test(`${captured.case}: combined Create rejects ${variant}`,()=>{
|
||
const f=fixture(captured);try {
|
||
let screen=f.screen;
|
||
if(variant==='foreign-directory')screen=screen.replace(f.cwd,f.cwd+'-foreign');
|
||
if(variant==='ancestor')screen=screen.replace(f.cwd,path.dirname(f.cwd));
|
||
if(variant==='relative')screen=screen.replace(f.cwd,'./owned report directory');
|
||
if(variant==='traversal')screen=screen.replace(f.cwd,f.cwd+'/../owned report directory');
|
||
if(variant==='quoted-directory')screen=screen.replace(f.cwd,'"'+f.cwd+'"');
|
||
if(variant==='extra-directory')screen=screen.replace(f.cwd,f.cwd+' and /foreign');
|
||
if(variant==='selected-2')screen=screen.replace('❯ 1. Yes',' 1. Yes').replace(' 2. Yes',' ❯ 2. Yes');
|
||
if(variant==='selected-no')screen=screen.replace('❯ 1. Yes',' 1. Yes').replace(' 3. No',' ❯ 3. No');
|
||
if(variant==='missing-no')screen=screen.replace('3. No','');
|
||
if(variant==='changed-action')screen=screen.replace('1. Yes','1. Yes, always allow');
|
||
if(variant==='permanent-access')screen=screen.replace(/for\s+this\s+session(?=\s*\(shift\+tab\))/,'permanently');
|
||
if(variant==='quoted')screen=screen.split('\n').map(l=>'> '+l).join('\n');
|
||
if(variant==='auq')screen='☐ Finding\n'+screen;
|
||
if(variant==='missing-footer')screen=screen.replace('Esc to cancel · Tab to amend','');
|
||
expect(screen).not.toBe(f.screen);expect(f.read(screen)).toBeFalsy();
|
||
}finally{f.close();}
|
||
});
|
||
|
||
for(const variant of ['foreign-path','foreign-id','completed','ambiguous','wrong-tool','wrong-content',
|
||
'missing-journal','wrong-session','completed-replay','conflicting-duplicate'])
|
||
test(`${captured.case}: combined footer cannot override ${variant} native evidence`,()=>{
|
||
const f=fixture(captured);try {
|
||
const changed=structuredClone(f.block);
|
||
if(variant==='foreign-path')changed.input.file_path=path.join(f.dir,path.basename(f.expected));
|
||
if(variant==='foreign-id')changed.id='foreign';
|
||
if(variant==='wrong-tool')changed.name='Edit';
|
||
if(variant==='wrong-content')changed.input.content='unrelated content';
|
||
const rows=[f.native('assistant',[{type:'text',text:'Preparing the review.'},changed])];
|
||
if(variant==='completed')rows.push(f.native('user',[{type:'tool_result',tool_use_id:f.id,content:'done'}]));
|
||
if(variant==='ambiguous')rows.push(f.native('assistant',[{...f.block,id:'foreign'}]));
|
||
if(variant==='completed-replay')rows.push(f.native('user',[{type:'tool_result',tool_use_id:f.id,content:'done'}]),f.native('assistant',[structuredClone(f.block)]));
|
||
if(variant==='conflicting-duplicate')rows.unshift(f.native('assistant',[{...f.block,input:{...f.input,content:'different'}}]));
|
||
if(variant==='wrong-session')rows[0].sessionId='foreign';
|
||
f.write(rows);if(variant==='missing-journal')fs.unlinkSync(f.journal);
|
||
expect(f.read()).toBeNull();
|
||
}finally{f.close();}
|
||
});
|
||
}
|
||
|
||
describe('Create pane cropped below its title (plan-eng-multi-finding-batching, CLI 2.1.284)', () => {
|
||
function cropped() {
|
||
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'create-cropped-'));
|
||
const cwd=path.join(dir,path.basename(croppedTitle.cwd)), config=path.join(dir,'config');fs.mkdirSync(cwd);
|
||
const originalDir=path.dirname(croppedTitle.event.input.file_path);
|
||
const expected=path.join(dir,path.basename(originalDir),path.basename(croppedTitle.event.input.file_path));
|
||
fs.mkdirSync(path.dirname(expected));
|
||
const {sessionId,toolUseId:id}=croppedTitle.event, timestamp=new Date().toISOString();
|
||
const journal=path.join(config,'projects','owned',sessionId+'.jsonl');fs.mkdirSync(path.dirname(journal),{recursive:true});
|
||
const recorder=createFilePermissionRecorder(cwd,config,expected)!;
|
||
const input={...croppedTitle.event.input,file_path:expected};
|
||
fs.writeFileSync(journal,JSON.stringify({cwd,sessionId,isSidechain:false,timestamp,
|
||
message:{role:'assistant',content:[{type:'text',text:'Writing the review report.'},{type:'tool_use',id,name:'Write',input}]}})+'\n');
|
||
recordFilePermission(JSON.stringify({hook_event_name:'PreToolUse',tool_name:'Write',session_id:sessionId,
|
||
tool_use_id:id,cwd,transcript_path:journal,tool_input:input}),recorder.file,cwd,config,expected);
|
||
// The relative file row keeps its captured sibling layout; only the footer's
|
||
// absolute directory is rebound to this fixture.
|
||
const screen=croppedTitle.screen.replaceAll(originalDir,path.dirname(expected));
|
||
const read=(s=screen)=>currentFilePermissionEpoch(recorder.file,expected,cwd,config,Date.now()-1000,readPlanCountTranscript(config,cwd),s);
|
||
return {screen,read,id:`${sessionId}:${id}`,close(){recorder.dispose();fs.rmSync(dir,{recursive:true,force:true});}};
|
||
}
|
||
|
||
test('the captured pane shows its file row and rule but not the Create file title', () => {
|
||
expect(croppedTitle.screen).not.toMatch(/(?:^|\n) {0,3}Create file[ \t]*\n/);
|
||
expect(croppedTitle.screen.split('\n')[0]).toBe(' ../gstack-e2e-plan-eng-batching-DINQ9m/gstack-test-plan-eng-batching.md');
|
||
});
|
||
|
||
test('the owned file row binds the pending Write and grants it once', () => {
|
||
const f=cropped();try {
|
||
const epoch=f.read();expect(epoch?.pendingId).toBe(f.id);
|
||
const guard=createPlanCountPermissionGuard();
|
||
expect(guard(f.screen,'',epoch)).toBe('grant');
|
||
expect(guard(f.screen,'',epoch)).toBe('handled');
|
||
}finally{f.close();}
|
||
});
|
||
|
||
for(const [name,change] of [
|
||
['a file row naming another file',(s:string)=>s.replace('/gstack-test-plan-eng-batching.md\n','/other.md\n')],
|
||
['a file row in another directory',(s:string)=>s.replace(' ../gstack-e2e-plan-eng-batching-DINQ9m/',' ../elsewhere/')],
|
||
['an edited preview row',(s:string)=>s.replace('We will leave the','We will fix the')],
|
||
] as const) test(`the cropped pane rejects ${name}`,()=>{
|
||
const f=cropped();try {
|
||
const screen=change(f.screen);expect(screen).not.toBe(f.screen);expect(f.read(screen)).toBeFalsy();
|
||
}finally{f.close();}
|
||
});
|
||
});
|