import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const emitCliBannerMock = vi.hoisted(() => vi.fn()); const ensureConfigReadyMock = vi.hoisted(() => vi.fn(async (_params: { runtime?: unknown; commandPath?: unknown }) => {}), ); const ensurePluginRegistryLoadedMock = vi.hoisted(() => vi.fn()); const findRoutedCommandMock = vi.hoisted(() => vi.fn()); const runRouteMock = vi.hoisted(() => vi.fn(async () => true)); vi.mock("./banner.js", () => ({ emitCliBanner: emitCliBannerMock, })); vi.mock("./program/config-guard.js", () => ({ ensureConfigReady: ensureConfigReadyMock, })); vi.mock("./plugin-registry.js", () => ({ ensurePluginRegistryLoaded: ensurePluginRegistryLoadedMock, })); vi.mock("./program/routes.js", () => ({ findRoutedCommand: findRoutedCommandMock, })); vi.mock("../runtime.js", () => ({ defaultRuntime: { error: vi.fn(), log: vi.fn(), exit: vi.fn(), writeStdout: vi.fn(), writeJson: vi.fn(), }, })); function firstConfigReadyCall() { return ensureConfigReadyMock.mock.calls[0]?.[0] as | { runtime?: unknown; commandPath?: unknown } | undefined; } describe("tryRouteCli", () => { let tryRouteCli: typeof import("./route.js").tryRouteCli; // Capture the same loggingState reference that route.js uses. let loggingState: typeof import("../logging/state.js").loggingState; let originalDisableRouteFirst: string | undefined; let originalHideBanner: string | undefined; let originalForceStderr: boolean; beforeAll(async () => { ({ tryRouteCli } = await import("./route.js")); ({ loggingState } = await import("../logging/state.js")); }); beforeEach(() => { vi.clearAllMocks(); originalDisableRouteFirst = process.env.OPENCLAW_DISABLE_ROUTE_FIRST; originalHideBanner = process.env.OPENCLAW_HIDE_BANNER; delete process.env.OPENCLAW_DISABLE_ROUTE_FIRST; delete process.env.OPENCLAW_HIDE_BANNER; originalForceStderr = loggingState.forceConsoleToStderr; loggingState.forceConsoleToStderr = false; findRoutedCommandMock.mockReturnValue({ loadPlugins: (argv: string[]) => !argv.includes("--json"), run: runRouteMock, }); }); afterEach(() => { if (loggingState) { loggingState.forceConsoleToStderr = originalForceStderr; } if (originalDisableRouteFirst === undefined) { delete process.env.OPENCLAW_DISABLE_ROUTE_FIRST; } else { process.env.OPENCLAW_DISABLE_ROUTE_FIRST = originalDisableRouteFirst; } if (originalHideBanner === undefined) { delete process.env.OPENCLAW_HIDE_BANNER; } else { process.env.OPENCLAW_HIDE_BANNER = originalHideBanner; } }); it("keeps config guard for routed status --json commands", async () => { await expect(tryRouteCli(["node", "openclaw", "status", "--json"])).resolves.toBe(true); expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1); expect(firstConfigReadyCall()?.commandPath).toEqual(["status"]); expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled(); }); it("keeps config guard for the parent tasks JSON list alias", async () => { await expect(tryRouteCli(["node", "openclaw", "tasks", "--json"])).resolves.toBe(true); expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1); expect(firstConfigReadyCall()?.commandPath).toEqual(["tasks"]); expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled(); }); it("does not pass suppressDoctorStdout for routed non-json commands", async () => { await expect(tryRouteCli(["node", "openclaw", "status"])).resolves.toBe(true); expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1); const configReadyCall = firstConfigReadyCall(); expect(typeof configReadyCall?.runtime).toBe("object"); expect(configReadyCall?.commandPath).toEqual(["status"]); expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels", }); }); it("keeps logs routed to stderr for routed --json commands", async () => { findRoutedCommandMock.mockReturnValue({ loadPlugins: true, run: runRouteMock, }); // Capture the value inside the mock callback using the same loggingState // reference that route.js sees. const captured: boolean[] = []; ensurePluginRegistryLoadedMock.mockImplementation(() => { captured.push(loggingState.forceConsoleToStderr); }); await tryRouteCli(["node", "openclaw", "agents", "--json"]); expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledTimes(1); expect(captured[0]).toBe(true); expect(loggingState.forceConsoleToStderr).toBe(true); }); it("routes command-run logs to stderr for config-guard-skipping --json routes", async () => { const captured: boolean[] = []; runRouteMock.mockImplementationOnce(async () => { captured.push(loggingState.forceConsoleToStderr); return true; }); await expect(tryRouteCli(["node", "openclaw", "models", "status", "--json"])).resolves.toBe( true, ); expect(ensureConfigReadyMock).not.toHaveBeenCalled(); expect(captured).toEqual([true]); }); it("does not route logs to stderr during plugin loading without --json", async () => { findRoutedCommandMock.mockReturnValue({ loadPlugins: true, run: runRouteMock, }); const captured: boolean[] = []; ensurePluginRegistryLoadedMock.mockImplementation(() => { captured.push(loggingState.forceConsoleToStderr); }); await tryRouteCli(["node", "openclaw", "agents"]); expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledTimes(1); expect(captured[0]).toBe(false); expect(loggingState.forceConsoleToStderr).toBe(false); }); it("routes status when root options precede the command", async () => { await expect(tryRouteCli(["node", "openclaw", "--log-level", "debug", "status"])).resolves.toBe( true, ); expect(findRoutedCommandMock).toHaveBeenCalledWith( ["status"], ["node", "openclaw", "--log-level", "debug", "status"], ); expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1); const configReadyCall = firstConfigReadyCall(); expect(typeof configReadyCall?.runtime).toBe("object"); expect(configReadyCall?.commandPath).toEqual(["status"]); expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels", }); }); it("respects OPENCLAW_HIDE_BANNER for routed commands", async () => { process.env.OPENCLAW_HIDE_BANNER = "1"; await expect(tryRouteCli(["node", "openclaw", "status"])).resolves.toBe(true); expect(emitCliBannerMock).not.toHaveBeenCalled(); }); it("falls back before bootstrap when the route cannot parse the argv", async () => { findRoutedCommandMock.mockReturnValue({ canRun: () => false, loadPlugins: true, run: runRouteMock, }); await expect(tryRouteCli(["node", "openclaw", "tasks", "list"])).resolves.toBe(false); expect(ensureConfigReadyMock).not.toHaveBeenCalled(); expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled(); expect(runRouteMock).not.toHaveBeenCalled(); }); });