fix(background): exit after fatal cleanup

This commit is contained in:
auyua9
2026-05-01 16:04:03 +08:00
parent 606f93e0e1
commit 875fffb7d1
2 changed files with 17 additions and 16 deletions
@@ -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()
}
@@ -3,11 +3,18 @@ import { log } from "../../shared"
type ProcessCleanupSignal = NodeJS.Signals | "beforeExit" | "exit"
type ProcessCleanupErrorEvent = "uncaughtException" | "unhandledRejection"
function scheduleForcedExit(cleanupResult: void | Promise<void>, exitCode: number): void {
function scheduleForcedExit(
cleanupResult: void | Promise<void>,
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