mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
* feat: add surface-aware exploratory QA and ship documentation gates * test: preserve delegated QA setup authority after main integration * fix(qa): clarify exploration order and preserve report artifacts * test(qa): follow the shared setup reference directly * refactor(ship): make verification and recovery routes explicit * test(ship): align evidence and review guards with explicit routes * fix(workflows): clarify ship recovery and functional QA evidence * fix(workflows): clarify approval recovery and full QA coverage * refactor(workflows): order review transactions and clarify ship state * fix(ship): clarify final verification and fail closed at publication * fix(evals): attribute native atomic documentation writes * fix(ship): clarify recovery and documentation lifecycle guidance * fix(test): preserve observed native placeholder styling in CI * fix(codex): report watchdog timeouts without a process-exit race * Checkpoint functional QA implementation and workflow validation repairs * Fix documentation and shared-review fixture contracts * docs: clarify judge reuse and evaluation supervision * test: align review evidence and selected case contracts * test: verify append-only documentation checkpoints and recovery * fix: qualify QA workflows and CI validation repairs * fix: launch shared-libs fixture scripts on Windows * fix: qualify QA deadlines, fixture isolation, and shard cleanup * fix: preserve qualified QA and cancellation repairs * fix: enforce functional fixture authority and share strict event decoding * fix: retain free-test evidence and explain recovery * fix: reject malformed native evidence after decoder consolidation * test: use reliable capture for telemetry privacy filters * test: refresh measured quick coverage and document validation costs * Fix native fixture receipts and preserve VM validation evidence * Align negative judge controls with upstream clarity policy * Fix report-only QA preparation and public evidence handling * Clarify QA-only preparation and current-report preservation * Stream Ship quality judgments with an explicit 64k response contract * Validate compact judge reasoning locally with supported wire schema * Align functional QA fixture instructions with evidence acceptance * Bind native browser diagnostics to execution evidence and align review verdicts * Preserve native diagnostic line boundaries * Serialize functional QA evidence from native captures * Keep large QA evidence fixture payload out of Windows argv
112 lines
11 KiB
TypeScript
112 lines
11 KiB
TypeScript
import {test,expect,afterEach} from 'bun:test';import fs from 'node:fs';import os from 'node:os';import path from 'node:path';
|
||
import fixture from './fixtures/autoplan-clipped-suffix-aq.json';
|
||
import {createAutoplanEditDigest,validAutoplanEditDigest,matchesAutoplanDigestRows} from './helpers/autoplan-artifact-digest';
|
||
import {createAutoplanArtifactRecorder,recordAutoplanArtifact,readPendingAutoplanArtifact,autoplanArtifactRecorderStatus} from './helpers/autoplan-artifact-recorder';
|
||
import {pendingAutoplanArtifactPermissionInput,autoplanArtifactMenuKey} from './helpers/autoplan-artifact-permission';
|
||
import {E2E_TOUCHFILES} from './helpers/touchfiles-data';
|
||
const cleanup:Array<()=>void>=[];afterEach(()=>{for(const f of cleanup.splice(0))f()});
|
||
function replay(before=fixture.before,removed=fixture.request.old_string,added=fixture.request.new_string,clock=Date.now){
|
||
const root=fs.mkdtempSync(path.join(os.tmpdir(),'ap-suffix-')),cwd=path.join(root,path.basename(fixture.cwd)),config=path.join(root,'config'),stateRoot=path.join(root,'home/.gstack');
|
||
const file=path.normalize(fixture.hook.pending.file.replace(fixture.stateRoot,stateRoot)),native=path.join(config,'projects/owned',fixture.hook.sessionId+'.jsonl');
|
||
fs.mkdirSync(cwd,{recursive:true});fs.mkdirSync(path.dirname(file),{recursive:true});fs.mkdirSync(path.dirname(native),{recursive:true});fs.writeFileSync(native,'');fs.writeFileSync(file,before);fs.utimesSync(file,new Date(0),new Date(0));
|
||
const recorder=createAutoplanArtifactRecorder(cwd,config,stateRoot);cleanup.push(()=>{recorder.dispose();fs.rmSync(root,{recursive:true,force:true})});
|
||
const event={hook_event_name:'PreToolUse',tool_name:'Edit',session_id:fixture.hook.sessionId,tool_use_id:fixture.hook.pending.toolUseId,cwd,transcript_path:native,tool_input:{file_path:file,old_string:removed,new_string:added}};
|
||
recordAutoplanArtifact(JSON.stringify(event),recorder.file,cwd,config,stateRoot);
|
||
const publicTools=structuredClone(fixture.publicTools) as any[];for(const e of publicTools)if(e.input)e.input.file_path=file;
|
||
const commandStartedAt=Date.parse(publicTools[0].timestamp)-1;
|
||
const pending=readPendingAutoplanArtifact(recorder.file,cwd,config,stateRoot,commandStartedAt,publicTools);
|
||
const observedAt=clock();
|
||
const context={cwd,ownedStateRoot:stateRoot,commandStartedAt,transcriptStatus:'ready',publicTools,pending,now:observedAt+1000,viewportCapturedAt:observedAt};
|
||
const invoke=(viewport=fixture.viewport,seen=new Set<string>())=>pendingAutoplanArtifactPermissionInput(viewport,context,seen);
|
||
return {root,cwd,config,stateRoot,file,recorder,event,context,invoke};
|
||
}
|
||
const menu=fixture.viewport.slice(fixture.viewport.indexOf('╌'));
|
||
const panel=(rows:string[])=>rows.join('\n')+'\n'+menu;
|
||
test('one clock sample preserves the suffix replay margin even across a longer scheduling gap',()=>{
|
||
let first:number|undefined,reads=0;
|
||
const clock=()=>(first??=Date.now())+1001*reads++;
|
||
const r=replay(undefined,undefined,undefined,clock);
|
||
expect(reads).toBe(1);
|
||
expect(r.context.now-r.context.viewportCapturedAt).toBe(1000);
|
||
expect(r.invoke()?.input).toBe('1\r');
|
||
const later=clock();
|
||
expect(later).toBe(r.context.now+1);
|
||
expect(pendingAutoplanArtifactPermissionInput(fixture.viewport,{...r.context,viewportCapturedAt:later},new Set())).toBeNull();
|
||
expect(pendingAutoplanArtifactPermissionInput(fixture.viewport,{...r.context,now:later,viewportCapturedAt:later},new Set())?.input).toBe('1\r');
|
||
});
|
||
|
||
test('exact current clipped pane requires new recorded suffix commitments and preserves original request bytes',()=>{
|
||
const r=replay(),digest=r.context.pending!.editDigest!;
|
||
expect(digest.beforeSHA256).toBe(fixture.provenance.beforeSHA256);expect(digest.requestSHA256).toBe(fixture.provenance.requestSHA256);
|
||
expect(digest.oldLineHashes).toEqual(fixture.hook.pending.editDigest.oldLineHashes);expect(digest.newLineHashes).toEqual(fixture.hook.pending.editDigest.newLineHashes);
|
||
expect(digest.clippedAdditions?.status).toBe('complete');expect(r.invoke()?.input).toBe('1\r');
|
||
delete digest.clippedAdditions;expect(r.invoke()).toBeNull();expect(r.invoke(fixture.viewport.split('\n').slice(1).join('\n'))?.input).toBe('1\r');
|
||
expect(fixture.provenance.actualCoverage).toContain('no phase credit');
|
||
});
|
||
test('first, middle and last changed lines support full120-column crops and following context',()=>{
|
||
const lines=Array.from({length:32},(_,i)=>'Line '+i+' '+String.fromCharCode(65+i%26).repeat(180));
|
||
const r=replay('Heading\nAnchor\nAfter one\nAfter two\n','Anchor',lines.join('\n'));
|
||
expect(r.context.pending!.editDigest!.clippedAdditions?.status).toBe('complete');
|
||
for(const i of [0,15,31]){
|
||
const row=i+2,tail=lines[i]!.slice(-114),next=i+1<lines.length?`${row+1} +${lines[i+1]}`:`${row+1} After one`,second=i+2<lines.length?`${row+2} +${lines[i+2]}`:`${row+2} ${i+1<lines.length?'After one':'After two'}`;
|
||
const column=String(row+1).length+2;
|
||
const viewport=panel([' '.repeat(column)+'+'+tail,' '+next,' '+second]);
|
||
expect(r.invoke(viewport)?.input).toBe('1\r');expect(r.invoke(viewport.replace(tail,'foreign'+tail))).toBeNull();
|
||
}
|
||
expect(fs.statSync(r.recorder.file).size).toBeLessThan(1024*1024);
|
||
});
|
||
test('exact suffix, corresponding line, next line and complete crop content are all mandatory',()=>{
|
||
for(const change of [
|
||
(s:string)=>s.replace(/^ \+t\./,' +x.'), (s:string)=>s.replace(/^ \+t\./,' +t!'),
|
||
(s:string)=>s.replace(/^ \+t\./,' +t.'),(s:string)=>s.replace(/^ \+t\./,' +t.'),
|
||
(s:string)=>s.replace(/^ \+t\./,' -t.'),(s:string)=>s.replace(/^ \+t\./,' Source: t.'),
|
||
// A forged deletion marker cannot make rejected digest rows use legacy authority.
|
||
(s:string)=>s.replace(/^ \+t\./,' -t.').replace(/^ 139 /m,' 140 '),
|
||
(s:string)=>s.replace(/^ \+t\./,' -t.').replace('Snapshot consistency','Foreign consistency'),
|
||
(s:string)=>s.replace(/^ 139 /m,' 140 '),(s:string)=>s.replace('Snapshot consistency','Foreign consistency'),
|
||
(s:string)=>s.replace('authoritative gate','unrequested gate'),(s:string)=>'> source\n'+s,
|
||
(s:string)=>s.replace('3. No','3. Maybe'),(s:string)=>s.replace('❯ 1. Yes','❯ 2. Yes'),
|
||
(s:string)=>s+'\nUnrelated menu',
|
||
]){const r=replay();expect(r.invoke(change(fixture.viewport))).toBeNull()}
|
||
});
|
||
test('wrong digest, file, current native history and previously seen menu remain denied',()=>{
|
||
for(const edit of [
|
||
(r:any)=>{r.context.pending.sessionId='foreign';},(r:any)=>{r.context.pending.editDigest.beforeSHA256='0'.repeat(64);},
|
||
(r:any)=>{r.context.pending.editDigest.clippedAdditions.lines[0].lineHash='0'.repeat(64);},
|
||
(r:any)=>{r.context.publicTools[1].isError=true;},(r:any)=>{r.context.publicTools=[];},
|
||
(r:any)=>{r.context.viewportCapturedAt=Date.parse(r.context.pending.timestamp)-1;},
|
||
(r:any)=>{fs.appendFileSync(r.file,'changed');fs.utimesSync(r.file,new Date(0),new Date(0));},
|
||
(r:any)=>{r.context.pending.file=r.file.replace('user-dashboard','foreign-dashboard');},
|
||
(r:any)=>{r.context.publicTools.push({kind:'use',name:'Edit',sessionId:r.context.pending.sessionId,toolUseId:'queued',timestamp:new Date().toISOString(),input:{file_path:r.file}});},
|
||
]){const r=replay();edit(r);expect(r.invoke()).toBeNull()}
|
||
const r=replay();expect(r.invoke(fixture.viewport,new Set([autoplanArtifactMenuKey(fixture.viewport)]))).toBeNull();expect(r.invoke(fixture.viewport,new Set([r.context.pending!.sessionId+':'+r.context.pending!.toolUseId]))).toBeNull();
|
||
});
|
||
test('suffix commitments are not body persistence and current replay cannot retain stale hashes',()=>{
|
||
const r=replay(),raw=fs.readFileSync(r.recorder.file,'utf8');for(const text of ['old_string','new_string','Preconditions heading','Snapshot consistency'])expect(raw).not.toContain(text);
|
||
recordAutoplanArtifact(JSON.stringify(r.event),r.recorder.file,r.cwd,r.config,r.stateRoot);expect(fs.readFileSync(r.recorder.file,'utf8')).toBe(raw);
|
||
r.event.tool_input.new_string+='changed';recordAutoplanArtifact(JSON.stringify(r.event),r.recorder.file,r.cwd,r.config,r.stateRoot);
|
||
expect(autoplanArtifactRecorderStatus(r.recorder.file,r.cwd,r.config,r.stateRoot)).toEqual({status:'invalid',reason:'conflicting_replay'});
|
||
});
|
||
test('legacy digest replay is harmless and partial-edge requests do not manufacture suffix authority',()=>{
|
||
const r=replay(),state=JSON.parse(fs.readFileSync(r.recorder.file,'utf8'));delete state.pending.editDigest.clippedAdditions;
|
||
fs.writeFileSync(r.recorder.file,JSON.stringify(state)+'\n');const raw=fs.readFileSync(r.recorder.file,'utf8');recordAutoplanArtifact(JSON.stringify(r.event),r.recorder.file,r.cwd,r.config,r.stateRoot);expect(fs.readFileSync(r.recorder.file,'utf8')).toBe(raw);
|
||
const q=replay('Prefix Anchor suffix\nAfter one\nAfter two\n','Anchor','New');expect(q.context.pending!.editDigest!.clippedAdditions).toBeUndefined();
|
||
});
|
||
test('malformed, sparse, tampered and excessive suffix records fail closed',()=>{
|
||
for(const edit of [
|
||
(c:any)=>{c.version=2;},(c:any)=>{c.extra=true;},(c:any)=>{c.startLine=0;},(c:any)=>{c.lines=Array(2);},
|
||
(c:any)=>{c.lines[0].suffixHashes=Array(2);},(c:any)=>{c.lines[0].suffixHashes=Array(257).fill('0'.repeat(64));},
|
||
(c:any)=>{c.lines[0].nextLineHash='0'.repeat(64);},(c:any)=>{c.lines[0].line++;},
|
||
]){const r=replay(),d=r.context.pending!.editDigest!;edit(d.clippedAdditions);expect(validAutoplanEditDigest(d)).toBe(false);expect(r.invoke()).toBeNull()}
|
||
const r=replay(),c=r.context.pending!.editDigest!.clippedAdditions;if(c?.status!=='complete')throw Error('missing');const target=c.lines.find(x=>x.line===138)!;target.suffixHashes[1]='0'.repeat(64);expect(r.invoke()).toBeNull();
|
||
});
|
||
test('overflow is explicit for every crop while complete-row legacy authority remains intact',()=>{
|
||
const lines=Array.from({length:40},(_,i)=>'Line '+i+' '+String.fromCharCode(65+i%26).repeat(300));const r=replay('Anchor\nAfter one\nAfter two\n','Anchor',lines.join('\n'));const d=r.context.pending!.editDigest!;
|
||
expect(d.clippedAdditions).toEqual({version:1,status:'overflow'});expect(validAutoplanEditDigest(d)).toBe(true);
|
||
for(const i of [0,20,39]){const n=i+1,next=i+1<lines.length?lines[i+1]:'After one',last=i+2<lines.length?lines[i+2]:'After two';expect(matchesAutoplanDigestRows([' '.repeat(String(n+1).length+2)+'+'+lines[i]!.slice(-114),` ${n+1} +${next}`,` ${n+2} +${last}`],Buffer.from('Anchor\nAfter one\nAfter two\n'),d)).toBe(false)}
|
||
expect(matchesAutoplanDigestRows([' 1 +'+lines[0],' 2 +'+lines[1]],Buffer.from('Anchor\nAfter one\nAfter two\n'),d)).toBe(true);
|
||
});
|
||
test('new regression files register only the actual Autoplan owner',()=>{
|
||
for(const p of ['test/autoplan-clipped-suffix-aq.test.ts','test/fixtures/autoplan-clipped-suffix-aq.json'])expect(Object.entries(E2E_TOUCHFILES).filter(([,files])=>files.includes(p)).map(([owner])=>owner)).toEqual(['autoplan-chain-pty']);
|
||
});
|