import assert from 'node:assert/strict' import { EventEmitter } from 'node:events' import { chmod, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import test from 'node:test' import { inspect } from 'node:util' import { createMacosShutdownController, readPrivateInstanceRecord, requestDevQuit, waitForPortClosed, } from './dev-control.mjs' import { windowsElevationInvocation } from './dev-service.mjs' import { buildTauriInvocation, mirrorChildExit, prepareDevRuntime, runDevAnchor, runDevCommand, terminateDetachedGroup, } from './dev.mjs' const token = 'ab'.repeat(32) const posixOnly = { skip: process.platform === 'win32' ? 'requires POSIX file metadata' : false, } test('development startup preserves the installed service state unless service mode is explicit', async () => { for (const mode of ['dev', 'trace', 'sidecar', 'service']) { const calls = [] const result = await prepareDevRuntime(mode, { ensureService: async () => { calls.push('ensure') return '/service' }, prepareService: async () => { calls.push('prepare') return '/service' }, }) assert.deepEqual(calls, [mode === 'service' ? 'ensure' : 'prepare']) assert.equal(result.mode, mode === 'service' ? 'dev' : mode) assert.equal(result.serviceDirectory, '/service') } }) test('Windows elevation passes the installer path outside PowerShell source', () => { const installer = String.raw`C:\ssp\path with spaces\clash-verge-service-install.exe` const baseEnvironment = { SYSTEMROOT: String.raw`C:\Windows` } const invocation = windowsElevationInvocation(installer, baseEnvironment) assert.equal(invocation.command, 'powershell.exe') assert.equal(invocation.args.at(-2), '-Command') assert.equal(invocation.args.includes(installer), false) assert.equal(invocation.env.CLASH_VERGE_DEV_SERVICE_INSTALLER, installer) assert.equal(baseEnvironment.CLASH_VERGE_DEV_SERVICE_INSTALLER, undefined) }) async function withTemporaryDirectory(run) { const root = await mkdtemp(join(tmpdir(), 'clash-verge-dev-control-')) try { await run(root) } finally { await rm(root, { recursive: true, force: true }) } } function requestSequence(statuses, requests) { return (options, onResponse) => { const outgoing = new EventEmitter() outgoing.setTimeout = () => outgoing outgoing.destroy = (error) => queueMicrotask(() => outgoing.emit('error', error)) outgoing.end = () => { requests.push(options) const outcome = statuses.shift() if (outcome instanceof Error) { queueMicrotask(() => outgoing.emit('error', outcome)) return } const response = new EventEmitter() response.statusCode = outcome response.resume = () => {} response.destroy = () => {} queueMicrotask(() => { onResponse(response) queueMicrotask(() => response.emit('end')) }) } return outgoing } } function responseLifecycleRequest(status, events) { return (_options, onResponse) => { const outgoing = new EventEmitter() outgoing.setTimeout = () => outgoing outgoing.destroy = (error) => queueMicrotask(() => outgoing.emit('error', error)) outgoing.end = () => { const response = new EventEmitter() response.statusCode = status response.resume = () => {} response.destroy = () => {} queueMicrotask(() => { onResponse(response) for (const event of events) { response.emit( event, event === 'error' ? new Error('response failed') : undefined, ) } }) } return outgoing } } function releasedResponseSequence(statuses, releases) { return (_options, onResponse) => { const outgoing = new EventEmitter() outgoing.setTimeout = () => outgoing outgoing.destroy = (error) => queueMicrotask(() => outgoing.emit('error', error)) outgoing.end = () => { const response = new EventEmitter() response.statusCode = statuses.shift() response.resume = () => {} response.destroy = () => { releases.push(response.statusCode) response.emit('aborted') response.emit('error', new Error('destroyed response')) response.emit('close') } queueMicrotask(() => onResponse(response)) } return outgoing } } function createChild(pid) { const child = new EventEmitter() child.pid = pid return child } function flushTasks() { return new Promise((resolve) => setImmediate(resolve)) } test( 'private instance record must be an owned 0600 ordinary file', posixOnly, async () => { await withTemporaryDirectory(async (root) => { const recordPath = join(root, 'singleton-instance.json') await writeFile(recordPath, JSON.stringify({ port: 42123, token }), { mode: 0o600, }) assert.deepEqual( await readPrivateInstanceRecord(recordPath, process.getuid()), { port: 42123, token, }, ) await chmod(recordPath, 0o644) await assert.rejects( readPrivateInstanceRecord(recordPath, process.getuid()), /0600/, ) }) }, ) test('private instance record rejects a symbolic link', posixOnly, async () => { await withTemporaryDirectory(async (root) => { const targetPath = join(root, 'target.json') const recordPath = join(root, 'singleton-instance.json') await writeFile(targetPath, JSON.stringify({ port: 42123, token }), { mode: 0o600, }) await symlink(targetPath, recordPath) await assert.rejects( readPrivateInstanceRecord(recordPath, process.getuid()), ) }) }) test('private instance record rejects the wrong owner', posixOnly, async () => { await withTemporaryDirectory(async (root) => { const recordPath = join(root, 'singleton-instance.json') await writeFile(recordPath, JSON.stringify({ port: 42123, token }), { mode: 0o600, }) await assert.rejects( readPrivateInstanceRecord(recordPath, process.getuid() + 1), /owner/, ) }) }) test('private instance record rejects invalid ports', posixOnly, async (t) => { for (const port of [0, 65536, 1.5, '42123']) { await t.test(String(port), async () => { await withTemporaryDirectory(async (root) => { const recordPath = join(root, 'singleton-instance.json') await writeFile(recordPath, JSON.stringify({ port, token }), { mode: 0o600, }) await assert.rejects( readPrivateInstanceRecord(recordPath, process.getuid()), /port/, ) }) }) } }) test( 'private instance record rejects tokens outside the lowercase 64-character format', posixOnly, async (t) => { for (const invalidToken of [ 'ab'.repeat(31), 'AB'.repeat(32), 'z'.repeat(64), ]) { await t.test( invalidToken.length === 64 ? invalidToken.slice(0, 2) : 'short', async () => { await withTemporaryDirectory(async (root) => { const recordPath = join(root, 'singleton-instance.json') await writeFile( recordPath, JSON.stringify({ port: 42123, token: invalidToken }), { mode: 0o600 }, ) await assert.rejects( readPrivateInstanceRecord(recordPath, process.getuid()), /token/, ) }) }, ) } }, ) test('requestDevQuit posts only to loopback and retries 503', async () => { const requests = [] const record = { port: 42123, token } assert.deepEqual( await requestDevQuit(record, { requestImpl: requestSequence([503, 202], requests), retryDelayMs: 1, timeoutMs: 1_000, }), record, ) assert.equal(requests.length, 2) for (const options of requests) { assert.equal(options.host, '127.0.0.1') assert.equal(options.port, 42123) assert.equal(options.method, 'POST') assert.equal(options.path, '/commands/dev/quit') assert.equal(options.headers['X-Instance-Token'], token) } }) test('requestDevQuit retries a missing record source and returns the accepted record', async () => { let reads = 0 const record = await requestDevQuit( async () => { reads += 1 if (reads === 1) { const error = new Error('missing') error.code = 'ENOENT' throw error } return { port: 42123, token } }, { requestImpl: requestSequence([202], []), retryDelayMs: 1, timeoutMs: 1_000, }, ) assert.deepEqual(record, { port: 42123, token }) assert.equal(reads, 2) }) test('requestDevQuit errors never disclose the instance token', async () => { await assert.rejects( requestDevQuit( { port: 42123, token }, { requestImpl: requestSequence([500], []) }, ), (error) => { assert.equal(error.message.includes(token), false) assert.match(error.message, /500/) return true }, ) }) test('non-transient request errors preserve only a token-safe error cause', async () => { const unsafe = new Error(`permission denied for token ${token}`) unsafe.code = 'EACCES' unsafe.options = { headers: { 'X-Instance-Token': token } } let resultError await assert.rejects( requestDevQuit( { port: 42123, token }, { requestImpl: requestSequence([unsafe], []) }, ), (error) => { resultError = error return true }, ) const rendered = inspect(resultError, { depth: null }) assert.equal(rendered.includes(token), false) assert.match(resultError.cause.message, /EACCES/) assert.notEqual(resultError.cause, unsafe) }) test('requestDevQuit settles from response headers and consumes shutdown lifecycle events', async () => { const record = { port: 42123, token } for (const events of [ ['aborted', 'close'], ['error', 'close'], ]) { assert.deepEqual( await requestDevQuit(record, { requestImpl: responseLifecycleRequest(202, events), }), record, ) } }) test('requestDevQuit actively releases 503 and 202 responses after headers', async () => { const releases = [] assert.deepEqual( await requestDevQuit( { port: 42123, token }, { requestImpl: releasedResponseSequence([503, 202], releases), retryDelayMs: 1, timeoutMs: 100, }, ), { port: 42123, token }, ) assert.deepEqual(releases, [503, 202]) }) test('requestDevQuit retries transient connection errors without disclosing the token', async () => { const transient = new Error(`connect failed with secret ${token}`) transient.code = 'ECONNREFUSED' const requests = [] assert.deepEqual( await requestDevQuit( { port: 42123, token }, { requestImpl: requestSequence([transient, 202], requests), retryDelayMs: 1, timeoutMs: 100, }, ), { port: 42123, token }, ) assert.equal(requests.length, 2) }) test('requestDevQuit bounds ENOENT, 503, and transient-error retries', async (t) => { const cases = [ { name: 'ENOENT', source: async () => { const error = new Error('missing') error.code = 'ENOENT' throw error }, requestImpl: requestSequence([], []), }, { name: '503', source: { port: 42123, token }, requestImpl: requestSequence(Array(20).fill(503), []), }, { name: 'transient error', source: { port: 42123, token }, requestImpl: requestSequence( Array.from({ length: 20 }, () => { const error = new Error(`reset ${token}`) error.code = 'ECONNRESET' return error }), [], ), }, ] for (const entry of cases) { await t.test(entry.name, async () => { await assert.rejects( requestDevQuit(entry.source, { requestImpl: entry.requestImpl, retryDelayMs: 1, timeoutMs: 5, }), (error) => { assert.match(error.message, /timed out/) assert.equal(error.message.includes(token), false) return true }, ) }) } }) test('waitForPortClosed resolves only after the loopback connection probe fails', async () => { let probes = 0 await waitForPortClosed(42123, { probePort: async () => { probes += 1 return probes < 3 }, retryDelayMs: 1, timeoutMs: 1_000, }) assert.equal(probes, 3) }) test('waitForPortClosed gives each probe only the remaining absolute budget', async () => { let now = 1_000 const budgets = [] await assert.rejects( waitForPortClosed(42123, { now: () => now, probePort: async (_port, budget) => { budgets.push(budget) return true }, retryDelayMs: 3, sleep: async (milliseconds) => { now += milliseconds }, timeoutMs: 10, }), /timed out/, ) assert.deepEqual(budgets, [10, 7, 4, 1]) }) test('first SIGINT finalizes the group before exiting 130', async () => { const calls = [] const controller = createMacosShutdownController({ requestQuit: async () => { calls.push('request') return { port: 42123 } }, waitForPortClosed: async () => { calls.push('closed') }, waitForChildExit: async () => { calls.push('child') }, terminateGroup: () => calls.push('terminate'), exit: (code) => calls.push(`exit:${code}`), }) await controller.handleSigint() assert.deepEqual(calls, [ 'request', 'closed', 'child', 'terminate', 'exit:130', ]) }) test('first timeout preserves the group and second SIGINT forces it exactly once', async () => { const calls = [] const controller = createMacosShutdownController({ requestQuit: async () => { throw new Error('timed out') }, waitForPortClosed: async () => {}, waitForChildExit: async () => {}, terminateGroup: () => calls.push('terminate'), exit: (code) => calls.push(`exit:${code}`), report: (message) => calls.push(message), }) await controller.handleSigint() assert.equal(calls.includes('terminate'), false) assert.equal( calls.some((call) => String(call).includes('timed out')), true, ) await controller.handleSigint() await controller.handleSigint() assert.equal(calls.filter((call) => call === 'terminate').length, 1) }) test('second SIGINT terminates the group while the first request is still pending', async () => { let releaseRequest const requestPending = new Promise((resolve) => { releaseRequest = resolve }) const calls = [] const controller = createMacosShutdownController({ requestQuit: async () => requestPending, waitForPortClosed: async () => {}, waitForChildExit: async () => {}, terminateGroup: () => calls.push('terminate'), exit: (code) => calls.push(`exit:${code}`), }) const graceful = controller.handleSigint() await controller.handleSigint() assert.deepEqual(calls, ['terminate', 'exit:130']) releaseRequest({ port: 42123 }) await graceful }) test('third SIGINT shares the in-progress forced termination', async () => { let finishTermination const terminating = new Promise((resolve) => { finishTermination = resolve }) const calls = [] const controller = createMacosShutdownController({ requestQuit: async () => new Promise(() => {}), waitForPortClosed: async () => {}, waitForChildExit: async () => {}, terminateGroup: async () => { calls.push('terminate') await terminating }, exit: (code) => calls.push(`exit:${code}`), }) void controller.handleSigint() const forced = controller.handleSigint() const repeated = controller.handleSigint() await flushTasks() assert.deepEqual(calls, ['terminate']) finishTermination() assert.equal(await forced, true) assert.equal(await repeated, true) assert.deepEqual(calls, ['terminate', 'exit:130']) }) test('trace invocation preserves the existing Rust flags and Tauri arguments', () => { const invocation = buildTauriInvocation('trace', { PATH: '/bin' }, 'darwin') assert.deepEqual(invocation.args, [ 'exec', 'tauri', 'dev', '-f', 'verge-dev', 'tokio-trace', ]) assert.equal(invocation.env.RUST_BACKTRACE, 'full') assert.equal(invocation.env.RUSTFLAGS, '--cfg tokio_unstable') assert.equal(invocation.detached, true) }) test('normal dev invocation preserves arguments and remains direct off macOS', () => { const invocation = buildTauriInvocation('dev', { PATH: '/bin' }, 'linux') assert.equal(invocation.command, 'pnpm') assert.deepEqual(invocation.args, ['exec', 'tauri', 'dev', '-f', 'verge-dev']) assert.deepEqual(invocation.env, { PATH: '/bin', RUST_BACKTRACE: 'full' }) assert.equal(invocation.detached, false) }) test('sidecar invocation explicitly enables only the development sidecar feature', () => { const invocation = buildTauriInvocation('sidecar', { PATH: '/bin' }, 'win32') assert.deepEqual(invocation.args, [ 'exec', 'tauri', 'dev', '-f', 'verge-dev,dev-sidecar', ]) assert.equal(invocation.detached, false) }) test('macOS parent launches a detached Node anchor with inherited stdio and IPC', async () => { const anchor = createChild(4321) let spawned const running = runDevCommand('trace', { environment: { HOME: '/tmp/home', PATH: '/bin' }, installSigint: () => () => {}, platform: 'darwin', spawnChild: (command, args, options) => { spawned = { command, args, options } queueMicrotask(() => { anchor.emit('spawn') anchor.emit('message', { type: 'ready' }) }) return anchor }, }) await running assert.equal(spawned.command, process.execPath) assert.deepEqual(spawned.args.slice(-2), ['--anchor', 'trace']) assert.equal(spawned.options.detached, true) assert.deepEqual(spawned.options.stdio, [ 'inherit', 'inherit', 'inherit', 'ipc', ]) }) test('anchor launches the exact inner command non-detached and stays for release', async () => { const inner = createChild(8765) const sent = [] const exits = [] let handleMessage let handleSigterm let spawned const starting = runDevAnchor('trace', { environment: { PATH: '/bin' }, exit: (code) => exits.push(code), installDisconnect: () => () => {}, installMessage: (handler) => { handleMessage = handler return () => {} }, installSigterm: (handler) => { handleSigterm = handler return () => {} }, send: (message) => sent.push(message), spawnChild: (command, args, options) => { spawned = { command, args, options } queueMicrotask(() => inner.emit('spawn')) return inner }, }) await starting assert.equal(spawned.command, 'pnpm') assert.deepEqual(spawned.args, [ 'exec', 'tauri', 'dev', '-f', 'verge-dev', 'tokio-trace', ]) assert.equal(spawned.options.detached, false) assert.equal(spawned.options.stdio, 'inherit') assert.deepEqual(sent, [{ type: 'ready' }]) handleSigterm() assert.deepEqual(exits, []) inner.emit('exit', 0, null) await flushTasks() assert.deepEqual(sent.at(-1), { type: 'inner-result', code: 0, signal: null, }) assert.deepEqual(exits, []) handleMessage({ type: 'release' }) assert.deepEqual(exits, [0]) }) test('anchor cleans its own group after parent IPC disconnect', async () => { const inner = createChild(8765) const calls = [] let handleDisconnect const starting = runDevAnchor('dev', { environment: { PATH: '/bin' }, exit: (code) => calls.push(`exit:${code}`), installDisconnect: (handler) => { handleDisconnect = handler return () => {} }, installMessage: () => () => {}, installSigterm: () => () => {}, ownPid: 4321, send: () => {}, spawnChild: () => { queueMicrotask(() => inner.emit('spawn')) return inner }, terminateGroup: async (pid) => calls.push(`terminate:${pid}`), }) await starting handleDisconnect() await flushTasks() assert.deepEqual(calls, ['terminate:4321', 'exit:1']) }) test('first finalize and second SIGINT share one group finalization', async () => { let finishFinalize const finalizing = new Promise((resolve) => { finishFinalize = resolve }) const calls = [] const controller = createMacosShutdownController({ exit: (code) => calls.push(`exit:${code}`), finalizeGroup: async () => { calls.push('finalize') await finalizing }, requestQuit: async () => ({ port: 42123 }), terminateGroup: async () => calls.push('force'), waitForChildExit: async () => {}, waitForPortClosed: async () => {}, }) const graceful = controller.handleSigint() await flushTasks() const forced = controller.handleSigint() const repeated = controller.handleSigint() assert.deepEqual(calls, ['finalize']) finishFinalize() await Promise.all([graceful, forced, repeated]) assert.equal(calls.filter((call) => call === 'finalize').length, 1) assert.equal(calls.includes('force'), false) assert.deepEqual( calls.filter((call) => call.startsWith('exit:')), ['exit:130'], ) }) test('graceful shutdown shares one absolute ten-second budget', async () => { let now = 1_000 const budgets = [] const controller = createMacosShutdownController({ exit: () => {}, finalizeGroup: async (budget) => { budgets.push(['finalize', budget]) now += 500 }, now: () => now, requestQuit: async (budget) => { budgets.push(['request', budget]) now += 3_000 return { port: 42123 } }, terminateGroup: async () => {}, timeoutMs: 10_000, waitForChildExit: async (budget) => { budgets.push(['inner', budget]) now += 4_000 }, waitForPortClosed: async (_port, budget) => { budgets.push(['endpoint', budget]) now += 2_000 }, }) await controller.handleSigint() assert.deepEqual(budgets, [ ['request', 10_000], ['endpoint', 7_000], ['inner', 5_000], ['finalize', 1_000], ]) }) test('macOS validates HOME before spawning', async () => { let spawned = false await assert.rejects( runDevCommand('dev', { environment: { PATH: '/bin' }, platform: 'darwin', spawnChild: () => { spawned = true }, }), /HOME/, ) assert.equal(spawned, false) }) test('macOS rejects spawn failure and unavailable PID without group signals', async (t) => { for (const scenario of ['error', 'missing pid']) { await t.test(scenario, async () => { const child = createChild(undefined) const groupSignals = [] const start = runDevCommand('dev', { environment: { HOME: '/tmp/home', PATH: '/bin' }, installSigint: () => () => {}, platform: 'darwin', signalGroup: (...args) => groupSignals.push(args), spawnChild: () => { queueMicrotask(() => { if (scenario === 'error') child.emit('error', new Error('spawn failed')) else child.emit('spawn') }) return child }, }) await assert.rejects(start, scenario === 'error' ? /spawn failed/ : /PID/) assert.deepEqual(groupSignals, []) }) } }) test('SIGINT received during macOS spawn setup waits for a validated child PID', async () => { const child = createChild(4321) let handleSigint const calls = [] const starting = runDevCommand('dev', { environment: { HOME: '/tmp/home', PATH: '/bin' }, exit: (code) => calls.push(`exit:${code}`), installSigint: (handler) => { handleSigint = handler return () => {} }, platform: 'darwin', requestQuit: async () => new Promise(() => {}), spawnChild: () => child, terminateGroup: async (pid) => calls.push(`terminate:${pid}`), }) await flushTasks() handleSigint() handleSigint() assert.deepEqual(calls, []) child.emit('spawn') child.emit('message', { type: 'ready' }) await starting await flushTasks() assert.deepEqual(calls, ['terminate:4321', 'exit:130']) }) test('inner leader exit leaves the anchor available for lingering forced group cleanup', async () => { const child = createChild(4321) let handleSigint let finishTermination const terminating = new Promise((resolve) => { finishTermination = resolve }) const calls = [] const started = runDevCommand('dev', { environment: { HOME: '/tmp/home', PATH: '/bin' }, exit: (code) => calls.push(`exit:${code}`), installSigint: (handler) => { handleSigint = handler return () => {} }, mirrorResult: (result) => calls.push(`mirror:${result.code}:${result.signal}`), platform: 'darwin', report: (message) => calls.push(message), requestQuit: async () => { throw new Error('timed out') }, spawnChild: () => { queueMicrotask(() => { child.emit('spawn') child.emit('message', { type: 'ready' }) }) return child }, terminateGroup: async () => { calls.push('terminate') await terminating }, }) await started handleSigint() await flushTasks() child.emit('message', { type: 'inner-result', code: 0, signal: null, }) await flushTasks() assert.equal( calls.some((call) => call.startsWith('mirror:')), false, ) handleSigint() handleSigint() await flushTasks() assert.equal(calls.filter((call) => call === 'terminate').length, 1) assert.equal(calls.includes('exit:130'), false) finishTermination() await flushTasks() assert.equal(calls.filter((call) => call === 'exit:130').length, 1) }) test('unexpected anchor exit makes delayed force cleanup fail closed', async () => { const anchor = createChild(4321) let handleSigint const calls = [] await runDevCommand('dev', { environment: { HOME: '/tmp/home', PATH: '/bin' }, exit: (code) => calls.push(`exit:${code}`), installSigint: (handler) => { handleSigint = handler return () => {} }, platform: 'darwin', report: (message) => calls.push(`report:${message}`), requestQuit: async () => { throw new Error('timed out') }, signalGroup: (pid, signal) => calls.push(`signal:${pid}:${signal}`), spawnChild: () => { queueMicrotask(() => { anchor.emit('spawn') anchor.emit('message', { type: 'ready' }) }) return anchor }, }) handleSigint() await flushTasks() anchor.emit('exit', 1, null) await flushTasks() handleSigint() await flushTasks() assert.equal( calls.some((call) => call.startsWith('signal:')), false, ) assert.equal( calls.some((call) => call.includes('unsafe')), true, ) assert.deepEqual( calls.filter((call) => call.startsWith('exit:')), ['exit:1'], ) }) test('normal inner completion releases the anchor before mirroring the result', async () => { const anchor = createChild(4321) const calls = [] anchor.send = (message, callback) => { calls.push(message.type) queueMicrotask(() => { callback?.() anchor.emit('exit', 0, null) }) } await runDevCommand('dev', { environment: { HOME: '/tmp/home', PATH: '/bin' }, installSigint: () => () => {}, mirrorResult: ({ code, signal }) => calls.push(`mirror:${code}:${signal}`), platform: 'darwin', signalGroup: (pid, signal) => { calls.push(`probe:${pid}:${signal}`) const error = new Error('group absent') error.code = 'ESRCH' throw error }, spawnChild: () => { queueMicrotask(() => { anchor.emit('spawn') anchor.emit('message', { type: 'ready' }) }) return anchor }, }) anchor.emit('message', { type: 'inner-result', code: 0, signal: null, }) await flushTasks() await flushTasks() assert.deepEqual(calls, ['release', 'probe:-4321:0', 'mirror:0:null']) }) test('normal release fails closed while the original group still exists', async () => { const anchor = createChild(4321) const calls = [] anchor.send = (message, callback) => { calls.push(message.type) queueMicrotask(() => { callback?.() anchor.emit('exit', 0, null) }) } await runDevCommand('dev', { environment: { HOME: '/tmp/home', PATH: '/bin' }, exit: (code) => calls.push(`exit:${code}`), installSigint: () => () => {}, mirrorResult: () => calls.push('mirror'), platform: 'darwin', report: (message) => calls.push(`report:${message}`), signalGroup: (pid, signal) => calls.push(`probe:${pid}:${signal}`), spawnChild: () => { queueMicrotask(() => { anchor.emit('spawn') anchor.emit('message', { type: 'ready' }) }) return anchor }, }) anchor.emit('message', { type: 'inner-result', code: 0, signal: null, }) await flushTasks() await flushTasks() assert.deepEqual(calls, [ 'release', 'probe:-4321:0', 'report:Dev command failed: dev process group still exists after anchor exit', 'exit:1', ]) }) test('failed forced cleanup is shared, reported once, and never mirrors child exit', async () => { const child = createChild(4321) let handleSigint const calls = [] await runDevCommand('dev', { environment: { HOME: '/tmp/home', PATH: '/bin' }, exit: (code) => calls.push(`exit:${code}`), installSigint: (handler) => { handleSigint = handler return () => {} }, mirrorResult: () => calls.push('mirror'), platform: 'darwin', report: (message) => calls.push(`report:${message}`), requestQuit: async () => new Promise(() => {}), spawnChild: () => { queueMicrotask(() => { child.emit('spawn') child.emit('message', { type: 'ready' }) }) return child }, terminateGroup: async () => { calls.push('terminate') throw new Error('forced cleanup failed') }, }) handleSigint() handleSigint() handleSigint() child.emit('exit', 0, null) await flushTasks() await flushTasks() assert.equal(calls.filter((call) => call === 'terminate').length, 1) assert.equal(calls.filter((call) => call.startsWith('report:')).length, 1) assert.deepEqual( calls.filter((call) => call.startsWith('exit:')), ['exit:1'], ) assert.equal(calls.includes('mirror'), false) }) test('detached group termination waits after TERM, then treats KILL ESRCH as success', async () => { let now = 0 const calls = [] await terminateDetachedGroup(4321, { graceMs: 100, now: () => now, signalGroup: (pid, signal) => { calls.push(`${pid}:${signal}`) if (signal === 'SIGKILL') { const error = new Error('gone') error.code = 'ESRCH' throw error } }, sleep: async (milliseconds) => { calls.push(`sleep:${milliseconds}`) now += milliseconds }, }) assert.deepEqual(calls, [ '-4321:SIGTERM', 'sleep:50', '-4321:0', 'sleep:50', '-4321:SIGKILL', ]) }) test('non-macOS wiring mirrors child exit codes and signals without installing SIGINT', async (t) => { for (const result of [ { code: 7, signal: null }, { code: null, signal: 'SIGTERM' }, ]) { await t.test( result.code === null ? result.signal : String(result.code), async () => { const child = createChild(4321) const calls = [] const running = runDevCommand('dev', { environment: { PATH: '/bin' }, installSigint: () => { throw new Error('must not install SIGINT') }, mirrorResult: (childResult) => mirrorChildExit(childResult, { exit: (code) => calls.push(`exit:${code}`), signalSelf: (signal) => calls.push(`signal:${signal}`), }), platform: 'linux', spawnChild: () => { queueMicrotask(() => child.emit('exit', result.code, result.signal)) return child }, }) await running assert.deepEqual(calls, [ result.code === null ? 'signal:SIGTERM' : 'exit:7', ]) }, ) } }) test('non-macOS spawn errors consume every lifecycle rejection', async () => { const child = createChild(undefined) const unhandled = [] const captureUnhandled = (error) => unhandled.push(error) process.prependListener('unhandledRejection', captureUnhandled) try { const running = runDevCommand('dev', { environment: { PATH: '/bin' }, platform: 'linux', spawnChild: () => { queueMicrotask(() => child.emit('error', new Error('spawn failed'))) return child }, }) await assert.rejects(running, /spawn failed/) await flushTasks() assert.deepEqual(unhandled, []) } finally { process.removeListener('unhandledRejection', captureUnhandled) } })