@@ -12,6 +12,7 @@ import {
1212} from "node:fs" ;
1313import { createConnection , type Socket } from "node:net" ;
1414import { join , resolve } from "node:path" ;
15+ import { Writable } from "node:stream" ;
1516import { afterEach , describe , expect , it } from "vitest" ;
1617import { APP_NAME , ENV_AGENT_DIR } from "../../../src/config.js" ;
1718import { getProcessStartId } from "../../../src/core/session-lease.js" ;
@@ -26,6 +27,8 @@ import {
2627import {
2728 DAEMON_WORKER_ACTIVE_SESSION_ID_ENV ,
2829 DAEMON_WORKER_ROLE_ENV ,
30+ DAEMON_WORKER_STARTUP_GATE_COMMIT ,
31+ DAEMON_WORKER_STARTUP_GATE_FD_ENV ,
2932 DAEMON_WORKER_SUPERVISOR_SOCKET_ENV ,
3033 DAEMON_WORKER_TOKEN_ENV ,
3134 type DaemonWorkerDescriptor ,
@@ -94,6 +97,7 @@ const fixturePath = resolve(__dirname, "../../fixtures/eng-4600-supervisor-fixtu
9497const fauxExtensionPath = resolve ( __dirname , "../../fixtures/eng-4600-faux-extension.ts" ) ;
9598const cliPath = resolve ( __dirname , "../../../src/cli.ts" ) ;
9699const tsxPath = resolve ( __dirname , "../../../../../node_modules/tsx/dist/cli.mjs" ) ;
100+ const tsxLoaderPath = resolve ( __dirname , "../../../../../node_modules/tsx/dist/loader.mjs" ) ;
97101const tsconfigPath = resolve ( __dirname , "../../../../../tsconfig.json" ) ;
98102const supervisorRegistryDirEnv = "PRIME_AGENT_INTERNAL_DAEMON_SUPERVISOR_REGISTRY_DIR" ;
99103const handles = new Set < ProcessHandle > ( ) ;
@@ -174,10 +178,11 @@ function spawnStandaloneWorker(
174178 token : string ,
175179 extraEnv : NodeJS . ProcessEnv = { } ,
176180) : ProcessHandle {
177- return trackProcess (
181+ // The tsx CLI wrapper drops fd 3 when it relaunches Node, so load tsx in-process.
182+ const worker = trackProcess (
178183 spawn (
179184 paths . executablePath ,
180- [ tsxPath , cliPath , "--mode" , "daemon" , "--daemon-socket" , workerSocketPath , "--offline" ] ,
185+ [ "--import" , tsxLoaderPath , cliPath , "--mode" , "daemon" , "--daemon-socket" , workerSocketPath , "--offline" ] ,
181186 {
182187 cwd : paths . agentDir ,
183188 env : {
@@ -188,15 +193,22 @@ function spawnStandaloneWorker(
188193 [ DAEMON_WORKER_ROLE_ENV ] : "1" ,
189194 [ DAEMON_WORKER_TOKEN_ENV ] : token ,
190195 [ DAEMON_WORKER_ACTIVE_SESSION_ID_ENV ] : "eng-4603-worker" ,
196+ [ DAEMON_WORKER_STARTUP_GATE_FD_ENV ] : "3" ,
191197 [ DAEMON_WORKER_SUPERVISOR_SOCKET_ENV ] : paths . socketPath ,
192198 PI_OFFLINE : "1" ,
193199 TSX_TSCONFIG_PATH : tsconfigPath ,
194200 } ,
195- stdio : [ "ignore" , "pipe" , "pipe" ] ,
201+ stdio : [ "ignore" , "pipe" , "pipe" , "pipe" ] ,
196202 } ,
197203 ) ,
198204 "worker" ,
199205 ) ;
206+ const startupGate = worker . child . stdio [ 3 ] ;
207+ if ( ! ( startupGate instanceof Writable ) ) {
208+ throw new Error ( "Standalone worker startup gate is not writable" ) ;
209+ }
210+ startupGate . end ( DAEMON_WORKER_STARTUP_GATE_COMMIT ) ;
211+ return worker ;
200212}
201213
202214function trackProcess ( child : ChildProcess , role : FixtureProcessIdentity [ "role" ] ) : ProcessHandle {
@@ -342,6 +354,9 @@ function isFixtureDescendant(pid: number, rootPid: number, processes: Map<number
342354}
343355
344356function signalFixtureProcess ( identity : FixtureProcessIdentity , signal : NodeJS . Signals ) : boolean {
357+ if ( identity . pid === process . pid ) {
358+ throw new Error ( `Refusing to signal current Vitest process ${ process . pid } during fixture cleanup` ) ;
359+ }
345360 const state = fixtureProcessState ( identity ) ;
346361 if ( state === "exited" ) return false ;
347362 if ( state === "unverified" ) {
@@ -990,7 +1005,12 @@ describe("ENG-4603 worker recovery convergence", () => {
9901005 processStartId ?: string ;
9911006 } ;
9921007 expect ( record . pid ) . toBe ( process . pid ) ;
993- expect ( record . processStartId ) . toBe ( getProcessStartId ( process . pid ) ) ;
1008+ const processStartId = record . processStartId ;
1009+ expect ( processStartId ) . toBe ( getProcessStartId ( process . pid ) ) ;
1010+ if ( ! processStartId ) throw new Error ( "Shutdown admission did not capture the Vitest process identity" ) ;
1011+ expect ( ( ) => signalFixtureProcess ( { pid : record . pid , processStartId, role : "supervisor" } , "SIGSTOP" ) ) . toThrow (
1012+ `Refusing to signal current Vitest process ${ process . pid } during fixture cleanup` ,
1013+ ) ;
9941014 const renewal = Reflect . get ( first , "renewal" ) as object | undefined ;
9951015 const refreshTimer = renewal
9961016 ? ( Reflect . get ( renewal , "refreshTimer" ) as ReturnType < typeof setInterval > | undefined )
0 commit comments