From 875fffb7d1b8a9c81f8bda27207023c6ebeab82b Mon Sep 17 00:00:00 2001 From: auyua9 Date: Fri, 1 May 2026 16:04:03 +0800 Subject: [PATCH] fix(background): exit after fatal cleanup --- .../background-agent/process-cleanup.test.ts | 22 +++++++------------ .../background-agent/process-cleanup.ts | 11 ++++++++-- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/features/background-agent/process-cleanup.test.ts b/src/features/background-agent/process-cleanup.test.ts index a9ce35435..6b79d594e 100644 --- a/src/features/background-agent/process-cleanup.test.ts +++ b/src/features/background-agent/process-cleanup.test.ts @@ -135,9 +135,7 @@ describe("#given process cleanup registration", () => { }) test("#given two managers registered #when uncaughtException fires #then both shutdowns called", async () => { - const exitSpy = spyOn(process, "exit").mockImplementation((code?: number): never => { - throw new Error(`Unexpected process.exit(${String(code)})`) - }) + const exitSpy = spyOn(process, "exit").mockImplementation((() => undefined) as never) const shutdownOne = mock(() => {}) const shutdownTwo = mock(() => {}) const managerOne = { shutdown: shutdownOne } @@ -154,7 +152,7 @@ describe("#given process cleanup registration", () => { expect(shutdownOne).toHaveBeenCalledTimes(1) expect(shutdownTwo).toHaveBeenCalledTimes(1) expect(process.exitCode).toBe(1) - expect(exitSpy).not.toHaveBeenCalled() + expect(exitSpy).toHaveBeenCalledWith(1) } finally { exitSpy.mockRestore() } @@ -219,10 +217,8 @@ describe("#given process cleanup registration", () => { }) describe("#given uncaught exception and rejection cleanup", () => { - test("#given manager registered AND process emits uncaughtException #when event fires #then manager.shutdown() called AND process.exitCode set to 1", async () => { - const exitSpy = spyOn(process, "exit").mockImplementation((code?: number): never => { - throw new Error(`Unexpected process.exit(${String(code)})`) - }) + test("#given manager registered AND process emits uncaughtException #when event fires #then manager shuts down before process exits", async () => { + const exitSpy = spyOn(process, "exit").mockImplementation((() => undefined) as never) const shutdown = mock(() => {}) const manager = { shutdown } registeredManagers.push(manager) @@ -235,16 +231,14 @@ describe("#given process cleanup registration", () => { expect(shutdown).toHaveBeenCalledTimes(1) expect(process.exitCode).toBe(1) - expect(exitSpy).not.toHaveBeenCalled() + expect(exitSpy).toHaveBeenCalledWith(1) } finally { exitSpy.mockRestore() } }) - test("#given manager registered AND process emits unhandledRejection #when event fires #then manager.shutdown() called AND process.exitCode set to 1", async () => { - const exitSpy = spyOn(process, "exit").mockImplementation((code?: number): never => { - throw new Error(`Unexpected process.exit(${String(code)})`) - }) + test("#given manager registered AND process emits unhandledRejection #when event fires #then manager shuts down before process exits", async () => { + const exitSpy = spyOn(process, "exit").mockImplementation((() => undefined) as never) const shutdown = mock(() => {}) const manager = { shutdown } registeredManagers.push(manager) @@ -257,7 +251,7 @@ describe("#given process cleanup registration", () => { expect(shutdown).toHaveBeenCalledTimes(1) expect(process.exitCode).toBe(1) - expect(exitSpy).not.toHaveBeenCalled() + expect(exitSpy).toHaveBeenCalledWith(1) } finally { exitSpy.mockRestore() } diff --git a/src/features/background-agent/process-cleanup.ts b/src/features/background-agent/process-cleanup.ts index 20f8fab00..be4338073 100644 --- a/src/features/background-agent/process-cleanup.ts +++ b/src/features/background-agent/process-cleanup.ts @@ -3,11 +3,18 @@ import { log } from "../../shared" type ProcessCleanupSignal = NodeJS.Signals | "beforeExit" | "exit" type ProcessCleanupErrorEvent = "uncaughtException" | "unhandledRejection" -function scheduleForcedExit(cleanupResult: void | Promise, exitCode: number): void { +function scheduleForcedExit( + cleanupResult: void | Promise, + exitCode: number, + exitAfterCleanup = false, +): void { process.exitCode = exitCode const exitTimeout = setTimeout(() => process.exit(), 6000) void Promise.resolve(cleanupResult).finally(() => { clearTimeout(exitTimeout) + if (exitAfterCleanup) { + process.exit(exitCode) + } }) } @@ -32,7 +39,7 @@ function registerErrorEvent( ): (error: unknown) => void { const listener = (error: unknown) => { log(`[background-agent] ${signal} received during shutdown cleanup:`, error) - scheduleForcedExit(handler(error), 1) + scheduleForcedExit(handler(error), 1, true) } process.on(signal, listener) return listener