import {afterEach, expect, test} from 'bun:test'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import {createHash} from 'node:crypto'; import {readPlanCountTranscript, type NativePublicToolEvent} from './helpers/plan-count-transcript'; import {auditAutoplanMethodReads} from './helpers/autoplan-method-read-audit'; import {autoplanPhaseCompletions} from './helpers/autoplan-phase-observer'; import {readOwnedClaudePublicTranscript} from '../lib/claude-public-transcript'; const dirs:string[]=[]; afterEach(()=>{for(const d of dirs.splice(0))fs.rmSync(d,{recursive:true,force:true});}); const uuid=(n:number)=>`00000000-0000-4000-8000-${String(n).padStart(12,'0')}`; const sid='session-owned',cwd='/fixture/repo',archive='/fixture/state/project/ceo-plans'; const time='2026-09-11T01:23:54.673Z'; const record=(n:number,parent:number|null,where=cwd,role='assistant',content:any[]=[])=>({ sessionId:sid,cwd:where,isSidechain:false,uuid:uuid(n),parentUuid:parent===null?null:uuid(parent), timestamp:time,message:{role,content}, }); const use={type:'tool_use',id:'read-owned',name:'Read',input:{file_path:'/fixture/state/methodology.md',offset:1,limit:2}}; const file={filePath:'/fixture/state/methodology.md',startLine:1,numLines:2,totalLines:2,content:'Review every phase.\nPreserve ownership.'}; function rows():any[]{return [ record(1,null,cwd,'user',[{type:'text',text:'Run the review.'}]), {...record(2,1),message:undefined,type:'attachment'}, record(3,2,archive,'assistant',[use]), {...record(4,3,archive,'user',[{type:'tool_result',tool_use_id:'read-owned',content:'Public Read output.'}]),toolUseResult:{file}}, record(5,4,archive,'assistant',[{type:'text',text:'Phase 1 complete.'}]), record(6,5,cwd,'assistant',[{type:'tool_use',id:'dispatch',name:'Agent',input:{prompt:'You are the independent DESIGN reviewer for this phase.\nRead the bound methodology.'}}]), ];} function read(records:any[],options:{partial?:boolean,name?:string}={}){ const config=fs.mkdtempSync(path.join(os.tmpdir(),'plan-cwd-'));dirs.push(config); const project=path.join(config,'projects','owned');fs.mkdirSync(project,{recursive:true}); const journal=path.join(project,(options.name??sid)+'.jsonl'); const bytes=records.map(r=>JSON.stringify(r)).join('\n')+(options.partial?'':'\n');fs.writeFileSync(journal,bytes); const events:NativePublicToolEvent[]=[];const transcript=readPlanCountTranscript(config,cwd,e=>events.push(e)); expect(fs.readFileSync(journal,'utf8')).toBe(bytes); return {events,transcript}; } const methodology={phase:'design',path:file.filePath,content:file.content,lines:2,bytes:Buffer.byteLength(file.content),sha256:createHash('sha256').update(file.content).digest('hex')}; const designAudit=(events:NativePublicToolEvent[])=>auditAutoplanMethodReads(events,()=>methodology).find(a=>a.phase==='design'); test('same native ancestry survives cwd changes and retains exact methodology delivery',()=>{ const {events,transcript}=read(rows()); expect(events.map(e=>e.toolUseId)).toEqual(['read-owned','read-owned','dispatch']); expect(designAudit(events)?.passed).toBe(true); expect(autoplanPhaseCompletions(transcript,0)).toEqual([{phase:1,ts:Date.parse(time)}]); }); test('the directory name is not an allowlist and returning to the original cwd keeps the chain',()=>{ const r=rows();for(const x of r)if(x.cwd===archive)x.cwd='/different/owned/work-directory'; r.push(record(7,6,'/another/directory','assistant',[{type:'text',text:'Phase 2 complete.'}])); expect(autoplanPhaseCompletions(read(r).transcript,0).map(x=>x.phase)).toEqual([1,2]); }); for(const [name,change] of Object.entries({ 'missing origin':(r:any[])=>r.shift(), 'foreign origin':(r:any[])=>r[0].cwd='/another/fixture', 'assistant origin':(r:any[])=>r[0].message.role='assistant', 'non-root origin':(r:any[])=>r[0].parentUuid=uuid(99), 'missing root UUID':(r:any[])=>delete r[0].uuid, 'invalid root UUID':(r:any[])=>r[0].uuid='root', 'sidechain origin':(r:any[])=>r[0].isSidechain=true, 'agent origin':(r:any[])=>r[0].agentId='child', 'invalid origin time':(r:any[])=>r[0].timestamp='unknown', 'disconnected branch':(r:any[])=>r[2].parentUuid=uuid(99), 'foreign branch session':(r:any[])=>r[2].sessionId='another-session', 'sidechain branch':(r:any[])=>r[2].isSidechain=true, 'agent branch':(r:any[])=>r[2].agentId='child', 'relative branch cwd':(r:any[])=>r[2].cwd='relative-directory', 'missing branch cwd':(r:any[])=>delete r[2].cwd, 'invalid branch time':(r:any[])=>r[2].timestamp='unknown', 'missing branch UUID':(r:any[])=>delete r[2].uuid, 'duplicate root UUID':(r:any[])=>r[2].uuid=r[0].uuid, 'missing attachment link':(r:any[])=>r.splice(1,1), }))test(`${name} cannot supply changed-cwd evidence`,()=>{ const r=rows();change(r);const {events,transcript}=read(r); expect(events.some(e=>e.toolUseId==='read-owned')).toBe(false); expect(autoplanPhaseCompletions(transcript,0)).toEqual([]); expect(designAudit(events)?.passed).toBe(false); }); test('a later matching root cannot rebind a session that began in another fixture',()=>{ const r=rows();r.unshift(record(99,null,'/another/fixture','user')); expect(read(r).events.some(e=>e.toolUseId==='read-owned')).toBe(false); }); test('legacy records without native ancestry retain exact-cwd scoping',()=>{ const r=rows();for(const x of r){delete x.uuid;delete x.parentUuid;} const x=read(r);expect(x.events.map(e=>e.toolUseId)).toEqual(['dispatch']); expect(autoplanPhaseCompletions(x.transcript,0)).toEqual([]); }); test('filename and session identity remain bound',()=>{ expect(read(rows(),{name:'foreign'}).transcript.status).toBe('missing'); }); test('an incomplete final record cannot supply a changed-cwd completion',()=>{ const r=rows().slice(0,5);expect(autoplanPhaseCompletions(read(r,{partial:true}).transcript,0)).toEqual([]); }); test('a changed-cwd question packet still counts once and preserves unanswered tabs',()=>{ const questions=['First decision?','Second decision?'].map(question=>({header:'Decision',question,options:[{label:'A'},{label:'B'}]})); const r=rows();r.push(record(7,6,archive,'assistant',[{type:'tool_use',id:'ask',name:'AskUserQuestion',input:{questions}}]), {...record(8,7,archive,'user',[{type:'tool_result',tool_use_id:'ask',content:'Answered first tab.'}]),toolUseResult:{answers:{'First decision?':'A'}}}); const t=read(r).transcript;expect(t.calls).toHaveLength(1);expect(t.calls[0].answered).toBe(true); expect(t.calls[0].unansweredQuestionIndices).toEqual([1]);expect(t.calls[0].questions).toEqual(questions); }); test('a changed-cwd failed plan approval remains failed',()=>{ const r=rows();r.push(record(7,6,archive,'assistant',[{type:'tool_use',id:'exit',name:'ExitPlanMode',input:{}}]), record(8,7,archive,'user',[{type:'tool_result',tool_use_id:'exit',content:'Not approved.',is_error:true}])); const t=read(r).transcript;expect(t.planReadyRequests).toHaveLength(1);expect(t.planReadyRequests![0].failed).toBe(true);expect(t.calls).toEqual([]); }); for(const [name,change] of Object.entries({ 'foreign file path':(r:any[])=>r[3].toolUseResult={file:{...file,filePath:'/other/methodology.md'}}, 'incorrect content':(r:any[])=>r[3].toolUseResult={file:{...file,content:'Omitted required instructions.'}}, 'error result':(r:any[])=>r[3].message.content[0].is_error=true, 'wrong result id':(r:any[])=>r[3].message.content[0].tool_use_id='another-tool', 'future result':(r:any[])=>r[3].timestamp='2026-09-11T02:00:00.000Z', }))test(`cwd continuation does not bypass ${name}`,()=>{ const r=rows();change(r);expect(designAudit(read(r).events)?.passed).toBe(false); }); // Native90f compact boundaries reset parentUuid and retain the owned parent // in logicalParentUuid. Summary timestamps can precede their boundary slightly; // append-order graph metadata, not summary prose, connects the next tool turn. function compactRows():any[]{ const r=rows();r[2].parentUuid=uuid(21); r.splice(2,0,{...record(20,null),type:'system',subtype:'compact_boundary',logicalParentUuid:uuid(2),message:undefined}, {...record(21,20,cwd,'user'),isCompactSummary:true,timestamp:'2026-09-11T01:23:54.550Z',message:{role:'user',content:'Context summary; not an announcement or tool result.'}}); return r; } test('compact boundary retains changed-cwd tool request, ACK and actual phase text',()=>{ const {events,transcript}=read(compactRows()); expect(events.map(e=>e.toolUseId)).toEqual(['read-owned','read-owned','dispatch']); expect(designAudit(events)?.passed).toBe(true); expect(autoplanPhaseCompletions(transcript,0)).toEqual([{phase:1,ts:Date.parse(time)}]); }); test('compact boundary can repeat on the same owned append-order ancestry',()=>{ const r=compactRows();r.push({...record(30,null,archive),type:'system',subtype:'compact_boundary',logicalParentUuid:uuid(6),message:undefined}, {...record(31,30,archive,'user'),isCompactSummary:true,message:{role:'user',content:'Phase 3 complete. Quoted prior context only.'}}, record(32,31,archive,'assistant',[{type:'text',text:'Phase 2 complete.'}])); expect(autoplanPhaseCompletions(read(r).transcript,0).map(x=>x.phase)).toEqual([1,2]); }); for(const [name,change] of Object.entries({ 'missing owned origin':(r:any[])=>r.shift(), 'foreign owned origin':(r:any[])=>r[0].cwd='/other/fixture', 'rootless later reset':(r:any[])=>{r[0].parentUuid=uuid(99);}, 'unknown logical parent':(r:any[])=>r[2].logicalParentUuid=uuid(99), 'missing logical parent':(r:any[])=>delete r[2].logicalParentUuid, 'invalid logical parent':(r:any[])=>r[2].logicalParentUuid='prior-message', 'unowned prior session parent':(r:any[])=>{r[1].sessionId='foreign-session';}, 'foreign boundary session':(r:any[])=>r[2].sessionId='foreign-session', 'sidechain boundary':(r:any[])=>r[2].isSidechain=true, 'agent boundary':(r:any[])=>r[2].agentId='child', 'missing boundary scope':(r:any[])=>delete r[2].isSidechain, 'relative boundary cwd':(r:any[])=>r[2].cwd='relative', 'invalid boundary time':(r:any[])=>r[2].timestamp='unknown', 'missing boundary time':(r:any[])=>delete r[2].timestamp, 'missing boundary UUID':(r:any[])=>delete r[2].uuid, 'stale reused boundary UUID':(r:any[])=>r[2].uuid=uuid(2), 'wrong boundary type':(r:any[])=>r[2].type='assistant', 'wrong boundary subtype':(r:any[])=>r[2].subtype='summary', 'unknown non-null parent':(r:any[])=>r[2].parentUuid=uuid(99), 'body masquerading as boundary':(r:any[])=>r[2].message={role:'user',content:[{type:'text',text:'compact_boundary logicalParentUuid='+uuid(2)}]}, 'quoted boundary source':(r:any[])=>{const text=JSON.stringify(r[2]);r[2]={...record(20,null),message:{role:'assistant',content:[{type:'text',text}]}};}, }))test('compact boundary rejects '+name,()=>{ const r=compactRows();change(r);const {events,transcript}=read(r); expect(events.some(e=>e.toolUseId==='read-owned')).toBe(false); expect(autoplanPhaseCompletions(transcript,0)).toEqual([]); expect(designAudit(events)?.passed).toBe(false); }); test('compact boundary does not admit a foreign later root or bypass failed methodology',()=>{ const foreign=compactRows();foreign.unshift(record(99,null,'/other/fixture','user')); expect(read(foreign).events.some(e=>e.toolUseId==='read-owned')).toBe(false); const failed=compactRows();failed[5].message.content[0].is_error=true; expect(designAudit(read(failed).events)?.passed).toBe(false); }); // The pinned 2.1.251 native loopback appended assistant records before its // initial human/attachment prefix. UUID parents establish order; timestamps do // not. The complete original public replay and native failure remain retained. function readOwned(records:any[],partial=false){ const config=fs.mkdtempSync(path.join(os.tmpdir(),'owned-causal-'));dirs.push(config); const project=path.join(config,'projects','owned');fs.mkdirSync(project,{recursive:true}); const journal=path.join(project,sid+'.jsonl'); const bytes=records.map(r=>JSON.stringify(r)).join('\n')+(partial?'':'\n');fs.writeFileSync(journal,bytes); const result=readOwnedClaudePublicTranscript(journal,cwd,sid); expect(fs.readFileSync(journal,'utf8')).toBe(bytes); return result; } function delayedOriginRows():any[]{const r=rows();return [r[2],r[0],r[1],...r.slice(3)];} test('owned native public records admit a complete later-appended root by causal UUID ancestry',()=>{ const normal=readOwned(rows()),delayed=readOwned(delayedOriginRows()); expect(delayed.transcript.status).toBe('ready');expect(delayed).toEqual(normal); expect(delayed.events.filter(e=>e.kind==='use').map(e=>e.toolUseId)).toEqual(['read-owned','dispatch']); }); test('owned causal order preserves human rearm and end-turn ordering without timestamp sorting',()=>{ const r=rows();r[0].origin={kind:'human'};r[0].promptId=uuid(80); r[0].message.content='autoplan\n/autoplan'; r[4].message.stop_reason='end_turn'; r.push({...record(7,6,cwd,'user'),origin:{kind:'human'},promptSource:'typed',promptId:uuid(81),message:{role:'user',content:'Unrelated human task.'}}, record(8,7,cwd,'assistant',[{type:'text',text:'Ordinary human response.'}]), {...record(9,8,cwd,'user'),origin:{kind:'human'},promptId:uuid(82),message:{role:'user',content:'autoplan\n/autoplan'}}); const original=readOwned(r);const reordered=[r[6],r[2],r[8],r[0],r[1],...r.slice(3,6),r[7]]; expect(readOwned(reordered)).toEqual(original); expect(original.events.filter(e=>e.kind==='user_turn').map(e=>e.autoplan)).toEqual([true,false,true]); expect(original.events.findIndex(e=>e.kind==='end_turn')).toBeLessThan(original.events.findIndex(e=>e.kind==='user_turn'&&!e.autoplan)); }); test('owned native metadata siblings keep their physical tie order',()=>{ const r=rows();r.splice(4,0,{...record(90,3),type:'attachment',message:undefined}); expect(readOwned([r[2],r[0],r[1],...r.slice(3)])).toEqual(readOwned(r)); }); test('owned causal compaction follows the authenticated logical parent',()=>{ const r=compactRows();expect(readOwned([r[4],r[3],r[2],r[0],r[1],...r.slice(5)])).toEqual(readOwned(r)); expect(readOwned(r).events.some(e=>e.kind==='use'&&e.toolUseId==='read-owned')).toBe(true); }); for(const [name,change] of Object.entries({ 'missing root':(r:any[])=>r.splice(1,1), 'foreign root cwd':(r:any[])=>r[1].cwd='/another/fixture', 'sidechain root':(r:any[])=>r[1].isSidechain=true, 'agent root':(r:any[])=>r[1].agentId='child', 'foreign root session':(r:any[])=>r[1].sessionId='foreign', 'non-human root':(r:any[])=>r[1].message.role='assistant', 'missing attachment':(r:any[])=>r.splice(2,1), 'foreign attachment':(r:any[])=>r[2].sessionId='foreign', 'sidechain attachment':(r:any[])=>r[2].isSidechain=true, 'agent attachment':(r:any[])=>r[2].agentId='child', 'duplicate root UUID':(r:any[])=>r.push({...r[1]}), 'conflicting UUID':(r:any[])=>r.push({...r[2],parentUuid:uuid(88)}), 'competing root':(r:any[])=>r.push(record(99,null,cwd,'user')), 'cycle':(r:any[])=>r[2].parentUuid=r[0].uuid, 'invalid root timestamp':(r:any[])=>r[1].timestamp='not-time', 'later unrelated root':(r:any[])=>{r[1].parentUuid=uuid(99);r.push(record(77,null,cwd,'user'));}, }))test('owned causal admission rejects '+name,()=>{ const r=delayedOriginRows();change(r);const got=readOwned(r); expect(got.events.some(e=>e.kind==='use'&&e.toolUseId==='read-owned')).toBe(false); expect(got.events.some(e=>e.kind==='message'&&e.text==='Phase 1 complete.')).toBe(false); }); test('a partial later parent remains pending until its complete native line exists',()=>{ const r=delayedOriginRows();const root=r.splice(1,1)[0];r.push(root); const pending=readOwned(r,true);expect(pending.transcript.status).toBe('missing');expect(pending.events).toEqual([]); expect(readOwned(r).events.some(e=>e.kind==='use'&&e.toolUseId==='read-owned')).toBe(true); }); test('default projection retains its original physical-order and exact-cwd behavior',()=>{ const r=delayedOriginRows();const got=read(r); expect(got.events.map(e=>e.toolUseId)).toEqual(['dispatch']); expect(autoplanPhaseCompletions(got.transcript,0)).toEqual([]); });