From 74c3184dccf761bc04d144847d090ff3b347b0b3 Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 29 Sep 2026 19:47:15 +0000 Subject: [PATCH] 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. --- .../plan-create-cropped-title-batching.json | 13 +++++ test/helpers/plan-count-file-permission.ts | 6 ++- test/plan-create-combined-permission.test.ts | 50 ++++++++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/plan-create-cropped-title-batching.json diff --git a/test/fixtures/plan-create-cropped-title-batching.json b/test/fixtures/plan-create-cropped-title-batching.json new file mode 100644 index 000000000..8f970a06e --- /dev/null +++ b/test/fixtures/plan-create-cropped-title-batching.json @@ -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" + } + } +} \ No newline at end of file diff --git a/test/helpers/plan-count-file-permission.ts b/test/helpers/plan-count-file-permission.ts index 8a792fa0d..824df6fa5 100644 --- a/test/helpers/plan-count-file-permission.ts +++ b/test/helpers/plan-count-file-permission.ts @@ -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)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) { diff --git a/test/plan-create-combined-permission.test.ts b/test/plan-create-combined-permission.test.ts index a61523c62..639421938 100644 --- a/test/plan-create-combined-permission.test.ts +++ b/test/plan-create-combined-permission.test.ts @@ -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();} + }); +});