feat(background-agent): implement process cleanup for BackgroundManager

Add functionality to manage process cleanup by registering and unregistering signal listeners. This ensures that BackgroundManager instances properly shut down and remove their listeners on process exit. Introduce tests to verify listener removal after shutdown.
This commit is contained in:
Jeremy Gollehon
2026-01-14 23:11:38 -08:00
parent 4ac0fa7bb0
commit 7050d447cd
2 changed files with 95 additions and 21 deletions
+38 -1
View File
@@ -1,5 +1,6 @@
import { describe, test, expect, beforeEach } from "bun:test"
import { afterEach } from "bun:test"
import { tmpdir } from "node:os"
import type { PluginInput } from "@opencode-ai/plugin"
import type { BackgroundTask, ResumeInput } from "./types"
import { BackgroundManager } from "./manager"
@@ -167,7 +168,7 @@ function createBackgroundManager(): BackgroundManager {
prompt: async () => ({}),
},
}
return new BackgroundManager({ client, directory: "C:\\tmp" } as unknown as PluginInput)
return new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
}
function getConcurrencyManager(manager: BackgroundManager): ConcurrencyManager {
@@ -186,6 +187,18 @@ async function tryCompleteTaskForTest(manager: BackgroundManager, task: Backgrou
return (manager as unknown as { tryCompleteTask: (task: BackgroundTask, source: string) => Promise<boolean> }).tryCompleteTask(task, "test")
}
function getCleanupSignals(): Array<NodeJS.Signals | "beforeExit" | "exit"> {
const signals: Array<NodeJS.Signals | "beforeExit" | "exit"> = ["SIGINT", "SIGTERM", "beforeExit", "exit"]
if (process.platform === "win32") {
signals.push("SIGBREAK")
}
return signals
}
function getListenerCounts(signals: Array<NodeJS.Signals | "beforeExit" | "exit">): Record<string, number> {
return Object.fromEntries(signals.map((signal) => [signal, process.listenerCount(signal)]))
}
describe("BackgroundManager.getAllDescendantTasks", () => {
let manager: MockBackgroundManager
@@ -1023,3 +1036,27 @@ describe("BackgroundManager.resume concurrency key", () => {
})
})
describe("BackgroundManager process cleanup", () => {
test("should remove listeners after last shutdown", () => {
// #given
const signals = getCleanupSignals()
const baseline = getListenerCounts(signals)
const managerA = createBackgroundManager()
const managerB = createBackgroundManager()
// #when
const afterCreate = getListenerCounts(signals)
managerA.shutdown()
const afterFirstShutdown = getListenerCounts(signals)
managerB.shutdown()
const afterSecondShutdown = getListenerCounts(signals)
// #then
for (const signal of signals) {
expect(afterCreate[signal]).toBe(baseline[signal] + 1)
expect(afterFirstShutdown[signal]).toBe(baseline[signal] + 1)
expect(afterSecondShutdown[signal]).toBe(baseline[signal])
}
})
})