- Revert getMessageDir to original join(MESSAGE_STORAGE, sessionID) behavior - Fix dead subagentSessions.delete by capturing previousSessionID before tryFallbackRetry - Add .unref() to process cleanup setTimeout to prevent 6s hang on Ctrl-C - Add missing isUnstableAgent to fallback retry input mapping - Fix process-cleanup tests to use exit listener instead of SIGINT at index 0 - Swap test filenames in compaction-aware-message-resolver to exercise skip logic correctly
163 lines
5.1 KiB
TypeScript
163 lines
5.1 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"
|
|
import {
|
|
registerManagerForCleanup,
|
|
unregisterManagerForCleanup,
|
|
_resetForTesting,
|
|
} from "./process-cleanup"
|
|
|
|
describe("process-cleanup", () => {
|
|
const registeredManagers: Array<{ shutdown: () => void }> = []
|
|
const mockShutdown = mock(() => {})
|
|
|
|
const processOnCalls: Array<[string, Function]> = []
|
|
const processOffCalls: Array<[string, Function]> = []
|
|
const originalProcessOn = process.on.bind(process)
|
|
const originalProcessOff = process.off.bind(process)
|
|
|
|
beforeEach(() => {
|
|
mockShutdown.mockClear()
|
|
processOnCalls.length = 0
|
|
processOffCalls.length = 0
|
|
registeredManagers.length = 0
|
|
|
|
process.on = originalProcessOn as any
|
|
process.off = originalProcessOff as any
|
|
_resetForTesting()
|
|
|
|
process.on = ((event: string, listener: Function) => {
|
|
processOnCalls.push([event, listener])
|
|
return process
|
|
}) as any
|
|
|
|
process.off = ((event: string, listener: Function) => {
|
|
processOffCalls.push([event, listener])
|
|
return process
|
|
}) as any
|
|
})
|
|
|
|
afterEach(() => {
|
|
process.on = originalProcessOn as any
|
|
process.off = originalProcessOff as any
|
|
|
|
for (const manager of [...registeredManagers]) {
|
|
unregisterManagerForCleanup(manager)
|
|
}
|
|
})
|
|
|
|
describe("registerManagerForCleanup", () => {
|
|
test("registers signal handlers on first manager", () => {
|
|
const manager = { shutdown: mockShutdown }
|
|
registeredManagers.push(manager)
|
|
|
|
registerManagerForCleanup(manager)
|
|
|
|
const signals = processOnCalls.map(([signal]) => signal)
|
|
expect(signals).toContain("SIGINT")
|
|
expect(signals).toContain("SIGTERM")
|
|
expect(signals).toContain("beforeExit")
|
|
expect(signals).toContain("exit")
|
|
})
|
|
|
|
test("signal listener calls shutdown on registered manager", () => {
|
|
const manager = { shutdown: mockShutdown }
|
|
registeredManagers.push(manager)
|
|
|
|
registerManagerForCleanup(manager)
|
|
|
|
const exitEntry = processOnCalls.find(([signal]) => signal === "exit")
|
|
expect(exitEntry).toBeDefined()
|
|
const [, listener] = exitEntry!
|
|
listener()
|
|
|
|
expect(mockShutdown).toHaveBeenCalled()
|
|
})
|
|
|
|
test("multiple managers all get shutdown when signal fires", () => {
|
|
const shutdown1 = mock(() => {})
|
|
const shutdown2 = mock(() => {})
|
|
const shutdown3 = mock(() => {})
|
|
const manager1 = { shutdown: shutdown1 }
|
|
const manager2 = { shutdown: shutdown2 }
|
|
const manager3 = { shutdown: shutdown3 }
|
|
registeredManagers.push(manager1, manager2, manager3)
|
|
|
|
registerManagerForCleanup(manager1)
|
|
registerManagerForCleanup(manager2)
|
|
registerManagerForCleanup(manager3)
|
|
|
|
const exitEntry = processOnCalls.find(([signal]) => signal === "exit")
|
|
expect(exitEntry).toBeDefined()
|
|
const [, listener] = exitEntry!
|
|
listener()
|
|
|
|
expect(shutdown1).toHaveBeenCalledTimes(1)
|
|
expect(shutdown2).toHaveBeenCalledTimes(1)
|
|
expect(shutdown3).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test("does not re-register signal handlers for subsequent managers", () => {
|
|
const manager1 = { shutdown: mockShutdown }
|
|
const manager2 = { shutdown: mockShutdown }
|
|
registeredManagers.push(manager1, manager2)
|
|
|
|
registerManagerForCleanup(manager1)
|
|
const callsAfterFirst = processOnCalls.length
|
|
|
|
registerManagerForCleanup(manager2)
|
|
|
|
expect(processOnCalls.length).toBe(callsAfterFirst)
|
|
})
|
|
})
|
|
|
|
describe("unregisterManagerForCleanup", () => {
|
|
test("removes signal handlers when last manager unregisters", () => {
|
|
const manager = { shutdown: mockShutdown }
|
|
registeredManagers.push(manager)
|
|
|
|
registerManagerForCleanup(manager)
|
|
unregisterManagerForCleanup(manager)
|
|
registeredManagers.length = 0
|
|
|
|
const offSignals = processOffCalls.map(([signal]) => signal)
|
|
expect(offSignals).toContain("SIGINT")
|
|
expect(offSignals).toContain("SIGTERM")
|
|
expect(offSignals).toContain("beforeExit")
|
|
expect(offSignals).toContain("exit")
|
|
})
|
|
|
|
test("keeps signal handlers when other managers remain", () => {
|
|
const manager1 = { shutdown: mockShutdown }
|
|
const manager2 = { shutdown: mockShutdown }
|
|
registeredManagers.push(manager1, manager2)
|
|
|
|
registerManagerForCleanup(manager1)
|
|
registerManagerForCleanup(manager2)
|
|
|
|
unregisterManagerForCleanup(manager2)
|
|
|
|
expect(processOffCalls.length).toBe(0)
|
|
})
|
|
|
|
test("remaining managers still get shutdown after partial unregister", () => {
|
|
const shutdown1 = mock(() => {})
|
|
const shutdown2 = mock(() => {})
|
|
const manager1 = { shutdown: shutdown1 }
|
|
const manager2 = { shutdown: shutdown2 }
|
|
registeredManagers.push(manager1, manager2)
|
|
|
|
registerManagerForCleanup(manager1)
|
|
registerManagerForCleanup(manager2)
|
|
|
|
const exitEntry = processOnCalls.find(([signal]) => signal === "exit")
|
|
expect(exitEntry).toBeDefined()
|
|
const [, listener] = exitEntry!
|
|
unregisterManagerForCleanup(manager2)
|
|
|
|
listener()
|
|
|
|
expect(shutdown1).toHaveBeenCalledTimes(1)
|
|
expect(shutdown2).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|