import process from "node:process" ;
import { CommanderError } from "commander" ;
import { beforeEach, describe, expect, it, vi } from "vitest" ;
import { runCli } from "./run-main.js" ;
const tryRouteCliMock = vi.hoisted(() => vi.fn());
const loadDotEnvMock = vi.hoisted(() => vi.fn());
const normalizeEnvMock = vi.hoisted(() => vi.fn());
const ensurePathMock = vi.hoisted(() => vi.fn());
const assertRuntimeMock = vi.hoisted(() => vi.fn());
const closeActiveMemorySearchManagersMock = vi.hoisted(() => vi.fn(async () => {}));
const hasMemoryRuntimeMock = vi.hoisted(() => vi.fn(() => false ));
const ensureTaskRegistryReadyMock = vi.hoisted(() => vi.fn());
const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn());
const outputRootHelpMock = vi.hoisted(() => vi.fn());
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false ));
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[] }
>((argv: string[]) => ({ handled: false , argv })),
);
vi.mock("./route.js" , () => ({
tryRouteCli: tryRouteCliMock,
}));
vi.mock("./container-target.js" , () => ({
maybeRunCliInContainer: maybeRunCliInContainerMock,
parseCliContainerArgs: (argv: string[]) => ({ ok: true , container: null , argv }),
}));
vi.mock("./dotenv.js" , () => ({
loadCliDotEnv: loadDotEnvMock,
}));
vi.mock("../infra/env.js" , () => ({
normalizeEnv: normalizeEnvMock,
}));
vi.mock("../infra/path-env.js" , () => ({
ensureOpenClawCliOnPath: ensurePathMock,
}));
vi.mock("../infra/runtime-guard.js" , () => ({
assertSupportedRuntime: assertRuntimeMock,
}));
vi.mock("../plugins/memory-runtime.js" , () => ({
closeActiveMemorySearchManagers: closeActiveMemorySearchManagersMock,
}));
vi.mock("../plugins/memory-state.js" , () => ({
hasMemoryRuntime: hasMemoryRuntimeMock,
}));
vi.mock("../tasks/task-registry.js" , () => ({
ensureTaskRegistryReady: ensureTaskRegistryReadyMock,
}));
vi.mock("../tasks/task-registry.maintenance.js" , () => ({
startTaskRegistryMaintenance: startTaskRegistryMaintenanceMock,
}));
vi.mock("./program/root-help.js" , () => ({
outputRootHelp: outputRootHelpMock,
}));
vi.mock("./root-help-metadata.js" , () => ({
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
}));
vi.mock("./program.js" , () => ({
buildProgram: buildProgramMock,
}));
vi.mock("./program/program-context.js" , () => ({
getProgramContext: getProgramContextMock,
}));
vi.mock("./program/command-registry.js" , () => ({
registerCoreCliByName: registerCoreCliByNameMock,
}));
vi.mock("./program/register.subclis.js" , () => ({
registerSubCliByName: registerSubCliByNameMock,
}));
vi.mock("../terminal/restore.js" , () => ({
restoreTerminalState: restoreTerminalStateMock,
}));
describe("runCli exit behavior" , () => {
beforeEach(() => {
vi.clearAllMocks();
hasMemoryRuntimeMock.mockReturnValue(false );
outputPrecomputedRootHelpTextMock.mockReturnValue(false );
getProgramContextMock.mockReturnValue(null );
});
it("does not force process.exit after successful routed command" , async () => {
tryRouteCliMock.mockResolvedValueOnce(true );
const exitSpy = vi.spyOn(process, "exit" ).mockImplementation(((code?: number) => {
throw new Error(`unexpected process.exit(${String(code)})`);
}) as typeof process.exit);
await runCli(["node" , "openclaw" , "status" ]);
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node" , "openclaw" , "status" ]);
expect(tryRouteCliMock).toHaveBeenCalledWith(["node" , "openclaw" , "status" ]);
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
expect(ensureTaskRegistryReadyMock).not.toHaveBeenCalled();
expect(startTaskRegistryMaintenanceMock).not.toHaveBeenCalled();
expect(exitSpy).not.toHaveBeenCalled();
exitSpy.mockRestore();
});
it("renders root help without building the full program" , async () => {
const exitSpy = vi.spyOn(process, "exit" ).mockImplementation(((code?: number) => {
throw new Error(`unexpected process.exit(${String(code)})`);
}) as typeof process.exit);
await runCli(["node" , "openclaw" , "--help" ]);
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node" , "openclaw" , "--help" ]);
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1 );
expect(outputRootHelpMock).toHaveBeenCalledTimes(1 );
expect(buildProgramMock).not.toHaveBeenCalled();
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
expect(exitSpy).not.toHaveBeenCalled();
exitSpy.mockRestore();
});
it("closes memory managers when a runtime was registered" , async () => {
tryRouteCliMock.mockResolvedValueOnce(true );
hasMemoryRuntimeMock.mockReturnValue(true );
await runCli(["node" , "openclaw" , "status" ]);
expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledTimes(1 );
});
it("returns after a handled container-target invocation" , async () => {
maybeRunCliInContainerMock.mockReturnValueOnce({ handled: true , exitCode: 0 });
await runCli(["node" , "openclaw" , "--container" , "demo" , "status" ]);
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith([
"node" ,
"openclaw" ,
"--container" ,
"demo" ,
"status" ,
]);
expect(loadDotEnvMock).not.toHaveBeenCalled();
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
});
it("propagates a handled container-target exit code" , async () => {
const exitCode = process.exitCode;
maybeRunCliInContainerMock.mockReturnValueOnce({ handled: true , exitCode: 7 });
await runCli(["node" , "openclaw" , "--container" , "demo" , "status" ]);
expect(process.exitCode).toBe(7 );
process.exitCode = exitCode;
});
it("swallows Commander parse exits after recording the exit code" , async () => {
const exitCode = process.exitCode;
buildProgramMock.mockReturnValueOnce({
commands: [{ name: () => "status" }],
parseAsync: vi
.fn()
.mockRejectedValueOnce(
new CommanderError(1 , "commander.excessArguments" , "too many arguments for 'status'" ),
),
});
await expect(runCli(["node" , "openclaw" , "status" ])).resolves.toBeUndefined();
expect(registerSubCliByNameMock).toHaveBeenCalledWith(expect.anything(), "status" );
expect(process.exitCode).toBe(1 );
process.exitCode = exitCode;
});
it("loads the real primary command before rendering command help" , async () => {
buildProgramMock.mockReturnValueOnce({
commands: [{ name: () => "doctor" }],
parseAsync: vi.fn().mockResolvedValueOnce(undefined),
});
const ctx = { programVersion: "0.0.0-test" };
getProgramContextMock.mockReturnValueOnce(ctx as never);
await runCli(["node" , "openclaw" , "doctor" , "--help" ]);
expect(registerCoreCliByNameMock).toHaveBeenCalledWith(expect.anything(), ctx, "doctor" , [
"node" ,
"openclaw" ,
"doctor" ,
"--help" ,
]);
expect(registerSubCliByNameMock).toHaveBeenCalledWith(expect.anything(), "doctor" );
});
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();
}
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland