Files
openclaw/src/cli/route.test.ts
2026-05-12 23:59:23 +01:00

198 lines
6.5 KiB
TypeScript

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("skips config guard for routed status --json commands", async () => {
await expect(tryRouteCli(["node", "openclaw", "status", "--json"])).resolves.toBe(true);
expect(ensureConfigReadyMock).not.toHaveBeenCalled();
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();
});
});