Files
gstack/test/plan-count-file-permission.test.ts
T
Garry Tan 943105f109 v1.91.8.0 test: smaller suite, derived paid selection, retired never-green evals (#2994)
* test: delete test-infrastructure dead code (G)

- exit-propagation drives the runner's real strict verdict
  (BunTestOutputClassifier + strictTestExitCode); delete the unused
  shardRunLooksTruncated predicate.
- delete skill-coverage-matrix registry + its gate (nothing reads it; the
  floor already iterates skillCensus()).
- delete touchfiles-facade export-parity tests (Bun fails missing imports
  at link time) and the duplicated E2E_TIERS tier-value test.
- delete brain-cache-spec TRANSPORT_DEFAULT_POLICY, SKILL_RUN_RETENTION_DAYS
  and the now-unused BrainTrustPolicy type with their literal tests.
  AUTOPLAN_PREFLIGHT_BUDGET_BYTES stays: skill-preflight-budget enforces it
  against real resolver output.
- delete audit-compliance's JSDoc-comment grep.

* test: replace product tests that fake the product with real-boundary tests (F)

- design: serve.test.ts drove an inline mirror server; now two tests run the
  real serve() on an ephemeral port (reload confinement, submit exit 0).
- setup-gbrain: rollback + voyage tests execute the template-extracted init
  blocks (3 sites) instead of drifted local bash copies.
- terminal-agent: internalHandler source greps replaced by a behavioral
  /internal/grant + /internal/revoke auth matrix (no/wrong/valid token).
- /health: server-security-surface and the server-auth / security-audit-r2 /
  sidebar-tabs source greps fold into one liveness-only check on the real
  body; the L4 sidecar wiring gets a behavioral /pty-inject-scan test.
- delete tautologies (browser-manager onDisconnect, memory-command #12),
  ios swiftui tap fixture self-check, memory-ingest put_page grep, detach
  source greps, sidebar-agent absence pins, dead-CSS pins + the dead CSS,
  security-audit-r2 Task 1 + the test-only meta-commands re-export,
  duplicate generated-SKILL.md checks.
- make-pdf coverage-gaps cases move into their owner test files.

* test: delete tests of dead eval code (A)

- A1: the retired Eng lexical oracle (evaluateEngSeedCoverage,
  isEngSeedDecisionAUQ), the completion-handoff detector and the retained
  corpus had no paid caller since v1.87.6; delete their 26 replay files,
  ~2.6k helper LOC and fixtures, and the dead blocks in 8 mixed files
  (live hasNativePlanTerminal / batching assertions stay).
- A2: dead viewport approvers in autoplan-artifact-permission and their 11
  replay files + fixtures; recorder/launcher cases stay.
- A3: never-wired oracles and seeders (autoplan-phase-order,
  eng-finding-fixture, ceo-paired-fixture, design-ui-scope,
  plan-skill-completion, pty-current-screen, required-reads,
  transcript-section-logger); plan-seed-submission now decodes through the
  production createPtyScreen; section manifests name their actual guard.
- A4: zero-reference helper exports, plus execGit and invokeAndObserve
  found by the reachability pass.
- 52 fixtures orphaned by the deletions; touchfile and selection-table
  entries for every deleted path.

* test: clean up the paid eval lane (B1-B4, B6, B7)

- B1: delete paid files that assert nothing or cannot pass meaningfully:
  skill-llm-eval-spec and skill-e2e-spec-execute (test.todo), gemini-e2e
  (+ gemini-session-runner; no gemini CLI in CI), ship-idempotency (red
  since v1.63), the two opus-4-7 *-sonnet overlay wrappers, conductor-prose
  (+ its source-evaluation replay), codex-e2e-plan-format; drop their keys,
  scripts and census rows.
- B2: skill-llm-eval grades browse/sections/command-list.md with one union
  judge that also carries the baseline score pin; regression-vs-baseline
  deleted (paid run: pass, c4/c4/a4).
- B3: memory-pipeline, ios-qa, ios-qa-swift-build and plan-tune-cathedral
  make no model calls; renamed out of the paid glob so they run on every
  PR. Swift builds need GSTACK_TEST_SWIFT=1; device stub deleted.
- B4: codex-e2e*, outside-voice, aside and ios-device cannot run in the CI
  image; excluded from the weekly lane with a tracked re-entry condition.
- B6: fold opus-47's negative routing controls into skill-routing-e2e
  journey-negatives (paid run: 3/3 unrouted) and delete the file.
- B7: delete the never-green brain-privacy-gate eval; a free
  gstack-skill-start test now proves consent precedes artifacts egress.

* test: retire the finding-count cluster and trim its helpers (C)

- C0/C1: the five never-green evals (skill-e2e-autoplan-chain and
  skill-e2e-plan-{ceo,eng,design,devex}-finding-count) failed on harness and
  budget, never on skill behavior; delete them, their touchfile/tier ids,
  AUTOPLAN_CHAIN_BUDGET and the dedicated eighth periodic slice (--slices 7).
- C2: delete the helper groups whose only paid consumers were those files
  (11 modules), trim claude-pty-runner and eng-seeded-coverage to the paid
  closure, and delete the free replay tests whose assertions exercised only
  that dead code (89 files, 135 orphaned fixtures). Blocks that used dead code
  only as input for a live subject keep their assertions: the multiSelect
  default moved to plan-review-decisions, runner PTY tests use inline caller
  policies, and the timer-safe budget checks moved to eng-finding-retry-budget.
- The eight production-touching files stay except ceo-current-decision-record
  (its template read only feeds the retired counter).
- CARVE_GUARDS.autoplan is behavioral 'none'; TODOS records the lost chain
  and per-finding cadence coverage with their re-entry tests.

* test: fold per-incident replay series into their detector owners (D)

Twelve detector families move into one owner test each: 73 incident files
become describe blocks in ceo-section-loading-fixture (stale-fill race),
model-overlays, coverage-audit-evidence, autoplan-phase-observer,
native-auto-decide, outside-voice-evidence, eng-first-review,
plan-count-completion, plan-count-file-permission, ceo-mode-option,
plan-scope-selection and plan-count-prerequisite. Each block keeps its original
code and fixture, so every case still runs; only tests asserting the incident
file's own touchfile registration are dropped (41). Touchfile lists that named
an incident now name its owner.

* test: start the plan-count history PTY on its readiness marker (H)

The fake CLI prints a startup marker and the runner waits for it instead of the
fixed 8 s startup sleep (8.6 s -> 0.9 s locally). eng-semantic-terminal's
sleeping registration cases went with C; plan-count-timeout keeps the fixed wait
because it asserts deadline behavior.

* 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.

* test: skip hollow tier shards and census judges in the paid planner (B5)

A paid file is now skipped for a tier lane only when every E2E id it registers
is known statically and none has that tier; ids come from the touchfile
registrations and literal testName/*IfSelected arguments, so a comment or
skill path that quotes another id cannot unschedule it, and computed names
keep today's scheduling. --list and the manifest show each skip as
"skipped: no E2E_TIERS id has tier <tier>". The weekly gate census drops the
LLM judges (--skip-judges); they still run in the periodic census and PR gate
lanes. Gate lane 52 -> 42 files, census 41; periodic 77 -> 69.

* test: run seven paid evals on the current default capture model (B8)

skill-e2e-{auq-matrix,plan-format,qa-bugs,retro,workflow} pinned
claude-opus-4-7 and skill-e2e-office-hours plus -brain-writeback pinned
claude-sonnet-4-6; none tests a historical model, so they now capture with
resolveEvalModel('capture'), and the free harness tests that execute these
registrations receive the same resolver. The paid re-pin run passed all of
them. skill-e2e-{design,office-hours-phase4,plan-prosons,plan} keep
claude-opus-4-7: six of their cases failed on the default model (three
timeouts, a missing report file, a format miss and a posture score of 3), so
per the plan's fallback they keep their pins with a TODOS entry. The pre-spend
estimate and drop threshold are in docs/test-audit-2026-09.md.

* test: guard the reduced suite against new test-of-test files

- test/test-of-test-ratchet.test.ts records the 228 free tests that import only
  test/ code and fails on a new one, naming the owner test to extend instead;
  a stale baseline entry fails with the remove instruction.
- test/helpers/resolve-repo-path.ts is the one specifier/literal resolver for
  the ratchet and the touchfile closure invariant, with its own unit tests.
- CONTRIBUTING "Test tiers" describes the paid-failure workflow (fix, then one
  row in the detector's owner test) and the ratchet; TEST_PORTFOLIO gains the
  detector -> owner-test table and no longer claims an Autoplan chain eval.
- TODOS: automatic exclusion policy for chronically red periodic files (P3),
  the deferred native-completion table collapse, the unused CEO payment
  seeder; the PTY readiness item is narrowed to the paid runner.
- docs/test-audit-2026-09.md collects the triage, security mapping, inventories,
  selection proof, behavior-commit decisions and retained false positives.

* v1.91.8.0 test: smaller suite, derived paid selection, retired never-green evals

Release metadata for the test-reduction branch: VERSION 1.91.8.0 (1.91.7.0 is
claimed by #2983), CHANGELOG with the measured before/after table and a
contributor section, durations re-recorded on Ubicloud standard-16 (857 files,
0 failures), the agents digest, CONTRIBUTING's after-measurement row, the B8
fallback TODOS entry, and the after metrics, kept-vs-plan notes, B8 run and
census estimate in docs/test-audit-2026-09.md.

* fix(ubicloud): skip retrieval globs that match nothing instead of reporting a failed pull

* test: count issue-numbered Design findings in the UI-scope gate eval
2026-09-29 08:58:49 -07: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);
});