Files
oh-my-pi/packages/coding-agent/src/modes/noninteractive-dispose.test.ts
T
roboompandcan1357 ed4ddcba6c fix(session): await session.dispose() on non-interactive exit paths
Print-mode assistant-error/aborted exit, RPC pi.shutdown() and stdin-EOF
shutdowns, and the extension command-context shutdown() called
process.exit() before (or racing) session.dispose(), skipping the bounded
browser reaper (releaseTabsForOwner) installed in dispose(). An OMP-owned
Chromium could survive the parent and reparent to PID 1.

Route all four graceful paths through the idempotent, promise-memoized
session.dispose() and await it before the final exit. The RPC
performShutdown no longer emits session_shutdown directly (dispose() emits
it), avoiding a double emit.

Fixes #5643
2026-07-16 04:29:55 +02:00

61 lines
1.9 KiB
TypeScript

/**
* Contract: the print-mode assistant-error/aborted exit path MUST run the
* awaited `session.dispose()` (which contains the bounded browser reaper
* `releaseTabsForOwner`) before terminating the process. It previously called
* `process.exit(1)` ahead of the `dispose()` at the end of `runPrintMode`, so
* an OMP-owned Chromium survived the exit (issue #5643).
*/
import { describe, expect, it, spyOn } from "bun:test";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import type { AgentSession } from "../session/agent-session";
import { runPrintMode } from "./print-mode";
/** Stand-in for `process.exit`: it terminates, so nothing after it should run. */
class ProcessExit extends Error {
constructor(readonly code: number) {
super(`process.exit(${code})`);
}
}
describe("print-mode error exit disposes the session before exit", () => {
it("disposes on the assistant-error path before process.exit(1)", async () => {
const order: string[] = [];
const errorMsg: AssistantMessage = {
role: "assistant",
content: [],
api: "openai-responses",
provider: "openai",
model: "gpt-test",
usage: {} as AssistantMessage["usage"],
stopReason: "error",
errorMessage: "boom",
timestamp: 1,
};
const session = {
extensionRunner: undefined,
subscribe: () => {},
state: { messages: [errorMsg] },
dispose: async () => {
order.push("dispose");
},
} as unknown as AgentSession;
const exitSpy = spyOn(process, "exit").mockImplementation(((code: number) => {
order.push("exit");
throw new ProcessExit(code);
}) as never);
const stderrSpy = spyOn(process.stderr, "write").mockImplementation((() => true) as never);
try {
await runPrintMode(session, { mode: "text" });
} catch (err) {
if (!(err instanceof ProcessExit)) throw err;
} finally {
exitSpy.mockRestore();
stderrSpy.mockRestore();
}
expect(order).toEqual(["dispose", "exit"]);
});
});