import { EventEmitter } from 'node:events'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const originalPlatform = process.platform; const { mockExec, mockCreateServer, } = vi.hoisted(() => ({ mockExec: vi.fn(), mockCreateServer: vi.fn(), })); vi.mock('electron', () => ({ app: { isPackaged: false, getPath: () => '/tmp', }, utilityProcess: {}, })); vi.mock('child_process', () => ({ exec: mockExec, execSync: vi.fn(), spawn: vi.fn(), default: { exec: mockExec, execSync: vi.fn(), spawn: vi.fn(), }, })); vi.mock('net', () => ({ createServer: mockCreateServer, })); class MockUtilityChild extends EventEmitter { pid?: number; kill = vi.fn(); constructor(pid?: number) { super(); this.pid = pid; } } function setPlatform(platform: string): void { Object.defineProperty(process, 'platform', { value: platform, writable: true }); } describe('gateway supervisor process cleanup', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); mockExec.mockImplementation((_cmd: string, _opts: object, cb: (err: Error | null, stdout: string) => void) => { cb(null, ''); return {} as never; }); mockCreateServer.mockImplementation(() => { const handlers = new Map void>(); return { once(event: string, callback: (...args: unknown[]) => void) { handlers.set(event, callback); return this; }, listen() { queueMicrotask(() => handlers.get('listening')?.()); return this; }, close(callback?: () => void) { callback?.(); }, }; }); }); afterEach(() => { Object.defineProperty(process, 'platform', { value: originalPlatform, writable: true }); }); it('uses taskkill tree strategy for owned process on Windows', async () => { setPlatform('win32'); const child = new MockUtilityChild(4321); const { terminateOwnedGatewayProcess } = await import('@electron/gateway/supervisor'); const stopPromise = terminateOwnedGatewayProcess(child as unknown as Electron.UtilityProcess); child.emit('exit', 0); await stopPromise; await vi.waitFor(() => { expect(mockExec).toHaveBeenCalledWith( 'taskkill /F /PID 4321 /T', expect.objectContaining({ timeout: 5000, windowsHide: true }), expect.any(Function), ); }); expect(child.kill).not.toHaveBeenCalled(); }); it('uses direct child.kill for owned process on non-Windows', async () => { setPlatform('linux'); const child = new MockUtilityChild(9876); const { terminateOwnedGatewayProcess } = await import('@electron/gateway/supervisor'); const stopPromise = terminateOwnedGatewayProcess(child as unknown as Electron.UtilityProcess); child.emit('exit', 0); await stopPromise; expect(child.kill).toHaveBeenCalledTimes(1); }); it('waits for port release after orphan cleanup on Windows', async () => { setPlatform('win32'); const { findExistingGatewayProcess } = await import('@electron/gateway/supervisor'); mockExec.mockImplementation((cmd: string, _opts: object, cb: (err: Error | null, stdout: string) => void) => { if (cmd.includes('netstat -ano')) { cb(null, ' TCP 127.0.0.1:18789 0.0.0.0:0 LISTENING 4321\n'); return {} as never; } cb(null, ''); return {} as never; }); const result = await findExistingGatewayProcess({ port: 18789 }); expect(result).toBeNull(); expect(mockExec).toHaveBeenCalledWith( expect.stringContaining('taskkill /F /PID 4321 /T'), expect.objectContaining({ timeout: 5000, windowsHide: true }), expect.any(Function), ); expect(mockCreateServer).toHaveBeenCalled(); }); });