22import assert from 'node:assert/strict' ;
33import { mkdtemp , readFile , writeFile , mkdir , readdir , symlink , unlink } from 'node:fs/promises' ;
44import { tmpdir } from 'node:os' ;
5- import { join } from 'node:path' ;
5+ import { join , sep } from 'node:path' ;
66import { spawn } from 'node:child_process' ;
7+ import { fileURLToPath } from 'node:url' ;
78import test from 'node:test' ;
89import { resolveWorkspaceStorage } from '../scripts/lib/workspace.mjs' ;
910import { createStateStore } from '../scripts/lib/state.mjs' ;
@@ -12,11 +13,13 @@ import { createManagedZCodeClient, createZCodeClient, releaseManagedZCodeOwner }
1213import { ownerIdForSession } from '../scripts/lib/job-control.mjs' ;
1314import { brokerEndpointFor , ensureZCodeBroker , prioritizeBrokerOwnership , probeBrokerHealth , reconcileBrokerOwnership , writeBrokerIdentity } from '../scripts/zcode-broker.mjs' ;
1415
15- const root = new URL ( '../' , import . meta. url ) . pathname ;
16+ const root = fileURLToPath ( new URL ( '../' , import . meta. url ) ) ;
1617const fakeZCode = join ( root , 'tests/fixtures/fake-zcode-cli.mjs' ) ;
1718const legacyBroker = join ( root , 'tests/fixtures/legacy-zcode-broker-v1.mjs' ) ;
1819const ownerStoreLockHolder = join ( root , 'tests/fixtures/owner-store-lock-holder.mjs' ) ;
1920
21+ function isGateRunPath ( path ) { return path . split ( sep ) . includes ( 'gate-runs' ) ; }
22+
2023async function jsonFiles ( directory ) {
2124 const found = [ ] ; let entries ;
2225 try { entries = await readdir ( directory , { withFileTypes : true } ) ; } catch { return found ; }
@@ -121,7 +124,7 @@ test('unborn repositories get baselines and full untracked contents affect finge
121124 const prompt = await runHook ( 'user-prompt-hook.mjs' , { session_id : 'unborn' , turn_id : 'turn' , cwd, hook_event_name : 'UserPromptSubmit' , transcript_path : null , model : 'gpt' , permission_mode : 'default' , prompt : 'work' } , env ) ; assert . equal ( prompt . code , 0 ) ; assert . deepEqual ( prompt . json , { } ) ; assert . equal ( ( await createIdentityStore ( { dataRoot : data } ) . resolveActiveTurn ( { sessionId : 'unborn' , workspace : cwd } ) ) . turnId , 'turn' ) ;
122125 bytes . fill ( 66 , 160 * 1024 , 224 * 1024 ) ; await writeFile ( join ( cwd , 'large.bin' ) , bytes ) ;
123126 const stop = await runHook ( 'stop-review-gate-hook.mjs' , { session_id : 'unborn' , turn_id : 'turn' , cwd, hook_event_name : 'Stop' , transcript_path : null , model : 'gpt' , permission_mode : 'default' , stop_hook_active : false , last_assistant_message : 'done' } , env ) ; assert . equal ( stop . code , 0 ) ; assert . deepEqual ( stop . json , { } ) ;
124- assert . equal ( ( await jsonFiles ( join ( data , 'workspaces' ) ) ) . filter ( ( path ) => path . includes ( '/gate-runs/' ) ) . length , 1 , 'same-size middle-only untracked edits must change the fingerprint' ) ;
127+ assert . equal ( ( await jsonFiles ( join ( data , 'workspaces' ) ) ) . filter ( isGateRunPath ) . length , 1 , 'same-size middle-only untracked edits must change the fingerprint' ) ;
125128} ) ;
126129
127130test ( 'changing only an untracked symlink target changes the fingerprint without following it' , async ( ) => {
@@ -131,7 +134,7 @@ test('changing only an untracked symlink target changes the fingerprint without
131134 const prompt = await runHook ( 'user-prompt-hook.mjs' , { session_id : 'symlink' , turn_id : 'turn' , cwd, hook_event_name : 'UserPromptSubmit' , transcript_path : null , model : 'gpt' , permission_mode : 'default' , prompt : 'work' } , env ) ; assert . equal ( prompt . code , 0 ) ;
132135 await unlink ( link ) ; await symlink ( 'missing-target-b' , link ) ;
133136 const stop = await runHook ( 'stop-review-gate-hook.mjs' , { session_id : 'symlink' , turn_id : 'turn' , cwd, hook_event_name : 'Stop' , transcript_path : null , model : 'gpt' , permission_mode : 'default' , stop_hook_active : false , last_assistant_message : 'done' } , env ) ; assert . equal ( stop . code , 0 ) ; assert . deepEqual ( stop . json , { } ) ;
134- assert . equal ( ( await jsonFiles ( join ( data , 'workspaces' ) ) ) . filter ( ( path ) => path . includes ( '/gate-runs/' ) ) . length , 1 , 'same-path symlink target changes must reach the gate path' ) ;
137+ assert . equal ( ( await jsonFiles ( join ( data , 'workspaces' ) ) ) . filter ( isGateRunPath ) . length , 1 , 'same-path symlink target changes must reach the gate path' ) ;
135138} ) ;
136139
137140test ( 'SubagentStart marks forwarding suppression without changing parent permission snapshot' , async ( ) => {
@@ -332,7 +335,7 @@ test('Stop gate skips unchanged and atomically consumes exact changed baseline',
332335 const input = { session_id : 'parent' , turn_id : 'turn-2' , cwd, hook_event_name : 'Stop' , transcript_path : null , model : 'gpt' , permission_mode : 'default' , stop_hook_active : false , last_assistant_message : 'done' } ;
333336 const [ one , two ] = await Promise . all ( [ runHook ( 'stop-review-gate-hook.mjs' , input , { ...env , ZCODE_PATH : fakeZCode , FAKE_ZCODE_GATE_RESULT : 'ALLOW: clean' } ) , runHook ( 'stop-review-gate-hook.mjs' , input , { ...env , ZCODE_PATH : fakeZCode , FAKE_ZCODE_GATE_RESULT : 'ALLOW: clean' } ) ] ) ;
334337 assert . equal ( [ one , two ] . filter ( ( result ) => result . json ?. decision === 'block' ) . length , 0 ) ;
335- const snapshots = ( await jsonFiles ( join ( data , 'workspaces' ) ) ) . filter ( ( path ) => path . includes ( '/gate-runs/' ) ) ;
338+ const snapshots = ( await jsonFiles ( join ( data , 'workspaces' ) ) ) . filter ( isGateRunPath ) ;
336339 assert . equal ( snapshots . length , 1 ) ;
337340} ) ;
338341
@@ -369,7 +372,7 @@ test('Stop rechecks stale setup readiness before session creation and fails open
369372 ] ) await t . test ( scenario . name , async ( ) => {
370373 const { cwd, data, env } = await workspace ( ) ; const record = join ( data , 'zcode-calls.jsonl' ) ; await writeFile ( record , '' ) ; await writeGateConfig ( data , cwd , { enabled : true , setupReady : true , status : 'ready' } ) ; await runHook ( 'session-lifecycle-hook.mjs' , { session_id : 'owner' , cwd, hook_event_name : 'SessionStart' , transcript_path : null , model : 'gpt' , permission_mode : 'default' , source : 'startup' } , env ) ;
371374 const prompt = { session_id : 'owner' , turn_id : 'turn' , cwd, hook_event_name : 'UserPromptSubmit' , transcript_path : null , model : 'gpt' , permission_mode : 'default' , prompt : 'edit' } ; await runHook ( 'user-prompt-hook.mjs' , prompt , env ) ; await writeFile ( join ( cwd , 'tracked.txt' ) , `${ scenario . name } \n` ) ; const script = scenario . fixture ? join ( root , 'tests/fixtures/stop-gate-with-timeout.mjs' ) : 'stop-review-gate-hook.mjs' ; const result = await runHook ( script , { ...stopFields ( prompt ) , hook_event_name : 'Stop' , stop_hook_active : false , last_assistant_message : 'done' } , { ...env , ZCODE_PATH : fakeZCode , FAKE_ZCODE_RECORD : record , ...scenario . extra } , { absolute : scenario . fixture } ) ;
372- assert . equal ( result . code , 0 ) ; assert . notEqual ( result . json ?. decision , 'block' ) ; assert . match ( result . json . systemMessage , / \$ z c o d e : s e t u p / ) ; const runs = ( await jsonFiles ( join ( data , 'workspaces' ) ) ) . filter ( ( path ) => path . includes ( '/gate-runs/' ) ) ; assert . equal ( runs . length , 1 ) ; const snapshot = JSON . parse ( await readFile ( runs [ 0 ] , 'utf8' ) ) ; assert . equal ( snapshot . status , 'skipped_setup_not_ready' ) ; assert . equal ( snapshot . reason , scenario . reason ) ;
375+ assert . equal ( result . code , 0 ) ; assert . notEqual ( result . json ?. decision , 'block' ) ; assert . match ( result . json . systemMessage , / \$ z c o d e : s e t u p / ) ; const runs = ( await jsonFiles ( join ( data , 'workspaces' ) ) ) . filter ( isGateRunPath ) ; assert . equal ( runs . length , 1 ) ; const snapshot = JSON . parse ( await readFile ( runs [ 0 ] , 'utf8' ) ) ; assert . equal ( snapshot . status , 'skipped_setup_not_ready' ) ; assert . equal ( snapshot . reason , scenario . reason ) ;
373376 const calls = ( await readFile ( record , 'utf8' ) ) . trim ( ) . split ( '\n' ) . filter ( Boolean ) . map ( JSON . parse ) ; assert . ok ( ! calls . some ( ( call ) => call . method === 'session/send' ) ) ;
374377 } ) ;
375378} ) ;
0 commit comments