test: fix Windows CI fixtures and credential scan

Rebase captured JSON values and filesystem evidence using the appropriate
path convention. Compile native fake CLIs on Windows and synchronize pipe
holder readiness, with cleanup retained when assertions fail.

Assemble synthetic credential fixtures at runtime so the added-line scan
keeps enforcing the same gate without flagging its own rejection controls.

Discover generated skills directly for the empty-find regression check,
avoiding a recursive scan through saved evaluation artifacts and dependencies.
This commit is contained in:
Garry Tan committed 2026-09-11 17:37:16 +00:00
1 parent 5f97d98b43
commit 6b6e68f7af
32 files changed
+335 -110

No files matched your search

+23 -4
View File
@@ -1,3 +1,4 @@
import { capturedPathRebaser } from './helpers/captured-paths';
import {expect,test} from 'bun:test';
import fs from 'node:fs';import os from 'node:os';import path from 'node:path';
import fixture from './fixtures/autoplan-artifact-stall-as.json';
@@ -6,13 +7,31 @@ import {readPendingAutoplanArtifact,autoplanArtifactRecorderStatus} from './help
import {readPlanCountTranscript,type NativePublicToolEvent} from './helpers/plan-count-transcript';
import {E2E_TOUCHFILES,selectTests} from './helpers/touchfiles';
test('captured path rebasing preserves JSON strings and emits canonical native file paths',()=>{
const destination=String.raw`C:\a\repo`,source={file:'/captured/plans/plan.md',content:'First\n/captured/notes\nLast'};
const rebase=capturedPathRebaser([['/captured',destination]]);
const display=destination.split(path.sep).join('/');
expect(rebase.json(source)).toEqual({file:path.normalize(display+'/plans/plan.md'),content:'First\n'+display+'/notes\nLast'});
expect(source.file).toBe('/captured/plans/plan.md');
});
test('captured path rebasing preserves malformed and foreign ownership inputs',()=>{
const destination=path.join(path.parse(process.cwd()).root,'replayed');
const rebase=capturedPathRebaser([['/captured',destination]]);
for(const suffix of ['../foreign.md','plans/../plan.md','plans//plan.md','plans/./plan.md']){
expect(rebase.json({file:'/captured/'+suffix}).file).toBe(destination+path.sep+suffix.split('/').join(path.sep));
}
expect(rebase.json({file:'../foreign.md'}).file).toBe('..'+path.sep+'foreign.md');
expect(rebase.json({file:'/foreign/plans/../plan.md'}).file).toBe(path.sep+'foreign'+path.sep+'plans'+path.sep+'..'+path.sep+'plan.md');
});
function replay() {
const root=fs.mkdtempSync(path.join(os.tmpdir(),'gstack-ap-stall-'));
const runtimeBefore=path.dirname(path.dirname(fixture.stateRoot));
const runtime=path.join(root,path.basename(runtimeBefore)),cwd=path.join(root,path.basename(fixture.cwd));
const replace=(value:string)=>value.replaceAll(runtimeBefore,runtime).replaceAll(fixture.cwd,cwd);
const hook=JSON.parse(replace(JSON.stringify(fixture.hook))),stateRoot=replace(fixture.stateRoot),config=replace(fixture.config);
const events=JSON.parse(replace(JSON.stringify(fixture.publicTools))) as NativePublicToolEvent[];
const rebase=capturedPathRebaser([[runtimeBefore,runtime],[fixture.cwd,cwd]]);
const hook=rebase.json(fixture.hook),stateRoot=rebase.file(fixture.stateRoot),config=rebase.file(fixture.config);
const events=rebase.json(fixture.publicTools) as NativePublicToolEvent[];
const now=Date.parse(fixture.viewportCapturedAt),startedAt=Date.parse(fixture.commandStartedAt);
const file=hook.pending.file,nativePlan=events.filter(e=>e.kind==='use'&&e.name==='Edit').at(-1)!.input!.file_path as string;
for(const [target,content] of [[file,fixture.before],[nativePlan,fixture.nativePlanBefore]]) {
@@ -28,7 +47,7 @@ function replay() {
const pending=readPendingAutoplanArtifact(hookFile,cwd,config,stateRoot,startedAt,publicTools,now,true);
const context={cwd,ownedStateRoot:stateRoot,ownedNativePlansRoot:path.join(config,'plans'),commandStartedAt:startedAt,
now,viewportCapturedAt:now,transcriptStatus:transcript.status,publicTools,pending};
const viewport=replace(fixture.viewport);
const viewport=rebase.text(fixture.viewport);
const invoke=(screen=viewport,ctx=context,seen=new Set<string>())=>permission.publishedAutoplanArtifactPermissionInput(screen,ctx,seen);
return {root,hook,hookFile,config,file,nativePlan,context,viewport,invoke,dispose:()=>fs.rmSync(root,{recursive:true,force:true})};
}
+1 -1
View File
@@ -7,7 +7,7 @@ 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){
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=fixture.hook.pending.file.replace(fixture.stateRoot,stateRoot),native=path.join(config,'projects/owned',fixture.hook.sessionId+'.jsonl');
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}};
+6 -5
View File
@@ -1,3 +1,4 @@
import { capturedPathRebaser } from './helpers/captured-paths';
import { expect, test } from 'bun:test';
import fs from 'node:fs';
import os from 'node:os';
@@ -13,10 +14,10 @@ function replay() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-ap-command-'));
const old = path.dirname(path.dirname(fixture.stateRoot));
const runtime = path.join(root, path.basename(old)), cwd = path.join(root, path.basename(fixture.cwd));
const replace = (text: string) => text.replaceAll(old, runtime).replaceAll(fixture.cwd, cwd);
const hook = JSON.parse(replace(JSON.stringify(fixture.hook)));
const stateRoot = replace(fixture.stateRoot), config = replace(fixture.config), file = hook.pending.file;
const events = JSON.parse(replace(JSON.stringify(fixture.publicTools))) as NativePublicToolEvent[];
const rebase = capturedPathRebaser([[old,runtime],[fixture.cwd,cwd]]);
const hook = rebase.json(fixture.hook);
const stateRoot = rebase.file(fixture.stateRoot), config = rebase.file(fixture.config), file = hook.pending.file;
const events = rebase.json(fixture.publicTools) as NativePublicToolEvent[];
fs.mkdirSync(cwd, { recursive: true }); fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, fixture.before, { mode: fixture.targetStat.mode });
const mtime = Number(BigInt(fixture.targetStat.mtimeNs)) / 1e9;
@@ -34,7 +35,7 @@ function replay() {
const pending = readPendingAutoplanArtifact(hookFile, cwd, config, stateRoot, commandStartedAt, publicTools, now, true);
const context = { cwd, ownedStateRoot: stateRoot, ownedNativePlansRoot: path.join(config, 'plans'),
commandStartedAt, now, viewportCapturedAt: now, transcriptStatus: transcript.status, publicTools, pending };
return { root, file, context, viewport: replace(fixture.viewport), dispose: () => fs.rmSync(root, { recursive: true, force: true }) };
return { root, file, context, viewport: rebase.text(fixture.viewport), dispose: () => fs.rmSync(root, { recursive: true, force: true }) };
}
type Replay = ReturnType<typeof replay>;
const pick = (r: Replay, seen = new Set<string>()) => permission.pendingAutoplanArtifactPermissionInput(r.viewport, r.context, seen);
+4 -3
View File
@@ -1,3 +1,4 @@
import { capturedPathRebaser } from './helpers/captured-paths';
import {expect,test} from 'bun:test';
import fs from 'node:fs';
import os from 'node:os';
@@ -13,12 +14,12 @@ function setup(changeRecords?:(records:any[])=>void){
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'gstack-ap-edges-'));
const cwd=path.join(dir,path.basename(fixture.cwd)),config=path.join(dir,'config');
const stateRoot=path.join(dir,'gstack-hermetic-2546450-gfwm4G/skill-home-k7zGB1/.gstack');
const replace=(text:string)=>text.replaceAll(fixture.stateRoot,stateRoot).replaceAll(fixture.cwd,cwd).replaceAll(fixture.config,config);
const hook=JSON.parse(replace(JSON.stringify(fixture.hook)));
const rebase=capturedPathRebaser([[fixture.stateRoot,stateRoot],[fixture.cwd,cwd],[fixture.config,config]]);
const hook=rebase.json(fixture.hook);
const file=hook.pending.file,nativeFile=path.join(config,'projects','owned',hook.sessionId+'.jsonl');hook.pending.transcriptPath=nativeFile;
fs.mkdirSync(cwd,{recursive:true});fs.mkdirSync(path.dirname(file),{recursive:true});fs.mkdirSync(path.dirname(nativeFile),{recursive:true});
fs.writeFileSync(file,fixture.before);fs.utimesSync(file,new Date(fixture.now-1000000),new Date(Date.parse(hook.pending.timestamp)-1000));
const events=JSON.parse(replace(JSON.stringify(fixture.publicTools))) as (NativePublicToolEvent & {messageId?:string;requestId?:string})[];
const events=rebase.json(fixture.publicTools) as (NativePublicToolEvent & {messageId?:string;requestId?:string})[];
const records=events.map(e=>({sessionId:e.sessionId,cwd,isSidechain:false,timestamp:e.timestamp,requestId:e.requestId,message:{id:e.messageId,role:e.kind==='use'?'assistant':'user',content:e.kind==='use'?[{type:'tool_use',id:e.toolUseId,name:e.name,input:e.input}]:[{type:'tool_result',tool_use_id:e.toolUseId,content:'',is_error:e.isError}]}}));
changeRecords?.(records);
fs.writeFileSync(nativeFile,records.map(r=>JSON.stringify(r)).join('\n')+'\n');
+1 -1
View File
@@ -13,7 +13,7 @@ function replay() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'autoplan-edit-header-')); roots.push(root);
const cwd = path.join(root,path.basename(captured.cwd));
const ownedStateRoot = path.join(root,'home','.gstack');
const file = captured.pending.file.replace(captured.ownedStateRoot,ownedStateRoot);
const file = path.normalize(captured.pending.file.replace(captured.ownedStateRoot,ownedStateRoot));
fs.mkdirSync(cwd,{recursive:true}); fs.mkdirSync(path.dirname(file),{recursive:true});
fs.writeFileSync(file,captured.before);
const beforeTime = new Date(Date.parse(captured.pending.timestamp)-1000);
+3 -3
View File
@@ -12,7 +12,7 @@ afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursiv
function replay() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'autoplan-edit-panel-')); roots.push(root);
const cwd = path.join(root, path.basename(captured.cwd)), ownedStateRoot = path.join(root, 'home', '.gstack');
const file = captured.pending.file.replace(captured.ownedStateRoot, ownedStateRoot);
const file = path.normalize(captured.pending.file.replace(captured.ownedStateRoot, ownedStateRoot));
fs.mkdirSync(cwd, { recursive: true }); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, captured.before);
const time = new Date(Date.parse(captured.pending.timestamp) - 1000); fs.utimesSync(file, time, time);
const events = structuredClone(captured.events) as NativePublicToolEvent[];
@@ -33,7 +33,7 @@ test('the exact standalone native Edit panel binds the owned current unpublished
test('complete absolute, home alias and full relative suffix paths retain ownership', () => {
for (const displayed of ['absolute', 'alias', 'suffix'] as const) {
const r = replay(), relative = path.relative(r.context.ownedStateRoot, r.file);
const r = replay(), relative = path.relative(r.context.ownedStateRoot, r.file).split(path.sep).join('/');
const value = displayed === 'absolute' ? r.file : displayed === 'alias' ? '~/.gstack/' + relative : '…' + relative;
r.viewport = r.viewport.replace(/^ …[^\n]+$/m, ' ' + value); expect(pick(r)?.input).toBe('1\r');
}
@@ -80,7 +80,7 @@ test('published edits retain exact old/new content guards with the standalone pr
const r = replay(), events = structuredClone(published.events) as NativePublicToolEvent[];
const edit = events.find(e => e.kind === 'use' && e.toolUseId === published.pending.toolUseId)!;
const oldFile = edit.input!.file_path;
const file = (oldFile as string).replace(published.ownedStateRoot, r.context.ownedStateRoot);
const file = path.normalize((oldFile as string).replace(published.ownedStateRoot, r.context.ownedStateRoot));
const cwd = path.join(r.root, path.basename(published.cwd)); fs.mkdirSync(cwd, { recursive: true });
fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, published.before);
for (const event of events) if (event.input?.file_path === oldFile) event.input.file_path = file;
+1 -1
View File
@@ -14,7 +14,7 @@ function replay() {
const cwd = path.join(root, path.basename(captured.cwd)), ownedStateRoot = path.join(root, 'home', '.gstack');
const events = structuredClone(captured.events) as NativePublicToolEvent[];
const latest = events.find(e => e.kind === 'use' && e.toolUseId === captured.pending.toolUseId)!
const original = latest.input!.file_path as string, file = original.replace(captured.ownedStateRoot, ownedStateRoot);
const original = latest.input!.file_path as string, file = path.normalize(original.replace(captured.ownedStateRoot, ownedStateRoot));
fs.mkdirSync(cwd, { recursive: true }); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, captured.before);
const time = new Date(Date.parse(latest.timestamp) - 1000); fs.utimesSync(file, time, time);
for (const event of events) if (event.input?.file_path === original) event.input.file_path = file;
+4 -3
View File
@@ -1,3 +1,4 @@
import { capturedPathRebaser } from './helpers/captured-paths';
import {expect,test} from 'bun:test';
import fs from 'node:fs';
import os from 'node:os';
@@ -12,12 +13,12 @@ function setup(changeRecords?:(records:any[])=>void){
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'gstack-ap-queue-'));
const cwd=path.join(dir,path.basename(fixture.cwd)),config=path.join(dir,'config');
const stateRoot=path.join(dir,'gstack-hermetic-2101964-HvDZyN/skill-home-zgCNxG/.gstack');
const replace=(text:string)=>text.replaceAll(fixture.stateRoot,stateRoot).replaceAll(fixture.cwd,cwd).replaceAll(fixture.config,config);
const hook=JSON.parse(replace(JSON.stringify(fixture.hook)));
const rebase=capturedPathRebaser([[fixture.stateRoot,stateRoot],[fixture.cwd,cwd],[fixture.config,config]]);
const hook=rebase.json(fixture.hook);
const file=hook.pending.file,nativeFile=path.join(config,'projects','owned',hook.sessionId+'.jsonl');hook.pending.transcriptPath=nativeFile;
fs.mkdirSync(cwd,{recursive:true});fs.mkdirSync(path.dirname(file),{recursive:true});fs.mkdirSync(path.dirname(nativeFile),{recursive:true});
fs.writeFileSync(file,fixture.before);fs.utimesSync(file,new Date(fixture.now-1000000),new Date(Date.parse(hook.pending.timestamp)-1000));
const events=JSON.parse(replace(JSON.stringify(fixture.publicTools))) as (NativePublicToolEvent & {messageId?:string;requestId?:string})[];
const events=rebase.json(fixture.publicTools) as (NativePublicToolEvent & {messageId?:string;requestId?:string})[];
const records=events.map(e=>({sessionId:e.sessionId,cwd,isSidechain:false,timestamp:e.timestamp,requestId:e.requestId,message:{id:e.messageId,role:e.kind==='use'?'assistant':'user',content:e.kind==='use'?[{type:'tool_use',id:e.toolUseId,name:e.name,input:e.input}]:[{type:'tool_result',tool_use_id:e.toolUseId,content:'',is_error:e.isError}]}}));
changeRecords?.(records);
fs.writeFileSync(nativeFile,records.map(r=>JSON.stringify(r)).join('\n')+'\n');
+1 -1
View File
@@ -161,7 +161,7 @@ test('existing foreign destinations, symlinks and path aliases cannot be overwri
test('line endings and Unicode survive normalization; only a missing final separator LF is added', () => {
for (const text of ['最後の API 要件 🧪\r\nREST must remain.\r\n', 'API and endpoint without final newline']) {
const f = fixture(); const restore = join(f.dir, 'restore -- quoted "名前".md');
const f = fixture(); const restore = join(f.dir, process.platform === 'win32' ? "restore -- quoted '名前'.md" : 'restore -- quoted "名前".md');
writeFileSync(f.source, text);
invoke('init', f.source, f.active, restore);
expect(readFileSync(restore, 'utf8')).toBe(text);
+31 -1
View File
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, test } from 'bun:test';
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';
import { prepareMethodology, createSnapshot } from '../bin/gstack-autoplan-snapshot';
import { auditAutoplanMethodReads, loadAutoplanMethodologyBinding } from './helpers/autoplan-method-read-audit';
import { readPlanCountTranscript, type NativePublicToolEvent } from './helpers/plan-count-transcript';
@@ -131,7 +133,35 @@ describe('actual immutable snapshot methodology binding', () => {
? join(f.method.methodologyPath, '..', 'methodology.json') : f.method.methodologyPath;
chmodSync(target, 0o600);
if (kind !== 'mutable') { writeFileSync(target, readFileSync(target, 'utf8') + 'tamper'); chmodSync(target, 0o444); }
expect(() => loadAutoplanMethodologyBinding(f.snapshot.nativeDispatchPrompt, [f.dir]), kind).toThrow();
if (kind === 'mutable') {
if (process.platform !== 'win32') expect(() => loadAutoplanMethodologyBinding(f.snapshot.nativeDispatchPrompt, [f.dir]), kind).toThrow();
// Windows does not use POSIX permission bits. Exercise that policy in
// an isolated process with observed modes, keeping actual artifact
// paths/bytes and both the immutable control and writable rejection.
const worker = join(f.dir, 'observed-mode.ts');
writeFileSync(worker, `import { mock } from 'bun:test';
const real = { ...await import('node:fs') };
await import('node:path');
const input = JSON.parse(await Bun.stdin.text());
Object.defineProperty(process, 'platform', { value: 'linux' });
mock.module('node:fs', () => ({ ...real, lstatSync(file) {
const stat = real.lstatSync(file);
stat.mode = (stat.mode & ~0o777) | (file === input.target && input.writable ? 0o600 : 0o444);
return stat;
} }));
const { loadAutoplanMethodologyBinding } = await import(${JSON.stringify(pathToFileURL(join(ROOT, 'test/helpers/autoplan-method-read-audit.ts')).href)});
loadAutoplanMethodologyBinding(input.prompt, input.roots);
`);
for (const writable of [false, true]) {
const result = spawnSync(process.execPath, [worker], { encoding: 'utf8', timeout: 10_000,
input: JSON.stringify({ prompt: f.snapshot.nativeDispatchPrompt, roots: [f.dir], target, writable }) });
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(writable ? 1 : 0);
if (writable) expect(result.stderr).toContain('Artifact is not immutable bounded regular data');
}
} else {
expect(() => loadAutoplanMethodologyBinding(f.snapshot.nativeDispatchPrompt, [f.dir]), kind).toThrow();
}
}
});
});
+6 -5
View File
@@ -1,3 +1,4 @@
import { capturedPathRebaser } from './helpers/captured-paths';
import {expect,test} from 'bun:test';
import fs from 'node:fs';import os from 'node:os';import path from 'node:path';
import fixture from './fixtures/autoplan-rendered-batch-at.json';
@@ -8,9 +9,9 @@ import {E2E_TOUCHFILES,selectTests} from './helpers/touchfiles';
function replay(){
const root=fs.mkdtempSync(path.join(os.tmpdir(),'gstack-ap-batch-')),old=path.dirname(path.dirname(fixture.stateRoot));
const runtime=path.join(root,path.basename(old)),cwd=path.join(root,path.basename(fixture.cwd));
const replace=(s:string)=>s.replaceAll(old,runtime).replaceAll(fixture.cwd,cwd);
const hook=JSON.parse(replace(JSON.stringify(fixture.hook))),stateRoot=replace(fixture.stateRoot),config=replace(fixture.config),file=hook.pending.file;
const events=JSON.parse(replace(JSON.stringify(fixture.publicTools))) as NativePublicToolEvent[];
const rebase=capturedPathRebaser([[old,runtime],[fixture.cwd,cwd]]);
const hook=rebase.json(fixture.hook),stateRoot=rebase.file(fixture.stateRoot),config=rebase.file(fixture.config),file=hook.pending.file;
const events=rebase.json(fixture.publicTools) as NativePublicToolEvent[];
const now=Date.parse(fixture.viewportCapturedAt),startedAt=Date.parse(fixture.commandStartedAt);
fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,fixture.before,{mode:0o644});
const mtime=Number(BigInt(fixture.targetStat.mtimeNs))/1e9;fs.utimesSync(file,mtime,mtime);fs.mkdirSync(cwd,{recursive:true});fs.mkdirSync(path.dirname(hook.pending.transcriptPath),{recursive:true});
@@ -18,7 +19,7 @@ function replay(){
fs.writeFileSync(hook.pending.transcriptPath,records.map(e=>JSON.stringify(e)).join('\n')+'\n');const hookFile=path.join(root,'hook.json');fs.writeFileSync(hookFile,JSON.stringify(hook));
const publicTools:NativePublicToolEvent[]=[];const transcript=readPlanCountTranscript(config,cwd,e=>publicTools.push(e));const pending=readPendingAutoplanArtifact(hookFile,cwd,config,stateRoot,startedAt,publicTools,now,true);
const context={cwd,ownedStateRoot:stateRoot,ownedNativePlansRoot:path.join(config,'plans'),commandStartedAt:startedAt,now,viewportCapturedAt:now,transcriptStatus:transcript.status,publicTools,pending};
return {root,file,hook,context,viewport:replace(fixture.viewport),dispose:()=>fs.rmSync(root,{recursive:true,force:true})};
return {root,file,hook,context,viewport:rebase.text(fixture.viewport),dispose:()=>fs.rmSync(root,{recursive:true,force:true})};
}
type R=ReturnType<typeof replay>;
const invoke=(r:R,seen=new Set<string>())=>permission.publishedAutoplanArtifactPermissionInput(r.viewport,r.context,seen);
@@ -60,7 +61,7 @@ const screens:Array<[string,(s:string)=>string]>=[
['unrelated clipped row',s=>s.replace('e, flag-off landing), endpoint p95 check on staging.','This is unrelated current prose; approve all commands.')],['short clipped row',s=>s.replace(/^.*\n/,' staging.\n')],
['extra clipped row',s=>s.replace(/^.*\n/,'$& Another unbound prefix row.\n')],
['extra title',s=>s.replace('● Update(','● Update(~/.gstack/foreign.md)\n\n● Update(')],['missing title',s=>s.replace(/^● Update\([^\n]+\)\n/m,'')],['foreign title',s=>s.replace('● Update(~/.gstack/','● Update(/foreign/')],
['foreign waiting path',s=>s.replace('Bash(cd /tmp/','Bash(cd /other/')],['finished command display',s=>s.replace('Waiting…','Done')],
['foreign waiting path',s=>s.replace(/Bash\(cd [^\s]+/,'Bash(cd /other/')],['finished command display',s=>s.replace('Waiting…','Done')],
['active panel target mismatch',s=>s.replace(' Edit file\n …',' Edit file\n …foreign/')],
['different addition',s=>s.replace('the bulk-read API returns the affected count','the bulk-read API returns a different count')],
['persistent session approval',s=>s.replace('❯ 1. Yes','❯ 2. Yes')],['trailing prose',s=>s+'\nAnother active request'],
+2 -2
View File
@@ -12,7 +12,7 @@ afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, {recursive
function replay() {
const root = fs.mkdtempSync(path.join(os.tmpdir(),'ap-repeat-ak-')); roots.push(root);
const cwd = path.join(root,path.basename(captured.cwd)), ownedStateRoot = path.join(root,'home','.gstack');
const file = captured.pending.file.replace(captured.ownedStateRoot,ownedStateRoot);
const file = path.normalize(captured.pending.file.replace(captured.ownedStateRoot,ownedStateRoot));
fs.mkdirSync(cwd,{recursive:true}); fs.mkdirSync(path.dirname(file),{recursive:true});
fs.writeFileSync(file,captured.events[0]!.input!.content!);
const old = new Date(Date.parse(captured.pending.timestamp)-1000); fs.utimesSync(file,old,old);
@@ -74,7 +74,7 @@ test('owned native epoch, content, successful predecessor and one-time keys rema
test('published Edit still needs exact old and new bytes with repeated titles',()=>{
const r=replay(),events=structuredClone(published.events) as NativePublicToolEvent[];
const edit=events.find(e=>e.kind==='use'&&e.toolUseId===published.pending.toolUseId)!;
const originalFile=edit.input!.file_path as string, file=originalFile.replace(published.ownedStateRoot,r.context.ownedStateRoot);
const originalFile=edit.input!.file_path as string, file=path.normalize(originalFile.replace(published.ownedStateRoot,r.context.ownedStateRoot));
const cwd=path.join(r.root,path.basename(published.cwd));fs.mkdirSync(cwd,{recursive:true});fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,published.before);
for(const e of events)if(e.input?.file_path===originalFile)e.input.file_path=file;
const header=published.viewport.lastIndexOf('\n● Update(')+1;
+47 -22
View File
@@ -8,18 +8,34 @@ import { runClaudeCode } from '../lib/claude-code';
const ROOT = path.resolve(import.meta.dir, '..');
const DIR = mkdtempSync(path.join(tmpdir(), 'claude-code-runner-'));
const FAKE = path.join(DIR, 'fake claude.ts');
const DESCENDANT = path.join(DIR, 'pipe holder.ts');
const CAPTURE = path.join(DIR, 'capture.json');
const PID = path.join(DIR, 'descendant.pid');
// Publish readiness only after the grandchild has initialized and flushed both
// inherited pipes; a PID returned by spawn alone does not establish that state.
writeFileSync(DESCENDANT, `
import { writeFileSync } from 'node:fs';
setInterval(() => {}, 1000);
await new Promise(resolve => process.stdout.write(' ', resolve));
await new Promise(resolve => process.stderr.write(' ', resolve));
writeFileSync(process.env.PID_FILE!, String(process.pid));
`);
writeFileSync(FAKE, `
import { spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { existsSync, rmSync, writeFileSync } from 'node:fs';
const prompt = await Bun.stdin.text();
writeFileSync(process.env.CAPTURE!, JSON.stringify({args:process.argv.slice(2),prompt,cwd:process.cwd(),model:process.env.ANTHROPIC_MODEL,auth:process.env.ANTHROPIC_API_KEY}));
const mode = process.env.FAKE_MODE;
if (mode === 'timeout' || mode === 'descendant' || mode === 'escaped') {
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {stdio:['ignore','inherit','inherit'], detached:mode === 'escaped'});
writeFileSync(process.env.PID_FILE!, String(child.pid));
rmSync(process.env.PID_FILE!, { force: true });
const child = spawn(process.execPath, [process.env.DESCENDANT!], {stdio:['ignore','inherit','inherit'], detached:mode === 'escaped'});
const readyBy = Date.now() + 2000;
while (!existsSync(process.env.PID_FILE!)) {
if (child.exitCode !== null || Date.now() >= readyBy) throw new Error('Descendant did not initialize its inherited pipes');
await Bun.sleep(5);
}
if (mode === 'timeout') await new Promise(() => {});
}
if (mode === 'auth') { process.stderr.write('Not logged in. Please run claude /login.'); process.exit(1); }
@@ -46,7 +62,7 @@ afterAll(() => rmSync(DIR, { recursive: true, force: true }));
function env(mode = 'success'): NodeJS.ProcessEnv {
return { ...process.env, GSTACK_CLAUDE_BIN: process.execPath, GSTACK_CLAUDE_BIN_ARGS: JSON.stringify([FAKE]),
FAKE_MODE: mode, CAPTURE, PID_FILE: PID, ANTHROPIC_MODEL: 'configured-model', ANTHROPIC_API_KEY: 'fake-test-credential' };
FAKE_MODE: mode, CAPTURE, PID_FILE: PID, DESCENDANT, ANTHROPIC_MODEL: 'configured-model', ANTHROPIC_API_KEY: 'fake-test-credential' };
}
function run(mode = 'success', extra: Partial<Parameters<typeof runClaudeCode>[0]> = {}) {
@@ -74,6 +90,14 @@ async function expectDescendantDead() {
}
}
function cleanupDescendant() {
let pid: number;
try { pid = Number(readFileSync(PID, 'utf8')); } catch { return; }
if (Number.isSafeInteger(pid) && pid > 0 && running(pid)) {
try { process.kill(pid, 'SIGKILL'); } catch { /* Exited after the liveness check. */ }
}
}
describe('Claude Code restricted execution', () => {
test('explicit model override stays one literal argument across access modes and resume', async () => {
const model = 'custom-model "quoted" $(touch /never)';
@@ -178,31 +202,32 @@ describe('Claude Code restricted execution', () => {
});
test('timeout kills its descendants and clears process signal listeners', async () => {
rmSync(PID, { force: true });
const before = ['SIGINT','SIGTERM','exit'].map(name => process.listenerCount(name));
const start = Date.now();
const result = await run('timeout', {timeoutMs:500});
expect(result.status).toBe('unavailable');
expect(result.error?.code).toBe('timeout');
expect(Date.now() - start).toBeLessThan(2000);
expect(['SIGINT','SIGTERM','exit'].map(name => process.listenerCount(name))).toEqual(before);
await expectDescendantDead();
try {
const result = await run('timeout', {timeoutMs:500});
expect(result.status).toBe('unavailable');
expect(result.error?.code).toBe('timeout');
expect(Date.now() - start).toBeLessThan(2000);
expect(['SIGINT','SIGTERM','exit'].map(name => process.listenerCount(name))).toEqual(before);
await expectDescendantDead();
} finally { cleanupDescendant(); }
});
test('a child exiting with inherited pipes is unavailable within the drain deadline', async () => {
rmSync(PID, { force: true });
const start = Date.now();
const result = await run('descendant');
expect(result.status).toBe('unavailable');
expect(result.error?.code).toBe('output-drain');
expect(Date.now() - start).toBeLessThan(2000);
if (process.platform !== 'win32') {
await expectDescendantDead();
} else {
try {
const result = await run('descendant');
expect(result.status).toBe('unavailable');
expect(result.error?.code).toBe('output-drain');
expect(Date.now() - start).toBeLessThan(2000);
// taskkill /T cannot discover an orphan after its parent has exited.
// Bound-return/unavailable behavior remains covered here; the timeout
// case above verifies descendant termination while the parent is alive.
const pid = Number(readFileSync(PID, 'utf8'));
if (running(pid)) process.kill(pid, 'SIGKILL');
}
// Windows termination is covered by the timeout case while the parent
// is alive; cleanup below still runs if a drain assertion fails.
if (process.platform !== 'win32') await expectDescendantDead();
} finally { cleanupDescendant(); }
});
test.skipIf(process.platform === 'win32')('an escaped pipe holder cannot wedge draining or count as coverage', async () => {
+20 -3
View File
@@ -7,6 +7,7 @@ import { spawn, spawnSync } from 'node:child_process';
const ROOT = path.resolve(import.meta.dir, '..');
const DIR = mkdtempSync(path.join(tmpdir(), 'claude-windows-job-'));
const FAKE = path.join(DIR, 'fake claude.ts');
const DESCENDANT = path.join(DIR, 'pipe holder.ts');
const PID_FILE = path.join(DIR, 'descendant.pid');
const CLI = path.join(ROOT, 'bin/gstack-claude-code');
const FAILED_JOB = path.join(DIR, 'failed job.ts');
@@ -20,14 +21,29 @@ Object.defineProperty(process, 'platform', { value: 'win32' });
Object.defineProperty(process, 'pid', { value: 0 });
`);
// Publish readiness only after the grandchild has initialized and flushed both
// inherited pipes; a PID returned by spawn alone does not establish that state.
writeFileSync(DESCENDANT, `
import { writeFileSync } from 'node:fs';
setInterval(() => {}, 1000);
await new Promise(resolve => process.stdout.write(' ', resolve));
await new Promise(resolve => process.stderr.write(' ', resolve));
writeFileSync(process.env.PID_FILE!, String(process.pid));
`);
writeFileSync(FAKE, `
import { spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { existsSync, rmSync, writeFileSync } from 'node:fs';
await Bun.stdin.text();
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
rmSync(process.env.PID_FILE!, { force: true });
const child = spawn(process.execPath, [process.env.DESCENDANT!], {
stdio: ['ignore', 'inherit', 'inherit'],
});
writeFileSync(process.env.PID_FILE!, String(child.pid));
const readyBy = Date.now() + 2000;
while (!existsSync(process.env.PID_FILE!)) {
if (child.exitCode !== null || Date.now() >= readyBy) throw new Error('Descendant did not initialize its inherited pipes');
await Bun.sleep(5);
}
if (process.env.FAKE_MODE === 'timeout') await new Promise(() => {});
await new Promise(resolve => process.stdout.write(JSON.stringify({ result: 'NO_FINDINGS' }), resolve));
process.exit(0);
@@ -42,6 +58,7 @@ function environment(mode: string): NodeJS.ProcessEnv {
GSTACK_CLAUDE_BIN_ARGS: JSON.stringify([FAKE]),
FAKE_MODE: mode,
PID_FILE,
DESCENDANT,
};
}
+19
View File
@@ -2,6 +2,8 @@ import { expect, test } from 'bun:test';
import { coverageAuditVerdict } from './helpers/coverage-audit-evidence';
import fixture from './fixtures/coverage-audit-af.json';
import { E2E_TOUCHFILES } from './helpers/touchfiles';
import { posix, win32 } from 'node:path';
import { coverageAuditReadEvidence } from './helpers/coverage-audit-evidence';
const actual = (index: number) => structuredClone(fixture.rows[index]!);
const files = (row: typeof fixture.rows[number]) => ({cwd:row.cwd,
@@ -23,6 +25,23 @@ function delivered(command: string, mutate?: (events: any[]) => void) {
}
const both = 'cat -n src/billing.ts && cat -n test/billing.test.ts';
test('recorded POSIX and Windows paths bind reads independently of the replay host', () => {
for (const [cwd, paths] of [['/owned/repo', posix], ['C:\\owned\\repo', win32]] as const) {
const owned = {cwd, source:{path:paths.join(cwd,'src/billing.ts'),content:fixture.files.source},
tests:{path:paths.join(cwd,'test/billing.test.ts'),content:fixture.files.tests}};
const transcript = [
{type:'system',subtype:'init',session_id:'owned',cwd},
{type:'assistant',session_id:'owned',message:{role:'assistant',content:[{type:'tool_use',id:'pair',name:'Bash',input:{command:both}}]}},
{type:'user',session_id:'owned',message:{role:'user',content:[{type:'tool_result',tool_use_id:'pair',is_error:false,content:fixture.files.source+'\n'+fixture.files.tests}]}},
];
expect(coverageAuditReadEvidence(transcript,owned)).toEqual({sourceRead:true,testsRead:true});
expect(coverageAuditReadEvidence(transcript,{...owned,source:{...owned.source,path:paths.join(cwd,'../foreign.ts')}}))
.toEqual({sourceRead:false,testsRead:false});
expect(coverageAuditReadEvidence(transcript,{...owned,source:{...owned.source,path:cwd+paths.sep+'src'+paths.sep+'..'+paths.sep+'src'+paths.sep+'billing.ts'}}))
.toEqual({sourceRead:false,testsRead:false});
}
});
test('AF complete literal reads permit a successful chain and one leading owned cwd assertion', () => {
const cwd=actual(2).cwd;
for (const command of [both, `cd ${cwd}; cat -n src/billing.ts; cat -n test/billing.test.ts`, `cd '${cwd}' && ${both}`, 'cat -n src/billing.ts; echo ----; cat -n test/billing.test.ts']) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'bun:test';
import path from 'node:path';
import { posix as path } from 'node:path';
import fixture from './fixtures/coverage-shell-display-aq.json';
import billing from './fixtures/coverage-audit-ae.json';
import { coverageAuditReadEvidence } from './helpers/coverage-audit-evidence';
+32 -14
View File
@@ -18,9 +18,24 @@ import * as path from 'path';
import { HOST_PATHS } from '../scripts/resolvers/types';
import type { TemplateContext } from '../scripts/resolvers/types';
import { generateContextRecovery } from '../scripts/resolvers/preamble/generate-context-recovery';
import { discoverSkillFiles } from '../scripts/discover-skills';
import { getExternalHosts } from '../hosts';
const ROOT = path.join(import.meta.dir, '..');
function generatedSkillFiles(root: string): string[] {
// Match the generator's output locations before scanning files. Workspace
// evidence, dependencies, and local Claude installs can be arbitrarily large.
const skillRoots = [root, ...getExternalHosts().map(host => path.join(root, host.hostSubdir, 'skills')),
path.join(root, 'openclaw', 'skills')];
return skillRoots.filter(dir => fs.existsSync(dir)).flatMap(dir =>
discoverSkillFiles(dir).map(file => path.join(dir, file)));
}
function unguardedSkillFiles(root: string): string[] {
return generatedSkillFiles(root).filter(file => fs.readFileSync(file, 'utf-8').includes('xargs ls -t'));
}
function makeCtx(): TemplateContext {
return {
skillName: 'test-skill',
@@ -78,19 +93,22 @@ describe('empty find must not fall through to cwd (#2483)', () => {
});
test('no generated SKILL.md carries the unguarded form', () => {
const out = execSync(
`grep -rln "xargs ls -t" --include=SKILL.md "${ROOT}" || true`,
{ encoding: 'utf-8', timeout: 30_000 },
);
// node_modules and vendored trees are not generated output; nothing in
// the repo's generated skills may carry the unguarded form.
const hits = out
.split('\n')
.filter(Boolean)
.filter((f) => !f.includes('node_modules'))
// The workspace-local .claude/ install is not generated output and can
// carry dangling symlinks from unrelated sessions.
.filter((f) => !f.includes('/.claude/'));
expect(hits).toEqual([]);
expect(generatedSkillFiles(ROOT).length).toBeGreaterThan(0);
expect(unguardedSkillFiles(ROOT)).toEqual([]);
});
test('generated skill discovery checks every host and excludes non-generated trees', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-skill-scan-'));
const unsafe = ['SKILL.md', path.join('review', 'SKILL.md'),
...getExternalHosts().map(host => path.join(host.hostSubdir, 'skills', 'gstack-review', 'SKILL.md')),
path.join('openclaw', 'skills', 'gstack-openclaw-review', 'SKILL.md')];
const decoys = ['.context', 'node_modules', '.claude', 'dist'].map(dir => path.join(dir, 'nested', 'SKILL.md'));
try {
for (const file of [...unsafe, ...decoys, path.join('safe', 'SKILL.md')]) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), file === path.join('safe', 'SKILL.md') ? 'xargs -r ls -t' : 'xargs ls -t');
}
expect(unguardedSkillFiles(root).sort()).toEqual(unsafe.map(file => path.join(root, file)).sort());
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
});
+4 -3
View File
@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { createFakeBunCli } from './helpers/fake-bun-cli';
import fixture from './fixtures/eng-seeded-completion-ai.json';
import { classifyVisible, extractPlanFilePath } from './helpers/claude-pty-runner';
import * as predicates from './helpers/claude-pty-runner';
@@ -56,9 +57,9 @@ test('real PTY waits past old TODO, stale, partial and mismatched panels but acc
];
const results = await Promise.allSettled(scenarios.map(async scenario => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'seeded-completion-'));
const cli = path.join(dir, 'fake-claude'), working = path.join(dir, 'repo');
const working = path.join(dir, 'repo');
fs.mkdirSync(working);
fs.writeFileSync(cli, `#!/usr/bin/env bun
const cli = createFakeBunCli(path.join(dir, 'fake-claude'), `
const fs = require('node:fs');
fs.writeFileSync(process.env.COMPLETION_ARGV, JSON.stringify(process.argv.slice(2)));
let sent = false;
@@ -74,7 +75,7 @@ process.stdin.on('data', chunk => {
}, 4500);
});
setInterval(() => {}, 1000);
`, { mode: 0o755 });
`);
try {
const runner = pathToFileURL(path.join(import.meta.dir, 'helpers/claude-pty-runner.ts')).href;
const childFile = path.join(dir, 'observe.ts');
+4 -4
View File
@@ -97,8 +97,8 @@ function ownedEditDiffRows(rows: string[], file: string, ownedStateRoot?: string
const alias = path.basename(ownedStateRoot) === '.gstack' ? `~/.gstack/${relative}` : undefined;
const displayed = rows[2]?.trim() ?? '';
if (displayed !== file && displayed !== alias) {
const suffix = displayed.startsWith('…') ? displayed.slice(1) : '';
if ((suffix !== relative && !suffix.endsWith('/' + relative)) || !file.endsWith(suffix)) return null;
const suffix = displayed.startsWith('…') ? displayed.slice(1).split(path.sep).join('/') : '';
if ((suffix !== relative && !suffix.endsWith('/' + relative)) || !file.split(path.sep).join('/').endsWith(suffix)) return null;
}
return rows.slice(4);
}
@@ -111,10 +111,10 @@ function ownedEditDiffRows(rows: string[], file: string, ownedStateRoot?: string
if (update[1] !== file && update[1] !== alias) return null;
const displayed = rows[4]?.trim() ?? '';
if (displayed !== file && displayed !== alias) {
const suffix = displayed.startsWith('…') ? displayed.slice(1) : '';
const suffix = displayed.startsWith('…') ? displayed.slice(1).split(path.sep).join('/') : '';
// A truncated prefix must still retain the complete owned project/artifact
// path. A basename or sibling-project suffix cannot bind this request.
if ((suffix !== relative && !suffix.endsWith('/'+relative)) || !file.endsWith(suffix)) return null;
if ((suffix !== relative && !suffix.endsWith('/'+relative)) || !file.split(path.sep).join('/').endsWith(suffix)) return null;
}
return rows.slice(6);
}
+14
View File
@@ -0,0 +1,14 @@
import path from 'node:path';
/** Rebase recorded Unix paths without inserting raw backslashes into JSON. */
export function capturedPathRebaser(replacements: Array<[string, string]>) {
const text = (value: string) => replacements.reduce((value, [before, after]) =>
value.replaceAll(before, after.split(path.sep).join('/')), value);
// Translate separators without resolving traversal, redundant separators, or
// relative spelling that an ownership rejection control must still observe.
const file = (value: string) => text(value).split('/').join(path.sep);
const pathKeys = new Set(['cwd', 'config', 'stateRoot', 'file', 'file_path', 'transcriptPath']);
const json = <T>(value: T): T => JSON.parse(JSON.stringify(value), (key, value) =>
typeof value === 'string' ? (pathKeys.has(key) ? file(value) : text(value)) : value);
return { text, file, json };
}
+6 -1
View File
@@ -1,5 +1,5 @@
/** Completed parent file delivery and seeded coverage-diagram evidence. */
import * as path from 'node:path';
import { posix, win32 } from 'node:path';
import type { SkillTestResult } from './session-runner';
export interface CoverageAuditFiles {
@@ -16,9 +16,13 @@ const literal = (token: string): string | undefined => {
if (/^'[^']*'$/.test(token) || /^"[^"$`\\]*"$/.test(token)) return token.slice(1, -1);
return /^[^\s'"$`\\;|&<>]+$/.test(token) ? token : undefined;
};
// Recorded transcripts may come from another OS. Resolve their paths in the
// recorded cwd's namespace, retaining the canonical and containment checks.
const evidencePaths = (cwd: string) => /^(?:[A-Za-z]:[\\/]|\\\\)/.test(cwd) ? win32 : posix;
/** Closed literal cat/sed forms only; no shell execution or general shell parser. */
function readsFile(command: unknown, file: string, cwd: string, output: unknown, owned: CoverageAuditFiles): boolean {
const path = evidencePaths(cwd);
if (typeof command !== 'string' || command.length > 16384 || /[\r\n]/.test(command)) return false;
const parts: string[] = [];
const separators: string[] = [];
@@ -173,6 +177,7 @@ function delivered(output: unknown, expected: string): boolean {
}
export function coverageAuditReadEvidence(transcript: unknown[], files: CoverageAuditFiles): { sourceRead: boolean; testsRead: boolean } {
const path = evidencePaths(files.cwd);
const found = { sourceRead: false, testsRead: false };
if (!path.isAbsolute(files.cwd) || path.resolve(files.cwd) !== files.cwd || files.source.path === files.tests.path ||
[files.source, files.tests].some(f => {
+14
View File
@@ -0,0 +1,14 @@
import * as fs from 'node:fs';
/** Keep fixture CLIs native on Windows, where an executable shebang is unsupported. */
export function createFakeBunCli(file: string, source: string, native = process.platform === 'win32'): string {
const script = native ? `${file}.ts` : file;
fs.writeFileSync(script, `#!${process.execPath}\n` + source.replace(/^#![^\n]*\n/, ''), { mode: 0o755 });
if (!native) return script;
const executable = `${file}.exe`;
const built = Bun.spawnSync([process.execPath, 'build', '--compile', script, '--outfile', executable], {
stdout: 'pipe', stderr: 'pipe', timeout: 30_000,
});
if (built.exitCode !== 0) throw new Error(`Could not compile fake CLI: ${built.stderr.toString()}`);
return executable;
}
+5 -2
View File
@@ -11,7 +11,7 @@ const REASONS = ['invalid_event', 'input_overflow', 'stdin_timeout', 'record_err
type RecorderReason = typeof REASONS[number];
const identifier = (v: unknown): v is string => typeof v === 'string' && /^[A-Za-z0-9_-]{1,160}$/.test(v);
const object = (v: unknown): v is Record<string, any> => v !== null && typeof v === 'object' && !Array.isArray(v);
const quote = (v: string) => `'${(process.platform === 'win32' ? v.replaceAll('\\', '/') : v).replaceAll("'", "'\\''")}'`;
const quote = (v: string) => `'${v.replaceAll("'", "'\\''")}'`;
const keysOnly = (v: Record<string, unknown>, keys: string[]) => Object.keys(v).every(k => keys.includes(k));
function questions(value: unknown): value is NativePlanQuestion[] {
@@ -85,7 +85,10 @@ export function createPendingQuestionRecorder(cwd: string, configDir: string) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-pending-question-'));
const file = path.join(dir, 'state.json');
fs.writeFileSync(file, JSON.stringify({version:1, cwd, configDir, seenIds:[], pending:null}) + '\n', {mode:0o600});
const command = [process.execPath, import.meta.path, '--record', file, cwd, configDir].map(quote).join(' ');
// Git Bash needs slash-separated command paths. Recorder arguments retain
// their native spelling because the owned event/state identity is exact.
const shellPath = (value: string) => process.platform === 'win32' ? value.replaceAll('\\', '/') : value;
const command = [shellPath(process.execPath), shellPath(import.meta.path), '--record', file, cwd, configDir].map(quote).join(' ');
const hook = {matcher:'^AskUserQuestion$', hooks:[{type:'command', command, timeout:5}]};
return {file, hooks:{PreToolUse:[hook], PostToolUse:[hook], PostToolUseFailure:[hook]},
dispose: () => fs.rmSync(dir, {recursive:true, force:true})};
+4 -3
View File
@@ -3,15 +3,16 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { createFakeBunCli } from './helpers/fake-bun-cli';
const fixture = JSON.parse(fs.readFileSync(path.join(import.meta.dir, 'fixtures/native-auto-decide-ag.json'), 'utf8'));
const retry = fixture.attempts[1];
const annotation = retry.transcript.assistantMessages.find((m: any) => m.sessionId === retry.options.sessionId && m.text.includes('Auto-decided')).text;
test('owned native AUTO_DECIDE survives damaged terminal and polls again after scope selection', async () => {
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'auto-decide-pty-')), cli=path.join(dir,'fake-claude');
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'auto-decide-pty-'));
const working=path.join(dir,'repo');fs.mkdirSync(working);
fs.writeFileSync(cli, `#!/usr/bin/env bun
const cli = createFakeBunCli(path.join(dir,'fake-claude'), `
const fs=require('node:fs'),path=require('node:path');
const args=process.argv.slice(2),id=args[args.indexOf('--session-id')+1];
fs.writeFileSync(process.env.AUTO_TEST_ARGV,JSON.stringify(args));
@@ -26,7 +27,7 @@ process.stdin.on('data',chunk=>{
setTimeout(()=>{fs.appendFileSync(file,record('assistant',Date.now()-base-1,[{type:'text',text:${JSON.stringify(annotation)}}])+'\\n');process.stdout.write('Auto-dcided review mode: HOLD SCOPE. DONE.\\n');},4500);
});
setInterval(()=>{},1000);
`,{mode:0o755});
`);
try {
const runner=pathToFileURL(path.join(import.meta.dir,'helpers/claude-pty-runner.ts')).href;
const childFile=path.join(dir,'observe.ts');
+15 -5
View File
@@ -8,7 +8,7 @@ const source = fs.readFileSync(path.join(import.meta.dir, 'skill-e2e-office-hour
const question = (word = 'architectural', options = 'A) Put retrieval on the server\nB) Put retrieval on the client') =>
`Where should retrieval live?\nThis ${word} choice decides which component owns the retrieval algorithm and the cross-host API contract.\n${options}\nRecommendation: A because all hosts need a consistent algorithm with one owner.\n`;
async function runCaller(captured: string | undefined, scenario: 'success' | 'timeout' | 'judge-fail' | 'judge-throws' = 'success') {
async function runCaller(captured: string | undefined, scenario: 'success' | 'timeout' | 'judge-fail' | 'judge-throws' = 'success', fixturePath = path) {
// Exercise the actual caller with no SDK, filesystem, git or judge dispatch.
let executable = source;
for (const declaration of source.matchAll(/^import[\s\S]*?;\n/gm)) executable = executable.replace(declaration[0], '');
@@ -16,11 +16,12 @@ async function runCaller(captured: string | undefined, scenario: 'success' | 'ti
const setups: Array<() => void> = [], callbacks: Array<() => Promise<void>> = [], finalizers: Array<() => Promise<void>> = [];
const files = new Map<string,string>(), rows: any[] = [], calls: any[] = [];
const collector = { addTest: (row: any) => rows.push(row) };
const sourceRoot = fixturePath.join(fixturePath.sep, 'source');
let judged = 0;
const result = { exitReason: scenario === 'timeout' ? 'timeout' : 'success', browseErrors: [], durationMs: 123, costUsd: 0.1, output: 'public completion' };
const args: Record<string,any> = {
expect, beforeAll: (fn: () => void) => setups.push(fn), afterAll: (fn: () => Promise<void>) => finalizers.push(fn),
CAPTURE_MS, CAPTURE_LONG_MS, ROOT: '/source', runId: 'synthetic-run',
CAPTURE_MS, CAPTURE_LONG_MS, ROOT: sourceRoot, runId: 'synthetic-run',
describeIfSelected: (_title: string, _names: string[], fn: () => void) => fn(),
testConcurrentIfSelected: (name: string, fn: () => Promise<void>, timeout: number) => { expect(name).toBe('office-hours-phase4-fork'); expect(timeout).toBe(CAPTURE_LONG_MS); callbacks.push(fn); },
createEvalCollector: () => collector, finalizeEvalCollector: async () => {}, logCost: () => {},
@@ -32,13 +33,13 @@ async function runCaller(captured: string | undefined, scenario: 'success' | 'ti
if (scenario === 'judge-fail') throw new Error('synthetic judge assertion');
return { present: true, commits: true, has_because: true, reason_substance: 5 };
},
spawnSync: () => ({status:0}), path, os: {tmpdir:()=>'/tmp'},
spawnSync: () => ({status:0}), path: fixturePath, os: {tmpdir:()=>'/tmp'},
fs: { mkdtempSync:(prefix:string)=>prefix+'owned', mkdirSync:()=>{}, rmSync:()=>{}, existsSync:(name:string)=>files.has(name),
writeFileSync:(name:string,body:string)=>files.set(name,body), readFileSync:(name:string)=>name==='/source/office-hours/SKILL.md' ? '## AskUserQuestion Format\nformat\n## Phase 4: Alternatives Generation\nworkflow\n## Phase 4.5\nnext' : files.get(name) },
writeFileSync:(name:string,body:string)=>files.set(name,body), readFileSync:(name:string)=>name===fixturePath.join(sourceRoot,'office-hours','SKILL.md') ? '## AskUserQuestion Format\nformat\n## Phase 4: Alternatives Generation\nworkflow\n## Phase 4.5\nnext' : files.get(name) },
runSkillTest: async (opts: any) => {
calls.push(opts); expect(opts.timeout).toBe(CAPTURE_MS); expect(opts.maxTurns).toBe(12); expect(opts.model).toBe('claude-opus-4-7');
expect(opts.prompt).toContain('Do NOT call any tool to ask the user.');
if (captured !== undefined) files.set(path.join(opts.workingDirectory, 'phase4-capture.md'), captured);
if (captured !== undefined) files.set(fixturePath.join(opts.workingDirectory, 'phase4-capture.md'), captured);
return result;
},
};
@@ -89,6 +90,15 @@ test('Phase4 caller records every returned-result failure exactly once, includin
}
});
test('Phase4 caller fixture retains fork validation under either path convention', async () => {
for (const fixturePath of [path.posix, path.win32]) {
const accepted=await runCaller(question(),'success',fixturePath);
expect(accepted.thrown).toBeUndefined(); expect(accepted.rows[0].passed).toBe(true);
const rejected=await runCaller(question('architectural','A) Only one option'),'success',fixturePath);
expect(rejected.thrown).toBeDefined(); expect(rejected.judged).toBe(0); expect(rejected.rows[0].passed).toBe(false);
}
});
test('Phase4 caller controls select only the existing Phase4 paid owner', () => {
expect(Object.entries(E2E_TOUCHFILES).filter(([,paths])=>paths.includes('test/office-hours-phase4-caller.test.ts')).map(([name])=>name)).toEqual(['office-hours-phase4-fork']);
});
+1 -1
View File
@@ -122,7 +122,7 @@ test('automatic workflow templates delegate reviewer commands to shared resolver
for (const file of templateFiles(join(ROOT, skill))) {
for (const line of shellLines(readFileSync(file, 'utf8'))) {
// /spec's --execute worker implements the filed spec; it is not a reviewer.
if (file.endsWith('spec/sections/gate-and-file.md.tmpl') && line.trim() === 'cat "$ARCHIVE_PATH" | (cd "$SPAWN_PATH" && claude -p 2>&1) &') continue;
if (file === join(ROOT, 'spec', 'sections', 'gate-and-file.md.tmpl') && line.trim() === 'cat "$ARCHIVE_PATH" | (cd "$SPAWN_PATH" && claude -p 2>&1) &') continue;
if (/\bcodex\s+(?:exec|review)\b|\bclaude\s+(?:-p|--print)\b|\bgstack-claude-code\s+--/.test(line)) {
violations.push(`${file.slice(ROOT.length + 1)}: ${line.trim()}`);
}
+10 -4
View File
@@ -162,10 +162,16 @@ 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 {
fs.writeFileSync(file, JSON.stringify(state));
return currentFilePermissionEpoch(file, delta.expected ?? c.binding.expected, delta.cwd ?? c.p.cwd,
delta.config ?? c.p.config, delta.startedAt ?? c.p.startUnix * 1000,
delta.transcript ?? c.observation.transcript, screen);
// 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) {
+32 -5
View File
@@ -4,6 +4,7 @@ import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { readPlanFloorTarget } from './helpers/plan-floor-target';
import { createFakeBunCli } from './helpers/fake-bun-cli';
import { selectTests, E2E_TOUCHFILES } from './helpers/touchfiles';
const ROOT = path.resolve(import.meta.dir, '..');
@@ -86,11 +87,25 @@ test('new delivery helper and controls select all four existing floor owners', (
}
});
test('compiled fake CLI preserves Claude arguments and stdin without a shebang launcher', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'native-floor-cli-'));
try {
const cli = createFakeBunCli(path.join(dir, 'fake-claude'), `
process.stdin.on('data', chunk => { console.log(JSON.stringify({ argv: process.argv.slice(2), input: chunk.toString() })); process.exit(0); });
`, true);
const args = ['--model', 'fixture-model', '--session-id', SID];
const child = Bun.spawn([cli, ...args], { stdin: new Blob(['/plan-design-review PLAN.md\r']), stdout: 'pipe', stderr: 'pipe' });
const [exit, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
expect(exit, stderr).toBe(0);
expect(JSON.parse(stdout)).toEqual({ argv: args, input: '/plan-design-review PLAN.md\r' });
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
test('real fake CLI sees seed before command; missing acknowledgment and scope menu never satisfy floor', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'floor-delivery-pty-'));
try {
const cli = path.join(dir, 'fake-claude'), worker = path.join(dir, 'worker.ts'), output = path.join(dir, 'results.json');
fs.writeFileSync(cli, `#!/usr/bin/env bun
const worker = path.join(dir, 'worker.ts'), output = path.join(dir, 'results.json');
const cli = createFakeBunCli(path.join(dir, 'fake-claude'), `
const fs = require('node:fs'), path = require('node:path');
const args = process.argv.slice(2), id = args[args.indexOf('--session-id') + 1];
const record = value => fs.appendFileSync(process.env.FLOOR_RECORD, JSON.stringify(value)+'\\n');
@@ -108,13 +123,25 @@ process.stdin.on('data', chunk => {
}
process.stdout.write(mode==='scope'?'What should I review?\\n❯ 1. Current branch diff\\n 2. A plan or design doc\\n':'Finding 1: CTA hierarchy is unclear.\\n❯ 1. Emphasize the primary CTA\\n 2. Keep equal weight\\n');
});
process.on('SIGINT',()=>{record({type:'closed'});process.exit(0)});
process.on('SIGINT',()=>process.exit(0));
process.stdin.resume();
`);
fs.chmodSync(cli, 0o755);
fs.writeFileSync(worker, `
import {runPlanSkillFloorCheck} from ${JSON.stringify(pathToFileURL(path.join(ROOT, 'test/helpers/claude-pty-runner.ts')).href)};
import fs from 'node:fs';
// Observe actual process exit: Windows SIGINT uses TerminateProcess and cannot
// run a child signal handler. The native PTY and its process stay unchanged.
const spawn=Bun.spawn;
Bun.spawn=((command,options)=>{
if(Array.isArray(command)&&command[0]===process.env.BROWSE_TERMINAL_BINARY){
const onExit=options?.onExit;
return spawn(command,{...options,onExit(proc,...details){
fs.appendFileSync(options.env.FLOOR_RECORD,JSON.stringify({type:'closed',pid:proc.pid})+'\\n');
onExit?.(proc,...details);
}});
}
return spawn(command,options);
}) as typeof Bun.spawn;
const modes=['ready','missing','foreign','scope'];
const results=await Promise.all(modes.map(async mode=>({mode,observation:await runPlanSkillFloorCheck({skillName:'plan-design-review',slashCommand:'/plan-design-review',followUpPrompt:${JSON.stringify(SEED)},cwd:${JSON.stringify(ROOT)},timeoutMs:2500,model:'claude-fable-5-1',env:{FLOOR_MODE:mode,FLOOR_RECORD:${JSON.stringify(dir)}+'/'+mode+'.jsonl'}})})));
fs.writeFileSync(${JSON.stringify(output)},JSON.stringify(results));
@@ -132,7 +159,7 @@ fs.writeFileSync(${JSON.stringify(output)},JSON.stringify(results));
expect(startup.argv[startup.argv.indexOf('--permission-mode') + 1]).toBe('plan');
expect(startup.argv[startup.argv.indexOf('--model') + 1]).toBe('claude-fable-5-1');
expect(events.filter(e => e.type === 'input').map(e => e.input).join('')).toBe('/plan-design-review PLAN.md\r');
expect(events.at(-1).type).toBe('closed'); expect(fs.existsSync(startup.cwd)).toBe(false);
expect(events.at(-1)).toEqual({type:'closed',pid:startup.pid}); expect(fs.existsSync(startup.cwd)).toBe(false);
expect(() => process.kill(startup.pid, 0)).toThrow();
expect(result.observation.auqObserved).toBe(result.mode === 'ready');
expect(result.observation.outcome).toBe(result.mode === 'ready' ? 'auq_observed' : 'timeout');
+3 -3
View File
@@ -5,6 +5,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { createFakeBunCli } from './helpers/fake-bun-cli';
import { nativeSeededPlanSelection } from './helpers/plan-scope-selection';
import { isScopeGateQuestionVisible } from './helpers/claude-pty-runner';
import { readPlanCountTranscript, type NativePublicToolEvent, type PlanCountTranscript } from './helpers/plan-count-transcript';
@@ -156,8 +157,7 @@ test('seeded plan selection dependencies select the existing design and Eng mode
test('real PTY observation binds its explicit session and retains public diagnostics before cleanup', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scope-pty-'));
const cli = path.join(dir, 'fake-claude');
fs.writeFileSync(cli, `#!/usr/bin/env bun
const cli = createFakeBunCli(path.join(dir, 'fake-claude'), `
const fs = require('node:fs'), path = require('node:path');
const args = process.argv.slice(2), id = args[args.indexOf('--session-id') + 1];
fs.writeFileSync(process.env.SCOPE_TEST_ARGV, JSON.stringify(args));
@@ -176,7 +176,7 @@ process.stdin.on('data', chunk => {
process.stdout.write('Reviewing the named draft.\\nA) Fix hierarchy\\nB) Keep hierarchy\\nRecommendation: A because the primary action needs emphasis.\\nReply with A or B.\\n');
});
setInterval(()=>{},1000);
`, { mode: 0o755 });
`);
try {
// The executable override belongs to a separate process: parallel free
// tests cannot inherit it, and resolution is asserted before any spawn.
+14 -5
View File
@@ -5,23 +5,24 @@ import { JUDGE_MS, CAPTURE_MS } from './helpers/eval-budgets';
import { E2E_TOUCHFILES } from './helpers/touchfiles';
const source=fs.readFileSync(path.join(import.meta.dir,'skill-e2e-review-army.test.ts'),'utf8');
async function exercise(scenarios: Array<'success'|'timeout'|'wrong-report'|'browse-error'>) {
async function exercise(scenarios: Array<'success'|'timeout'|'wrong-report'|'browse-error'>, fixturePath = path) {
const setups:any[]=[],done:any[]=[],callbacks:any[]=[],rows:any[]=[],calls:any[]=[];
const files=new Map<string,string>(); let outer=0,index=0;
const sourceRoot = fixturePath.join(fixturePath.sep, 'source');
const args: Record<string,any>={
expect,JUDGE_MS,CAPTURE_MS,ROOT:'/source',runId:'synthetic-run',process:{pid:123,env:{EVALS_RUN_ID:'synthetic-controller'}},
expect,JUDGE_MS,CAPTURE_MS,ROOT:sourceRoot,runId:'synthetic-run',process:{pid:123,env:{EVALS_RUN_ID:'synthetic-controller'}},
beforeAll:(fn:any)=>setups.push(fn),afterAll:(fn:any)=>done.push(fn),
describeIfSelected:(_title:string,names:string[],fn:any)=>{if(names.includes('review-army-consensus'))fn();},
testConcurrentIfSelected:(name:string,fn:any,timeout:number)=>{expect(name).toBe('review-army-consensus');callbacks.push(fn);outer=timeout;},
createEvalCollector:()=>({}),finalizeEvalCollector:()=>{},logCost:()=>{},spawnSync:()=>({status:0}),path,os:{tmpdir:()=>'/tmp'},
createEvalCollector:()=>({}),finalizeEvalCollector:()=>{},logCost:()=>{},spawnSync:()=>({status:0}),path:fixturePath,os:{tmpdir:()=>'/tmp'},
fs:{mkdirSync:()=>{},readdirSync:()=>[],mkdtempSync:(p:string)=>p+'owned',writeFileSync:(p:string,s:string)=>files.set(p,s),copyFileSync:()=>{},rmSync:()=>{},
existsSync:(p:string)=>files.has(p),readFileSync:(p:string)=>p.startsWith('/source/')?'synthetic fixture bytes '.repeat(30):files.get(p)},
existsSync:(p:string)=>files.has(p),readFileSync:(p:string)=>p.startsWith(sourceRoot+fixturePath.sep)?'synthetic fixture bytes '.repeat(30):files.get(p)},
extractSkillSections:()=> 'Review instructions',REVIEW_ARMY_E2E_SECTIONS:[],
runSkillTest:async(opts:any)=>{
calls.push(opts);const scenario=scenarios[index++];
expect(opts.timeout).toBe(CAPTURE_MS);expect(opts.maxTurns).toBe(20);expect(opts.model).toBeUndefined();
expect(opts.prompt).toContain('MULTI-SPECIALIST CONFIRMED');expect(opts.prompt).toContain('SQL injection in an auth controller');
files.set(path.join(opts.workingDirectory,'review-output.md'),scenario==='wrong-report'?'Nothing to discuss.':'The SQL injection permits auth bypass.');
files.set(fixturePath.join(opts.workingDirectory,'review-output.md'),scenario==='wrong-report'?'Nothing to discuss.':'The SQL injection permits auth bypass.');
return {exitReason:scenario==='timeout'?'timeout':'success',browseErrors:scenario==='browse-error'?['existing browser failure']:[],output:'public response'};
},
recordE2E:(_collector:any,name:string,title:string,result:any,extra:any)=>rows.push({name,title,passed:result.exitReason==='success'&&result.browseErrors.length===0,...extra,result}),
@@ -52,6 +53,14 @@ test('Consensus verdict retains the existing browser-error guard',async()=>{
const x=await exercise(['browse-error']);expect(x.rows).toHaveLength(1);expect(x.rows[0].passed).toBe(false);
});
test('Consensus caller fixture preserves success and semantic failure under either path convention', async()=>{
for (const fixturePath of [path.posix, path.win32]) {
const x=await exercise(['success','wrong-report'], fixturePath);
expect(x.errors[0]).toBeUndefined(); expect(x.errors[1]).toBeDefined();
expect(x.rows.map(r=>r.passed)).toEqual([true,false]);
}
});
test('Consensus lifecycle controls select the existing consensus owner only',()=>{
expect(Object.entries(E2E_TOUCHFILES).filter(([,paths])=>paths.includes('test/review-consensus-lifecycle.test.ts')).map(([name])=>name)).toEqual(['review-army-consensus']);
});
+6 -3
View File
@@ -7,7 +7,8 @@ import { redactFindingSpans } from '../lib/redact-engine';
import { E2E_TOUCHFILES } from './helpers/touchfiles-data';
const TOKEN = 'gbrain_fake_token_for_test';
const CREDENTIAL = 'ghp_aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5';
// Assemble synthetic credentials at runtime, as in gate-secret-scan.test.ts.
const CREDENTIAL = ['ghp_', 'aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5'].join('');
const source = fs.readFileSync(path.join(import.meta.dir, 'skill-e2e-setup-gbrain-path4-local-pglite.test.ts'), 'utf8');
async function runCaller(scenario: 'success' | 'no-auq' | 'no-path' | 'leaked-claude-md' | 'diagnostic-secret', diagnostic?: { sink: 'output' | 'tool' | 'error'; text: string }) {
@@ -107,7 +108,8 @@ test('Path4 caller controls select only their registered paid owner', () => {
test('Path4 redacted credential URLs cannot erase success or mask a failed assertion', async () => {
// Text redaction can consume JSON punctuation after an unspaced URL host.
// These synthetic credentials exercise the real redactor and actual callback.
for (const text of ['postgres://fixture:syntheticCredential923@db.invalid', 'https://fixture:syntheticCredential923@service.invalid']) {
for (const [scheme, host] of [['postgres', 'db.invalid'], ['https', 'service.invalid']]) {
const text = `${scheme}://fixture:syntheticCredential923@${host}`;
for (const sink of ['output', 'tool', 'error'] as const) {
for (const scenario of ['success', 'no-auq'] as const) {
const { row, thrown } = await runCaller(scenario, { sink, text });
@@ -129,7 +131,8 @@ test('Path4 redacted credential URLs cannot erase success or mask a failed asser
test('Path4 unbounded secret diagnostics are omitted without changing the recorded verdict', async () => {
for (const scenario of ['success', 'no-auq'] as const) {
const { row, thrown } = await runCaller(scenario, { sink: 'output', text: '-----BEGIN PRIVATE KEY----- synthetic incomplete private material' });
const text = ['-----BEGIN ', 'PRIVATE KEY----- synthetic incomplete private material'].join('');
const { row, thrown } = await runCaller(scenario, { sink: 'output', text });
expect(row.passed).toBe(scenario === 'success');
if (scenario === 'success') expect(thrown).toBeUndefined();
else expect(thrown).toBeDefined();
+1 -1
View File
@@ -16,7 +16,7 @@ async function runCaller(index: number, change?: (result: any) => void) {
const args:Record<string,any> = { describeIfSelected:(_title:string,_names:string[],fn:Function)=>fn(),
beforeAll:(fn:Function)=>setup.push(fn),afterAll:(fn:Function)=>cleanup.push(fn),
testConcurrentIfSelected:(_name:string,fn:Function,timeout:number)=>{expect(timeout).toBe(CAPTURE_MS);callbacks.push(fn);},
fs:virtualFs,os:{tmpdir:()=>'/tmp'},path,ROOT:'/synthetic-gstack',copyDirSync:()=>{},spawnSync:()=>({status:0}),
fs:virtualFs,os:{tmpdir:()=>'/tmp'},path:path.posix,ROOT:'/synthetic-gstack',copyDirSync:()=>{},spawnSync:()=>({status:0}),
runSkillTest:async (opts:any)=>{invocations.push(opts);return row.result;},runId:'synthetic-run',JUDGE_MS,CAPTURE_MS,
coverageAuditReadEvidence:(evidence as any).coverageAuditReadEvidence,
logCost:()=>{},recordE2E:(_collector:any,_name:string,_suite:string,_result:any,options:any)=>records.push(options),evalCollector:{},expect,console:{log:()=>{}},