11// Spawned as a subprocess by e2e/exec-signal-finalize.test.ts.
22// Installs process-level signal handlers the way import.meta.main does, then
3- // calls production runExec. Does not register the active-run handle itself —
4- // that is the product path under test.
3+ // calls production runExec. The fixture never registers the active-run handle
4+ // itself, because runExec registering it is the product path under test.
55import { existsSync , readFileSync } from "node:fs" ;
66import { join } from "node:path" ;
77
@@ -20,22 +20,33 @@ const runDir = sessionDir(cwd, sessionId);
2020const runJsonPath = join ( runDir , "run.json" ) ;
2121const turnsUsed = Number ( process . env [ "SIGNAL_TEST_TURNS_USED" ] ?? "5" ) ;
2222
23- async function waitForRunningRunJson ( ) : Promise < void > {
23+ async function pollUntil < T > (
24+ probe : ( ) => T | null ,
25+ timeoutMs : number ,
26+ errorMessage : string ,
27+ ) : Promise < T > {
28+ const deadline = Date . now ( ) + timeoutMs ;
2429 for ( ; ; ) {
25- if ( existsSync ( runJsonPath ) ) {
26- try {
27- const state = JSON . parse ( readFileSync ( runJsonPath , "utf8" ) ) as {
28- status ?: string ;
29- } ;
30- if ( state . status === "running" ) return ;
31- } catch {
32- // rename/parse race on the first persist
33- }
34- }
30+ const value = probe ( ) ;
31+ if ( value !== null ) return value ;
32+ if ( Date . now ( ) >= deadline ) throw new Error ( errorMessage ) ;
3533 await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
3634 }
3735}
3836
37+ function runJsonIsRunning ( ) : true | null {
38+ if ( ! existsSync ( runJsonPath ) ) return null ;
39+ try {
40+ const state = JSON . parse ( readFileSync ( runJsonPath , "utf8" ) ) as {
41+ status ?: string ;
42+ } ;
43+ return state . status === "running" ? true : null ;
44+ } catch {
45+ // The first persist can be read mid-rename; retry on the next poll.
46+ return null ;
47+ }
48+ }
49+
3950await withMockedModuleDuring (
4051 import . meta. resolve ( "../../src/session/assemble-runtime.js" ) ,
4152 ( real : typeof import ( "../../src/session/assemble-runtime.js" ) ) => ( {
@@ -64,11 +75,18 @@ await withMockedModuleDuring(
6475 sessionId,
6576 } as unknown as Config ;
6677 void runExec ( config ) ;
67- await waitForRunningRunJson ( ) ;
68- const handle = getActiveRun ( ) ;
69- if ( handle === null ) {
70- throw new Error ( "runExec did not register an active-run handle" ) ;
71- }
78+ await pollUntil (
79+ runJsonIsRunning ,
80+ 10_000 ,
81+ `run.json at ${ runJsonPath } did not reach status "running"` ,
82+ ) ;
83+ // runExec registers the handle after saveState resolves, so run.json can
84+ // show "running" on disk before the handle exists.
85+ const handle = await pollUntil (
86+ getActiveRun ,
87+ 5_000 ,
88+ "runExec did not register an active-run handle" ,
89+ ) ;
7290 // Advance the live counter the way a mid-run snapshot would, without
7391 // reading run.json on the signal path under test.
7492 syncRunStateHandle ( handle , {
0 commit comments