test(pty): grant an owned Create pane whose title row is cropped

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.
This commit is contained in:
garrytan committed 2026-09-29 19:47:15 +00:00
1 parent 49761c97d6
commit 74c3184dcc
3 files changed
+67 -2

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
{
"source": "local rerun smoke-2.1.284-1790709409 (Claude Code 2.1.284) of plan-eng-multi-finding-batching: the Create pane stayed unanswered for 1,372 s because its title row was cropped above the file row",
"cwd": "/tmp/gstack-plan-count-Z3cntL",
"screen": " ../gstack-e2e-plan-eng-batching-DINQ9m/gstack-test-plan-eng-batching.md\n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n 1 # Eng Review \u2014 Plan: Add background job retry framework\n 2\n 3 Review target (fixed): `/tmp/gstack-plan-count-Z3cntL/PLAN.md` on branch `main` (commit 844c6ae)\n 4 Reviewer: /plan-eng-review (Claude, session 196868-1790709430-09cebc2c), 2026-09-29\n 5 Report file: this file (user-requested destination)\n 6\n 7 ## Original plan (unchanged copy)\n 8\n 9 # Plan: Add background job retry framework\n 10\n 11 ## Architecture\n 12 We'll roll a custom exponential-backoff scheduler inline in each worker\n 13 rather than use the existing job library's built-in retry hooks. Same\n 14 shape as the library version, but we want full control over the curve.\n 15\n 16 ## Code quality\n 17 The retry envelope (compute delay, log attempt, dispatch) is duplicated\n 18 across 5 worker files with copy-pasted bodies. We will leave the\n 19 duplication for now and refactor \"later.\"\n 20\n 21 ## Tests\n 22 The existing `processWebhookJob()` flow gets rewritten as part of this\n 23 change. No regression test for the prior at-most-once delivery guarantee\n 24 is planned.\n 25\n 26 ## Performance\n 27 On every retry we re-fetch the full job payload from the database, then\n 28 iterate the payload to recompute the dependency graph. Could cache the\n 29 graph on the first attempt; not planned.\n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n Do you want to create gstack-test-plan-eng-batching.md?\n \u276f 1. Yes\n 2. Yes, and switch to accept edits (auto-approve file edits and common file commands) for this session; Yes, and\n always allow access to /tmp/gstack-e2e-plan-eng-batching-DINQ9m for this session (shift+tab)\n 3. No\n\n Esc to cancel \u00b7 Tab to amend\n",
"event": {
"sessionId": "16914623-f7a4-49ca-bea5-fb4b56274cb1",
"toolUseId": "toolu_01BaNDAjTWTr5bf2sdwWC4DA",
"input": {
"file_path": "/tmp/gstack-e2e-plan-eng-batching-DINQ9m/gstack-test-plan-eng-batching.md",
"content": "# Eng Review \u2014 Plan: Add background job retry framework\n\nReview target (fixed): `/tmp/gstack-plan-count-Z3cntL/PLAN.md` on branch `main` (commit 844c6ae)\nReviewer: /plan-eng-review (Claude, session 196868-1790709430-09cebc2c), 2026-09-29\nReport file: this file (user-requested destination)\n\n## Original plan (unchanged copy)\n\n# Plan: Add background job retry framework\n\n## Architecture\nWe'll roll a custom exponential-backoff scheduler inline in each worker\nrather than use the existing job library's built-in retry hooks. Same\nshape as the library version, but we want full control over the curve.\n\n## Code quality\nThe retry envelope (compute delay, log attempt, dispatch) is duplicated\nacross 5 worker files with copy-pasted bodies. We will leave the\nduplication for now and refactor \"later.\"\n\n## Tests\nThe existing `processWebhookJob()` flow gets rewritten as part of this\nchange. No regression test for the prior at-most-once delivery guarantee\nis planned.\n\n## Performance\nOn every retry we re-fetch the full job payload from the database, then\niterate the payload to recompute the dependency graph. Could cache the\ngraph on the first attempt; not planned.\n"
}
}
}
+5 -1
View File
@@ -284,7 +284,11 @@ function currentCreatePreview(preview: string, r: any, config: string, cwd: stri
if(event.name!=='Write'||`${event.sessionId}:${event.toolUseId}`!==r.pendingId||event.input?.file_path!==r.expected||
Date.parse(event.timestamp)<startedAt||typeof event.input.content!=='string'||
Buffer.byteLength(event.input.content)>MAX_WRITE_INPUT_BYTES) return false;
const source=event.input.content.split(/\r?\n/), rows=preview.split('\n');
// A crop can keep the pane's file row and rule above the preview while its
// "Create file" title scrolls away. That row must name the owned path.
const header=/^ {0,3}(?![1-9]\d*(?:[ \t]|\n))(\S[^\n]*)\n[╌─━]{3,}[ \t]*\n/.exec(preview);
if(header && path.resolve(cwd,header[1]!.trim())!==r.expected) return false;
const source=event.input.content.split(/\r?\n/), rows=preview.slice(header?.[0].length ?? 0).split('\n');
const numbered:Array<{line:number;text:string}>=[];
let leading='';
for(const row of rows) {
+49 -1
View File
@@ -1,4 +1,4 @@
import {expect, test} from 'bun:test';
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';
@@ -6,6 +6,7 @@ import {createFilePermissionRecorder, recordFilePermission, currentFilePermissio
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-'));
@@ -101,3 +102,50 @@ for(const captured of captures) {
}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();}
});
});