From a205c896c557f83dac646e8a63cd3e870ec5484b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:32:43 -0700 Subject: [PATCH] test(crash-reporting): detach the Windows survival fixture and observe real spawns --- src/main/windows-live-tree-kill.win32.test.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main/windows-live-tree-kill.win32.test.ts b/src/main/windows-live-tree-kill.win32.test.ts index 053eb630b88..45563e82c89 100644 --- a/src/main/windows-live-tree-kill.win32.test.ts +++ b/src/main/windows-live-tree-kill.win32.test.ts @@ -1,7 +1,8 @@ import { existsSync, mkdtempSync, readFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { spawn, type ChildProcess } from 'node:child_process' +import { spawn, ChildProcess } from 'node:child_process' +import { subscribe, unsubscribe } from 'node:diagnostics_channel' import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' import { setAppEnvironment, type AppEnvironment } from '../shared/app-environment' import { setProcessTreeKillGate } from '../shared/child-process/process-tree-kill-gate' @@ -70,6 +71,18 @@ let markerDirectory = '' let markerSequence = 0 const spawnedRoots: ChildProcess[] = [] const spawnedLeaves: number[] = [] +const observedSpawns: ChildProcess[] = [] + +function observeSpawn(message: unknown): void { + if ( + typeof message === 'object' && + message !== null && + 'process' in message && + message.process instanceof ChildProcess + ) { + observedSpawns.push(message.process) + } +} /** A real root with a real grandchild; the grandchild reports its pid on disk. */ async function spawnLiveTree(): Promise<{ @@ -79,7 +92,8 @@ async function spawnLiveTree(): Promise<{ }> { const marker = join(markerDirectory, `leaf-${markerSequence++}.pid`) const leafSource = `require('node:fs').writeFileSync(${JSON.stringify(marker)}, String(process.pid)); setTimeout(() => {}, 600000)` - const rootSource = `require('node:child_process').spawn(process.execPath, ['-e', ${JSON.stringify(leafSource)}], { stdio: 'ignore' }); setTimeout(() => {}, 600000)` + // Non-detached Windows children can die with the root's libuv Job Object. + const rootSource = `require('node:child_process').spawn(process.execPath, ['-e', ${JSON.stringify(leafSource)}], { stdio: 'ignore', detached: true, windowsHide: true }); setTimeout(() => {}, 600000)` const child = spawn(process.execPath, ['-e', rootSource], { stdio: 'ignore', windowsHide: true @@ -101,9 +115,12 @@ describeOnWindows('own-Chromium gate against real Windows process trees', () => orcaChromiumPids = [] setAppEnvironment(appEnvironment()) installMainProcessTreeKillGate() + observedSpawns.length = 0 + subscribe('child_process', observeSpawn) }) afterEach(async () => { + unsubscribe('child_process', observeSpawn) orcaChromiumPids = [] for (const leafPid of spawnedLeaves.splice(0)) { await terminateWindowsProcessTree(leafPid, { site: 'live-tree-kill-cleanup' }) @@ -148,6 +165,8 @@ describeOnWindows('own-Chromium gate against real Windows process trees', () => // The fallback every gated site runs after a refusal. child.kill('SIGKILL') expect(await waitFor(() => !isAlive(rootPid))).toBe(true) + // Let root-owned job cleanup finish before asserting independent survival. + await sleep(250) // Disclosed asymmetry: a refusal orphans descendants rather than reaping them. expect(isAlive(leafPid)).toBe(true) }) @@ -155,18 +174,23 @@ describeOnWindows('own-Chromium gate against real Windows process trees', () => it('signalProcessTree refused: the root goes by handle and the barrier reports unverified', async () => { const { child, rootPid, leafPid } = await spawnLiveTree() orcaChromiumPids = [rootPid] + observedSpawns.length = 0 await expect(signalProcessTree(child, 'SIGKILL')).resolves.toBe(false) + expect(observedSpawns).toHaveLength(0) expect(await waitFor(() => !isAlive(rootPid))).toBe(true) + await sleep(250) expect(isAlive(leafPid)).toBe(true) }) it('signalProcessTree admitted: the whole tree goes and the barrier reports verified', async () => { const { child, rootPid, leafPid } = await spawnLiveTree() + observedSpawns.length = 0 await expect(signalProcessTree(child, 'SIGKILL')).resolves.toBe(true) + expect(observedSpawns.map((child) => child.spawnfile)).toEqual(['taskkill']) expect(await waitFor(() => !isAlive(rootPid))).toBe(true) expect(await waitFor(() => !isAlive(leafPid))).toBe(true) })