mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 10:03:58 +00:00
265 lines
9.3 KiB
TypeScript
265 lines
9.3 KiB
TypeScript
// Route CLI tests cover route command registration, channel routing, and output.
|
|
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 originalLogLevel: 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;
|
|
originalLogLevel = process.env.OPENCLAW_LOG_LEVEL;
|
|
delete process.env.OPENCLAW_DISABLE_ROUTE_FIRST;
|
|
delete process.env.OPENCLAW_HIDE_BANNER;
|
|
delete process.env.OPENCLAW_LOG_LEVEL;
|
|
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;
|
|
}
|
|
if (originalLogLevel === undefined) {
|
|
delete process.env.OPENCLAW_LOG_LEVEL;
|
|
} else {
|
|
process.env.OPENCLAW_LOG_LEVEL = originalLogLevel;
|
|
}
|
|
});
|
|
|
|
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 () => {
|
|
const capturedLogLevels: Array<string | undefined> = [];
|
|
ensureConfigReadyMock.mockImplementationOnce(async () => {
|
|
capturedLogLevels.push(process.env.OPENCLAW_LOG_LEVEL);
|
|
});
|
|
|
|
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",
|
|
});
|
|
expect(capturedLogLevels).toEqual(["debug"]);
|
|
expect(process.env.OPENCLAW_LOG_LEVEL).toBe("debug");
|
|
});
|
|
|
|
it("applies routed log level options after the command", async () => {
|
|
const capturedLogLevels: Array<string | undefined> = [];
|
|
ensureConfigReadyMock.mockImplementationOnce(async () => {
|
|
capturedLogLevels.push(process.env.OPENCLAW_LOG_LEVEL);
|
|
});
|
|
|
|
await expect(tryRouteCli(["node", "openclaw", "status", "--log-level=trace"])).resolves.toBe(
|
|
true,
|
|
);
|
|
|
|
expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1);
|
|
expect(runRouteMock).toHaveBeenCalledTimes(1);
|
|
expect(capturedLogLevels).toEqual(["trace"]);
|
|
expect(process.env.OPENCLAW_LOG_LEVEL).toBe("trace");
|
|
});
|
|
|
|
it("uses the last valid routed log level option", async () => {
|
|
await expect(
|
|
tryRouteCli(["node", "openclaw", "--log-level", "debug", "status", "--log-level=trace"]),
|
|
).resolves.toBe(true);
|
|
|
|
expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1);
|
|
expect(runRouteMock).toHaveBeenCalledTimes(1);
|
|
expect(process.env.OPENCLAW_LOG_LEVEL).toBe("trace");
|
|
});
|
|
|
|
it.each([
|
|
["invalid value", ["node", "openclaw", "status", "--log-level", "verbose"]],
|
|
["missing value", ["node", "openclaw", "status", "--log-level"]],
|
|
[
|
|
"later invalid value",
|
|
["node", "openclaw", "--log-level", "debug", "status", "--log-level", "verbose"],
|
|
],
|
|
])("falls back for %s routed log level options before bootstrap", async (_name, argv) => {
|
|
await expect(tryRouteCli(argv)).resolves.toBe(false);
|
|
|
|
expect(ensureConfigReadyMock).not.toHaveBeenCalled();
|
|
expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled();
|
|
expect(runRouteMock).not.toHaveBeenCalled();
|
|
expect(process.env.OPENCLAW_LOG_LEVEL).toBeUndefined();
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|