import process from "node:process"; import { CommanderError } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { runCli, shouldStartProxyForCli } 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 listAgentHarnessIdsMock = vi.hoisted(() => vi.fn((): string[] => [])); const disposeRegisteredAgentHarnessesMock = vi.hoisted(() => vi.fn(async () => {})); 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 outputPrecomputedBrowserHelpTextMock = 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 registerPluginCliCommandsFromValidatedConfigMock = vi.hoisted(() => vi.fn(async () => ({}))); const resolvePluginCliRootOwnerIdsMock = vi.hoisted(() => vi.fn()); const restoreTerminalStateMock = vi.hoisted(() => vi.fn()); const hasEnvHttpProxyAgentConfiguredMock = vi.hoisted(() => vi.fn(() => false)); const ensureGlobalUndiciEnvProxyDispatcherMock = vi.hoisted(() => vi.fn()); const runCrestodianMock = vi.hoisted(() => vi.fn(async () => {})); const commanderParseAsyncMock = vi.hoisted(() => vi.fn(async () => {})); const addGatewayRunCommandMock = vi.hoisted(() => vi.fn((command: unknown) => command)); const emitCliBannerMock = vi.hoisted(() => vi.fn()); const progressDoneMock = vi.hoisted(() => vi.fn()); const createCliProgressMock = vi.hoisted(() => vi.fn(() => ({ done: progressDoneMock, })), ); const loadConfigMock = vi.hoisted(() => vi.fn(() => ({}))); const startProxyMock = vi.hoisted(() => vi.fn<(config: unknown) => Promise>(async () => null), ); const stopProxyMock = vi.hoisted(() => vi.fn<(handle: unknown) => Promise>(async () => {})); const maybeRunCliInContainerMock = vi.hoisted(() => vi.fn< (argv: string[]) => { handled: true; exitCode: number } | { handled: false; argv: string[] } >((argv: string[]) => ({ handled: false, argv })), ); vi.mock("commander", () => { class MockCommanderError extends Error { exitCode: number; code: string; constructor(exitCode: number, code: string, message: string) { super(message); this.exitCode = exitCode; this.code = code; } } class MockCommand { name = vi.fn(() => this); enablePositionalOptions = vi.fn(() => this); option = vi.fn(() => this); exitOverride = vi.fn(() => this); description = vi.fn(() => this); command = vi.fn(() => new MockCommand()); parseAsync = commanderParseAsyncMock; } return { Command: MockCommand, CommanderError: MockCommanderError, }; }); vi.mock("./route.js", () => ({ tryRouteCli: tryRouteCliMock, })); vi.mock("./gateway-cli/run.js", () => ({ addGatewayRunCommand: addGatewayRunCommandMock, })); vi.mock("../version.js", () => ({ VERSION: "9.9.9-test", })); vi.mock("./banner.js", () => ({ emitCliBanner: emitCliBannerMock, })); 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", () => ({ isTruthyEnvValue: (value?: string) => typeof value === "string" && ["1", "on", "true", "yes"].includes(value.trim().toLowerCase()), 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("../agents/harness/registry.js", () => ({ listAgentHarnessIds: listAgentHarnessIdsMock, disposeRegisteredAgentHarnesses: disposeRegisteredAgentHarnessesMock, })); 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", () => ({ outputPrecomputedBrowserHelpText: outputPrecomputedBrowserHelpTextMock, 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("../plugins/cli.js", () => ({ registerPluginCliCommandsFromValidatedConfig: registerPluginCliCommandsFromValidatedConfigMock, })); vi.mock("../plugins/cli-registry-loader.js", () => ({ resolvePluginCliRootOwnerIds: resolvePluginCliRootOwnerIdsMock, })); vi.mock("../terminal/restore.js", () => ({ restoreTerminalState: restoreTerminalStateMock, })); vi.mock("../infra/net/proxy-env.js", () => ({ hasEnvHttpProxyAgentConfigured: hasEnvHttpProxyAgentConfiguredMock, })); vi.mock("../infra/net/undici-global-dispatcher.js", () => ({ ensureGlobalUndiciEnvProxyDispatcher: ensureGlobalUndiciEnvProxyDispatcherMock, })); vi.mock("../crestodian/crestodian.js", () => ({ runCrestodian: runCrestodianMock, })); vi.mock("./progress.js", () => ({ createCliProgress: createCliProgressMock, })); vi.mock("../config/io.js", () => ({ readBestEffortConfig: loadConfigMock, })); vi.mock("../infra/net/proxy/proxy-lifecycle.js", () => ({ startProxy: startProxyMock, stopProxy: stopProxyMock, })); function makeProxyHandle() { return { proxyUrl: "http://127.0.0.1:19876", injectedProxyUrl: "http://127.0.0.1:19876", envSnapshot: { http_proxy: undefined, https_proxy: undefined, HTTP_PROXY: undefined, HTTPS_PROXY: undefined, GLOBAL_AGENT_HTTP_PROXY: undefined, GLOBAL_AGENT_HTTPS_PROXY: undefined, GLOBAL_AGENT_FORCE_GLOBAL_AGENT: undefined, no_proxy: undefined, NO_PROXY: undefined, GLOBAL_AGENT_NO_PROXY: undefined, OPENCLAW_PROXY_ACTIVE: undefined, }, stop: vi.fn(async () => {}), kill: vi.fn(), }; } describe("runCli exit behavior", () => { beforeEach(() => { vi.clearAllMocks(); hasMemoryRuntimeMock.mockReturnValue(false); listAgentHarnessIdsMock.mockReturnValue([]); outputPrecomputedBrowserHelpTextMock.mockReturnValue(false); outputPrecomputedRootHelpTextMock.mockReturnValue(false); hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false); loadConfigMock.mockReturnValue({}); startProxyMock.mockResolvedValue(null); stopProxyMock.mockResolvedValue(undefined); getProgramContextMock.mockReturnValue(null); resolvePluginCliRootOwnerIdsMock.mockImplementation( ({ primaryCommand }: { primaryCommand?: string }) => primaryCommand === "googlemeet" ? ["google-meet"] : [], ); delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH; delete process.env.OPENCLAW_HIDE_BANNER; }); 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(disposeRegisteredAgentHarnessesMock).not.toHaveBeenCalled(); expect(ensureTaskRegistryReadyMock).not.toHaveBeenCalled(); expect(startTaskRegistryMaintenanceMock).not.toHaveBeenCalled(); expect(exitSpy).not.toHaveBeenCalled(); exitSpy.mockRestore(); }); it("disposes registered harnesses after full CLI command completion", async () => { listAgentHarnessIdsMock.mockReturnValueOnce(["codex"]); tryRouteCliMock.mockResolvedValueOnce(false); const parseAsync = vi.fn().mockResolvedValueOnce(undefined); buildProgramMock.mockReturnValueOnce({ commands: [{ name: () => "agent", aliases: () => [] }], parseAsync, }); await runCli(["node", "openclaw", "agent", "--local"]); expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "agent", "--local"]); expect(disposeRegisteredAgentHarnessesMock).toHaveBeenCalledTimes(1); }); it("pauses non-tty stdin after full CLI command completion", async () => { tryRouteCliMock.mockResolvedValueOnce(false); const parseAsync = vi.fn().mockResolvedValueOnce(undefined); buildProgramMock.mockReturnValueOnce({ commands: [{ name: () => "channels", aliases: () => [] }], parseAsync, }); const stdinTty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY"); Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: false }); const pauseSpy = vi.spyOn(process.stdin, "pause").mockImplementation(() => process.stdin); try { await runCli(["node", "openclaw", "channels"]); expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "channels"]); expect(pauseSpy).toHaveBeenCalledTimes(1); } finally { pauseSpy.mockRestore(); if (stdinTty) { Object.defineProperty(process.stdin, "isTTY", stdinTty); } else { Reflect.deleteProperty(process.stdin, "isTTY"); } } }); it("emits the startup banner before gateway foreground fast-path startup", async () => { await runCli(["node", "openclaw", "gateway", "--force"]); expect(tryRouteCliMock).not.toHaveBeenCalled(); expect(emitCliBannerMock).toHaveBeenCalledWith("9.9.9-test", { argv: ["node", "openclaw", "gateway", "--force"], }); expect(addGatewayRunCommandMock).toHaveBeenCalledTimes(2); expect(commanderParseAsyncMock).toHaveBeenCalledWith([ "node", "openclaw", "gateway", "--force", ]); }); it("honors banner suppression on the gateway foreground fast path", async () => { process.env.OPENCLAW_HIDE_BANNER = "1"; await runCli(["node", "openclaw", "gateway"]); expect(tryRouteCliMock).not.toHaveBeenCalled(); expect(emitCliBannerMock).not.toHaveBeenCalled(); expect(commanderParseAsyncMock).toHaveBeenCalledWith(["node", "openclaw", "gateway"]); }); it("renders browser help from startup metadata without building the full program", async () => { outputPrecomputedBrowserHelpTextMock.mockReturnValueOnce(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", "browser", "--help"]); expect(maybeRunCliInContainerMock).toHaveBeenCalledWith([ "node", "openclaw", "browser", "--help", ]); expect(tryRouteCliMock).not.toHaveBeenCalled(); expect(outputPrecomputedBrowserHelpTextMock).toHaveBeenCalledTimes(1); expect(outputRootHelpMock).not.toHaveBeenCalled(); expect(buildProgramMock).not.toHaveBeenCalled(); expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled(); expect(exitSpy).not.toHaveBeenCalled(); exitSpy.mockRestore(); }); it("keeps root help on the precomputed path without proxy bootstrap", async () => { outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true); await runCli(["node", "openclaw", "--help"]); expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1); expect(hasEnvHttpProxyAgentConfiguredMock).not.toHaveBeenCalled(); expect(ensureGlobalUndiciEnvProxyDispatcherMock).not.toHaveBeenCalled(); expect(runCrestodianMock).not.toHaveBeenCalled(); }); 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("does not start the managed proxy for local gateway client commands", async () => { tryRouteCliMock.mockResolvedValueOnce(true); await runCli(["node", "openclaw", "status"]); expect(startProxyMock).not.toHaveBeenCalled(); expect(stopProxyMock).not.toHaveBeenCalled(); }); it.each([ ["gateway runtime", ["node", "openclaw", "gateway", "run"]], ["bare gateway runtime", ["node", "openclaw", "gateway"]], ["node runtime", ["node", "openclaw", "node", "run"]], ["local agent runtime", ["node", "openclaw", "agent", "--local"]], ["provider inference", ["node", "openclaw", "infer", "web", "fetch", "https://example.com"]], ["model command", ["node", "openclaw", "models", "auth", "login", "openai"]], ["plugin command", ["node", "openclaw", "plugins", "marketplace", "list"]], ["skill command", ["node", "openclaw", "skills", "search", "browser"]], ["update command", ["node", "openclaw", "update", "check"]], ["channel probe", ["node", "openclaw", "channels", "status", "--probe"]], ["channel capabilities probe", ["node", "openclaw", "channels", "capabilities"]], ["directory plugin command", ["node", "openclaw", "directory", "peers", "list"]], ["message plugin command", ["node", "openclaw", "message", "send", "--to", "demo"]], ["metadata-owned plugin command", ["node", "openclaw", "googlemeet", "login"]], ])("starts managed proxy routing for %s", (_name, argv) => { expect(shouldStartProxyForCli(argv)).toBe(true); }); it.each([ ["root help", ["node", "openclaw", "--help"]], ["root version", ["node", "openclaw", "--version"]], ["gateway help", ["node", "openclaw", "gateway", "--help"]], ["gateway run help", ["node", "openclaw", "gateway", "run", "--help"]], ["status", ["node", "openclaw", "status"]], ["health", ["node", "openclaw", "health"]], ["gateway status", ["node", "openclaw", "gateway", "status"]], ["gateway health", ["node", "openclaw", "gateway", "health"]], ["remote agent control-plane", ["node", "openclaw", "agent", "run"]], ["chat control-plane", ["node", "openclaw", "chat"]], ["terminal control-plane", ["node", "openclaw", "terminal"]], ["config", ["node", "openclaw", "config", "get", "proxy.enabled"]], ["channels parent help", ["node", "openclaw", "channels"]], ["completion", ["node", "openclaw", "completion", "zsh"]], ["debug proxy cli", ["node", "openclaw", "proxy", "start"]], ["agents list", ["node", "openclaw", "agents", "list"]], ["models list", ["node", "openclaw", "models", "list"]], ["models status without live probe", ["node", "openclaw", "models", "status"]], ["skills check", ["node", "openclaw", "skills", "check"]], ["skills info", ["node", "openclaw", "skills", "info", "weather"]], ["skills list", ["node", "openclaw", "skills", "list"]], ["tasks list", ["node", "openclaw", "tasks", "list"]], ["legacy singular tool namespace", ["node", "openclaw", "tool", "image_generate"]], ["gateway tools namespace typo", ["node", "openclaw", "tools", "effective"]], ["migrate", ["node", "openclaw", "migrate"]], ])("skips managed proxy routing for %s", (_name, argv) => { expect(shouldStartProxyForCli(argv)).toBe(false); }); it("starts the managed proxy for network-capable commands by default", async () => { tryRouteCliMock.mockResolvedValueOnce(true); await runCli(["node", "openclaw", "plugins", "marketplace", "list"]); expect(startProxyMock).toHaveBeenCalledWith(undefined); }); it("starts the managed proxy for metadata-owned plugin commands by default", async () => { tryRouteCliMock.mockResolvedValueOnce(true); await runCli(["node", "openclaw", "googlemeet", "login"]); expect(startProxyMock).toHaveBeenCalledWith(undefined); }); it("rejects unowned command roots before proxy and plugin runtime registration", async () => { await expect(runCli(["node", "openclaw", "foo"])).rejects.toThrow( 'No built-in command or plugin CLI metadata owns "foo"', ); expect(startProxyMock).not.toHaveBeenCalled(); expect(tryRouteCliMock).not.toHaveBeenCalled(); expect(buildProgramMock).not.toHaveBeenCalled(); expect(registerPluginCliCommandsFromValidatedConfigMock).not.toHaveBeenCalled(); }); it("does not install the env proxy dispatcher for bypassed skills inspection commands", async () => { hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true); tryRouteCliMock.mockResolvedValueOnce(true); await runCli(["node", "openclaw", "skills", "check"]); expect(hasEnvHttpProxyAgentConfiguredMock).not.toHaveBeenCalled(); expect(ensureGlobalUndiciEnvProxyDispatcherMock).not.toHaveBeenCalled(); }); it.each([ ["auth", ["node", "openclaw", "auth", "--help"]], ["tool", ["node", "openclaw", "tool", "image_generate"]], ["tools", ["node", "openclaw", "tools", "effective"]], ])("keeps reserved %s command roots out of plugin command discovery", async (_name, argv) => { const parseAsync = vi.fn().mockResolvedValueOnce(undefined); buildProgramMock.mockReturnValueOnce({ commands: [], parseAsync, }); await runCli(argv); expect(startProxyMock).not.toHaveBeenCalled(); expect(registerSubCliByNameMock).toHaveBeenCalledWith(expect.anything(), argv[2], argv); expect(registerPluginCliCommandsFromValidatedConfigMock).not.toHaveBeenCalled(); expect(parseAsync).toHaveBeenCalledWith(argv); }); it("fails protected commands when managed proxy activation fails", async () => { startProxyMock.mockRejectedValueOnce(new Error("proxy: enabled but no HTTP proxy URL")); await expect(runCli(["node", "openclaw", "gateway", "run"])).rejects.toThrow( "proxy: enabled but no HTTP proxy URL", ); expect(tryRouteCliMock).not.toHaveBeenCalled(); expect(stopProxyMock).not.toHaveBeenCalled(); }); it("fails protected commands when config cannot be loaded for managed proxy startup", async () => { loadConfigMock.mockImplementationOnce(() => { throw new Error("config parse failed"); }); await expect(runCli(["node", "openclaw", "gateway", "run"])).rejects.toThrow( "config parse failed", ); expect(startProxyMock).not.toHaveBeenCalled(); expect(tryRouteCliMock).not.toHaveBeenCalled(); }); it("stops the managed proxy after normal gateway runtime completion", async () => { const handle = makeProxyHandle(); startProxyMock.mockResolvedValueOnce(handle); await runCli(["node", "openclaw", "gateway", "run"]); expect(startProxyMock).toHaveBeenCalledWith(undefined); expect(stopProxyMock).toHaveBeenCalledOnce(); expect(stopProxyMock).toHaveBeenCalledWith(handle); }); it("stops the managed proxy and exits after SIGINT", async () => { const handle = makeProxyHandle(); startProxyMock.mockResolvedValueOnce(handle); let resolveRoute: (value: boolean) => void = () => {}; tryRouteCliMock.mockReturnValueOnce( new Promise((resolve) => { resolveRoute = resolve; }), ); const processOnceSpy = vi.spyOn(process, "once"); const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number | string) => { void code; return undefined as never; }) as typeof process.exit); try { const runPromise = runCli(["node", "openclaw", "plugins", "marketplace", "list"]); await vi.waitFor(() => { expect(processOnceSpy).toHaveBeenCalledWith("SIGINT", expect.any(Function)); }); const sigintHandler = processOnceSpy.mock.calls.find(([event]) => event === "SIGINT")?.[1]; if (typeof sigintHandler !== "function") { throw new Error("SIGINT handler was not registered"); } sigintHandler(); await vi.waitFor(() => { expect(stopProxyMock).toHaveBeenCalledWith(handle); }); await vi.waitFor(() => { expect(exitSpy).toHaveBeenCalledWith(130); }); resolveRoute(true); await runPromise; expect(stopProxyMock).toHaveBeenCalledTimes(1); } finally { exitSpy.mockRestore(); processOnceSpy.mockRestore(); } }); it("synchronously kills the managed proxy during hard process exit", async () => { const handle = makeProxyHandle(); startProxyMock.mockResolvedValueOnce(handle); let resolveRoute: (value: boolean) => void = () => {}; tryRouteCliMock.mockReturnValueOnce( new Promise((resolve) => { resolveRoute = resolve; }), ); const processOnceSpy = vi.spyOn(process, "once"); try { const runPromise = runCli(["node", "openclaw", "plugins", "marketplace", "list"]); await vi.waitFor(() => { expect(processOnceSpy.mock.calls.filter(([event]) => event === "exit")).toHaveLength(2); }); const exitHandler = processOnceSpy.mock.calls.find(([event]) => event === "exit")?.[1]; if (typeof exitHandler !== "function") { throw new Error("exit handler was not registered"); } exitHandler(0 as never); expect(handle.kill).toHaveBeenCalledWith("SIGTERM"); resolveRoute(true); await runPromise; expect(stopProxyMock).not.toHaveBeenCalledWith(handle); } finally { processOnceSpy.mockRestore(); } }); it("bootstraps env proxy before bare Crestodian startup", async () => { hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true); const stdinTty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY"); const stdoutTty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY"); Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true }); Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true }); try { await runCli(["node", "openclaw"]); } finally { if (stdinTty) { Object.defineProperty(process.stdin, "isTTY", stdinTty); } else { delete (process.stdin as { isTTY?: boolean }).isTTY; } if (stdoutTty) { Object.defineProperty(process.stdout, "isTTY", stdoutTty); } else { delete (process.stdout as { isTTY?: boolean }).isTTY; } } expect(ensureGlobalUndiciEnvProxyDispatcherMock).toHaveBeenCalledTimes(1); expect(runCrestodianMock).toHaveBeenCalledWith({ onReady: expect.any(Function) }); expect(ensureGlobalUndiciEnvProxyDispatcherMock.mock.invocationCallOrder[0]).toBeLessThan( runCrestodianMock.mock.invocationCallOrder[0], ); }); it("bootstraps env proxy before modern onboard Crestodian startup", async () => { hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true); await runCli(["node", "openclaw", "onboard", "--modern", "--json"]); expect(ensureGlobalUndiciEnvProxyDispatcherMock).toHaveBeenCalledTimes(1); expect(runCrestodianMock).toHaveBeenCalledWith({ message: undefined, yes: false, json: true, interactive: true, }); expect(ensureGlobalUndiciEnvProxyDispatcherMock.mock.invocationCallOrder[0]).toBeLessThan( runCrestodianMock.mock.invocationCallOrder[0], ); }); 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("does not fail the command when memory cleanup is unavailable", async () => { tryRouteCliMock.mockResolvedValueOnce(true); hasMemoryRuntimeMock.mockImplementationOnce(() => { throw new Error("stale memory-state chunk"); }); await expect(runCli(["node", "openclaw", "status"])).resolves.toBeUndefined(); expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled(); }); 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", [ "node", "openclaw", "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", [ "node", "openclaw", "doctor", "--help", ]); }); 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]; if (typeof handler !== "function") { throw new Error("uncaughtException handler was not registered"); } try { expect(() => handler(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(); } }); it("does not exit for transient uncaught CLI exceptions", async () => { buildProgramMock.mockReturnValueOnce({ commands: [{ name: () => "status" }], parseAsync: vi.fn().mockResolvedValueOnce(undefined), }); const processOnSpy = vi.spyOn(process, "on"); const consoleWarnSpy = vi.spyOn(console, "warn").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]; if (typeof handler !== "function") { throw new Error("uncaughtException handler was not registered"); } try { const hostUnreachable = Object.assign(new Error("connect EHOSTUNREACH 149.154.167.220:443"), { code: "EHOSTUNREACH", }); expect(() => handler(hostUnreachable)).not.toThrow(); expect(consoleWarnSpy).toHaveBeenCalledWith( "[openclaw] Non-fatal uncaught exception (continuing):", expect.stringContaining("EHOSTUNREACH"), ); expect(restoreTerminalStateMock).not.toHaveBeenCalled(); expect(exitSpy).not.toHaveBeenCalled(); } finally { if (typeof handler === "function") { process.off("uncaughtException", handler); } consoleWarnSpy.mockRestore(); exitSpy.mockRestore(); processOnSpy.mockRestore(); } }); });