From c497f1a2e36864641eea32b64584ab6c0c035bd8 Mon Sep 17 00:00:00 2001 From: YayoiNanoka Date: Wed, 14 Oct 2026 10:47:24 +0800 Subject: [PATCH] fix(test): synchronize escaped descendant before supervisor fault Wait for the detached descendant's atomic readiness marker before injecting the fault through the test preload. Trigger write probes after supervisor exit so startup scheduling cannot consume a fixed delay, and clean up the escaped PID if assertions fail. Preserve failure reporting and both post-exit write assertions without changing production process termination. Generated-by: OpenAI Codex --- .../src/__tests__/owned-child-process.test.ts | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/__tests__/owned-child-process.test.ts b/packages/runtime/src/__tests__/owned-child-process.test.ts index ae91f0c4f0c..555119e8ebe 100644 --- a/packages/runtime/src/__tests__/owned-child-process.test.ts +++ b/packages/runtime/src/__tests__/owned-child-process.test.ts @@ -238,18 +238,29 @@ test('a supervisor fault is reported as a failure before its tree is stopped', { const directory = await mkdtemp(join(tmpdir(), 'maka-owned-fault-')); const late = join(directory, 'late'); const escapedLate = join(directory, 'escaped-late'); - // Throw inside the real supervisor once it has admitted the command; the - // test plays the Host on the other end of its lease channel. + const escapedReady = join(directory, 'escaped-ready'); + const probe = join(directory, 'probe'); + // The escaped descendant must exist before the supervisor discovers its + // tree. `started` only means the direct command was spawned. const preload = join(directory, 'fault.cjs'); await writeFile( preload, - `const send = process.send.bind(process); -process.send = (message, ...rest) => { - const result = send(message, ...rest); - if (message && message.kind === 'started') setImmediate(() => { throw new Error('injected fault'); }); - return result; -};`, + `process.on('message', (message) => { + if (message && message.kind === 'inject-test-fault') throw new Error('injected fault'); +});`, ); + // Arm the writes only after the supervisor exits, so startup scheduling + // cannot spend a fixed timer's budget before the fault is injected. + const writeOnProbe = (path: string) => `const fs = require('node:fs'); + const timer = setInterval(() => { + if (!fs.existsSync(${JSON.stringify(probe)})) return; + fs.writeFileSync(${JSON.stringify(path)}, 'late'); + clearInterval(timer); + }, 10); + setInterval(() => {}, 1000);`; + const escaped = `${writeOnProbe(escapedLate)} + fs.writeFileSync(${JSON.stringify(`${escapedReady}.tmp`)}, String(process.pid)); + fs.renameSync(${JSON.stringify(`${escapedReady}.tmp`)}, ${JSON.stringify(escapedReady)});`; const supervisor = fork(fileURLToPath(new URL('../owned-process-main.js', import.meta.url)), [], { execArgv: ['--require', preload], stdio: ['ignore', 'pipe', 'pipe', 'ipc'], @@ -258,6 +269,7 @@ process.send = (message, ...rest) => { const messages: Array<{ kind: string; message?: string }> = []; supervisor.on('message', (message) => messages.push(message as (typeof messages)[number])); const exited = new Promise((resolve) => supervisor.once('exit', resolve)); + let escapedPid: number | undefined; try { supervisor.send({ kind: 'launch', @@ -266,22 +278,33 @@ process.send = (message, ...rest) => { '-e', // The command also starts a descendant in its own session, outside the // supervisor's process group: only the supervisor's tree walk reaches it. - `const { spawn } = require('node:child_process'); - spawn(process.execPath, ['-e', ${JSON.stringify(`setTimeout(() => require('node:fs').writeFileSync(${JSON.stringify(escapedLate)}, 'late'), 1000); setInterval(() => {}, 1000);`)}], { detached: true, stdio: 'ignore' }).unref(); - setTimeout(() => require('node:fs').writeFileSync(${JSON.stringify(late)}, 'late'), 1000); - setInterval(() => {}, 1000);`, + `${writeOnProbe(late)} + require('node:child_process').spawn(process.execPath, ['-e', ${JSON.stringify(escaped)}], { detached: true, stdio: 'ignore' }).unref();`, ], cwd: directory, env: process.env, shell: false, inheritedFds: [], }); + const deadline = Date.now() + 5000; + while (escapedPid === undefined) { + try { + escapedPid = Number(await readFile(escapedReady, 'utf8')); + assert.ok(Number.isSafeInteger(escapedPid) && escapedPid > 0); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + assert.ok(Date.now() < deadline, 'Escaped descendant did not start'); + if (escapedPid === undefined) await delay(10); + } + supervisor.send({ kind: 'inject-test-fault' }); await exited; assert.deepEqual( messages.map((message) => message.kind), ['started', 'failed'], ); assert.equal(messages[1]?.message, 'Command supervisor failed: injected fault'); + await writeFile(probe, 'probe'); await delay(1200); await assert.rejects(readFile(late), { code: 'ENOENT' }); await assert.rejects(readFile(escapedLate), { code: 'ENOENT' }); @@ -294,6 +317,13 @@ process.send = (message, ...rest) => { } } supervisor.kill('SIGKILL'); + if (escapedPid !== undefined) { + try { + process.kill(escapedPid, 'SIGKILL'); + } catch { + /* already exited */ + } + } await rm(directory, { recursive: true, force: true }); } });