mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-26 16:41:49 +00:00
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -18,6 +18,7 @@ const buildProgramMock = vi.hoisted(() => vi.fn());
|
||||
const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
|
||||
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
|
||||
const registerSubCliByNameMock = vi.hoisted(() => vi.fn());
|
||||
const restoreTerminalStateMock = vi.hoisted(() => vi.fn());
|
||||
const maybeRunCliInContainerMock = vi.hoisted(() =>
|
||||
vi.fn<
|
||||
(argv: string[]) => { handled: true; exitCode: number } | { handled: false; argv: string[] }
|
||||
@@ -89,6 +90,10 @@ vi.mock("./program/register.subclis.js", () => ({
|
||||
registerSubCliByName: registerSubCliByNameMock,
|
||||
}));
|
||||
|
||||
vi.mock("../terminal/restore.js", () => ({
|
||||
restoreTerminalState: restoreTerminalStateMock,
|
||||
}));
|
||||
|
||||
describe("runCli exit behavior", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -184,4 +189,42 @@ describe("runCli exit behavior", () => {
|
||||
expect(process.exitCode).toBe(1);
|
||||
process.exitCode = exitCode;
|
||||
});
|
||||
|
||||
it("restores terminal state before uncaught CLI exits", async () => {
|
||||
buildProgramMock.mockReturnValueOnce({
|
||||
commands: [{ name: () => "status" }],
|
||||
parseAsync: vi.fn().mockResolvedValueOnce(undefined),
|
||||
});
|
||||
|
||||
const processOnSpy = vi.spyOn(process, "on");
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
||||
throw new Error(`process.exit(${String(code)})`);
|
||||
}) as typeof process.exit);
|
||||
|
||||
await runCli(["node", "openclaw", "status"]);
|
||||
|
||||
const handler = processOnSpy.mock.calls.find(([event]) => event === "uncaughtException")?.[1];
|
||||
expect(typeof handler).toBe("function");
|
||||
|
||||
try {
|
||||
expect(() => (handler as (error: unknown) => void)(new Error("boom"))).toThrow(
|
||||
"process.exit(1)",
|
||||
);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"[openclaw] Uncaught exception:",
|
||||
expect.stringContaining("boom"),
|
||||
);
|
||||
expect(restoreTerminalStateMock).toHaveBeenCalledWith("uncaught exception", {
|
||||
resumeStdinIfPaused: false,
|
||||
});
|
||||
} finally {
|
||||
if (typeof handler === "function") {
|
||||
process.off("uncaughtException", handler);
|
||||
}
|
||||
consoleErrorSpy.mockRestore();
|
||||
exitSpy.mockRestore();
|
||||
processOnSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,9 +153,13 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
// Capture all console output into structured logs while keeping stdout/stderr behavior.
|
||||
enableConsoleCapture();
|
||||
|
||||
const { buildProgram } = await import("./program.js");
|
||||
const [{ buildProgram }, { installUnhandledRejectionHandler }, { restoreTerminalState }] =
|
||||
await Promise.all([
|
||||
import("./program.js"),
|
||||
import("../infra/unhandled-rejections.js"),
|
||||
import("../terminal/restore.js"),
|
||||
]);
|
||||
const program = buildProgram();
|
||||
const { installUnhandledRejectionHandler } = await import("../infra/unhandled-rejections.js");
|
||||
|
||||
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
||||
// These log the error and exit gracefully instead of crashing without trace.
|
||||
@@ -163,6 +167,7 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("[openclaw] Uncaught exception:", formatUncaughtError(error));
|
||||
restoreTerminalState("uncaught exception", { resumeStdinIfPaused: false });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user