refactor: reuse shared cli runtime test mocks

This commit is contained in:
Peter Steinberger 2026-03-23 01:42:00 +00:00
parent 2e6f2b0f07
commit 54213b587f
16 changed files with 176 additions and 283 deletions

View File

@ -1,5 +1,8 @@
import { Command } from "commander";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture();
const gatewayMocks = vi.hoisted(() => ({
callGatewayFromCli: vi.fn(async () => ({
@ -47,17 +50,6 @@ vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: sharedMocks.callBrowserRequest,
}));
const runtime = {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn((value: string) => {
runtime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
runtime.log(JSON.stringify(value, null, space));
}),
exit: vi.fn(),
};
vi.mock("../runtime.js", () => ({
defaultRuntime: runtime,
}));
@ -91,6 +83,7 @@ describe("browser cli snapshot defaults", () => {
afterEach(() => {
vi.clearAllMocks();
resetRuntimeCapture();
configMocks.loadConfig.mockReturnValue({ browser: {} });
});

View File

@ -1,11 +1,22 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
import { createBrowserProgram } from "./browser-cli-test-helpers.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
const runtimeState = vi.hoisted(() => ({ capture: null as CliRuntimeCapture | null }));
function getRuntimeCapture(): CliRuntimeCapture {
if (!runtimeState.capture) {
throw new Error("runtime capture not initialized");
}
return runtimeState.capture;
}
function getRuntime() {
return getRuntimeCapture().defaultRuntime;
}
const mocks = vi.hoisted(() => {
const runtimeLog = vi.fn();
const runtimeError = vi.fn();
const runtimeExit = vi.fn();
return {
callBrowserRequest: vi.fn<
(
@ -14,20 +25,6 @@ const mocks = vi.hoisted(() => {
runtimeOpts?: { timeoutMs?: number },
) => Promise<Record<string, unknown>>
>(async () => ({})),
runtimeLog,
runtimeError,
runtimeExit,
runtime: {
log: runtimeLog,
error: runtimeError,
writeStdout: vi.fn((value: string) =>
runtimeLog(value.endsWith("\n") ? value.slice(0, -1) : value),
),
writeJson: vi.fn((value: unknown, space = 2) =>
runtimeLog(JSON.stringify(value, null, space)),
),
exit: runtimeExit,
},
};
});
@ -43,9 +40,11 @@ vi.mock("./cli-utils.js", () => ({
) => await action().catch(onError),
}));
vi.mock("../runtime.js", () => ({
defaultRuntime: mocks.runtime,
}));
vi.mock("../runtime.js", async () => {
const { createCliRuntimeCapture } = await import("./test-runtime-capture.js");
runtimeState.capture ??= createCliRuntimeCapture();
return { defaultRuntime: runtimeState.capture.defaultRuntime };
});
function createProgram() {
const { program, browser, parentOpts } = createBrowserProgram();
@ -56,9 +55,7 @@ function createProgram() {
describe("browser manage output", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
mocks.runtimeLog.mockClear();
mocks.runtimeError.mockClear();
mocks.runtimeExit.mockClear();
getRuntimeCapture().resetRuntimeCapture();
});
it("shows chrome-mcp transport for existing-session status without fake CDP fields", async () => {
@ -91,7 +88,7 @@ describe("browser manage output", () => {
from: "user",
});
const output = mocks.runtimeLog.mock.calls.at(-1)?.[0] as string;
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("cdpPort:");
expect(output).not.toContain("cdpUrl:");
@ -127,7 +124,7 @@ describe("browser manage output", () => {
from: "user",
});
const output = mocks.runtimeLog.mock.calls.at(-1)?.[0] as string;
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain(
"userDataDir: /Users/test/Library/Application Support/BraveSoftware/Brave-Browser",
);
@ -158,7 +155,7 @@ describe("browser manage output", () => {
const program = createProgram();
await program.parseAsync(["browser", "profiles"], { from: "user" });
const output = mocks.runtimeLog.mock.calls.at(-1)?.[0] as string;
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain("chrome-live: running (2 tabs) [existing-session]");
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("port: 0");
@ -186,7 +183,7 @@ describe("browser manage output", () => {
{ from: "user" },
);
const output = mocks.runtimeLog.mock.calls.at(-1)?.[0] as string;
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain('Created profile "chrome-live"');
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("port: 0");
@ -223,7 +220,7 @@ describe("browser manage output", () => {
from: "user",
});
const output = mocks.runtimeLog.mock.calls.at(-1)?.[0] as string;
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain("cdpUrl: https://example.com/chrome?token=supers…7890");
expect(output).not.toContain("alice");
expect(output).not.toContain("supersecretpasswordvalue1234");

View File

@ -1,11 +1,18 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
import { createBrowserProgram } from "./browser-cli-test-helpers.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
const runtimeState = vi.hoisted(() => ({ capture: null as CliRuntimeCapture | null }));
function getRuntimeCapture(): CliRuntimeCapture {
if (!runtimeState.capture) {
throw new Error("runtime capture not initialized");
}
return runtimeState.capture;
}
const mocks = vi.hoisted(() => {
const runtimeLog = vi.fn();
const runtimeError = vi.fn();
const runtimeExit = vi.fn();
return {
callBrowserRequest: vi.fn(async (_opts: unknown, req: { path?: string }) =>
req.path === "/"
@ -22,20 +29,6 @@ const mocks = vi.hoisted(() => {
}
: {},
),
runtimeLog,
runtimeError,
runtimeExit,
runtime: {
log: runtimeLog,
error: runtimeError,
writeStdout: vi.fn((value: string) =>
runtimeLog(value.endsWith("\n") ? value.slice(0, -1) : value),
),
writeJson: vi.fn((value: unknown, space = 2) =>
runtimeLog(JSON.stringify(value, null, space)),
),
exit: runtimeExit,
},
};
});
@ -51,9 +44,11 @@ vi.mock("./cli-utils.js", () => ({
) => await action().catch(onError),
}));
vi.mock("../runtime.js", () => ({
defaultRuntime: mocks.runtime,
}));
vi.mock("../runtime.js", async () => {
const { createCliRuntimeCapture } = await import("./test-runtime-capture.js");
runtimeState.capture ??= createCliRuntimeCapture();
return { defaultRuntime: runtimeState.capture.defaultRuntime };
});
describe("browser manage start timeout option", () => {
function createProgram() {
@ -65,9 +60,7 @@ describe("browser manage start timeout option", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
mocks.runtimeLog.mockClear();
mocks.runtimeError.mockClear();
mocks.runtimeExit.mockClear();
getRuntimeCapture().resetRuntimeCapture();
});
it("uses parent --timeout for browser start instead of hardcoded 15s", async () => {

View File

@ -1,21 +1,24 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBrowserStateCommands } from "./browser-cli-state.js";
import { createBrowserProgram as createBrowserProgramShared } from "./browser-cli-test-helpers.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
const runtimeState = vi.hoisted(() => ({ capture: null as CliRuntimeCapture | null }));
function getRuntimeCapture(): CliRuntimeCapture {
if (!runtimeState.capture) {
throw new Error("runtime capture not initialized");
}
return runtimeState.capture;
}
function getRuntime() {
return getRuntimeCapture().defaultRuntime;
}
const mocks = vi.hoisted(() => ({
callBrowserRequest: vi.fn(async (..._args: unknown[]) => ({ ok: true })),
runBrowserResizeWithOutput: vi.fn(async (_params: unknown) => {}),
runtime: {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn((value: string) => {
mocks.runtime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
mocks.runtime.log(JSON.stringify(value, null, space));
}),
exit: vi.fn(),
},
}));
vi.mock("./browser-cli-shared.js", () => ({
@ -26,9 +29,11 @@ vi.mock("./browser-cli-resize.js", () => ({
runBrowserResizeWithOutput: mocks.runBrowserResizeWithOutput,
}));
vi.mock("../runtime.js", () => ({
defaultRuntime: mocks.runtime,
}));
vi.mock("../runtime.js", async () => {
const { createCliRuntimeCapture } = await import("./test-runtime-capture.js");
runtimeState.capture ??= createCliRuntimeCapture();
return { defaultRuntime: runtimeState.capture.defaultRuntime };
});
describe("browser state option collisions", () => {
const createStateProgram = ({ withGatewayUrl = false } = {}) => {
@ -59,11 +64,8 @@ describe("browser state option collisions", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
mocks.runBrowserResizeWithOutput.mockClear();
mocks.runtime.log.mockClear();
mocks.runtime.error.mockClear();
mocks.runtime.writeStdout.mockClear();
mocks.runtime.writeJson.mockClear();
mocks.runtime.exit.mockClear();
getRuntimeCapture().resetRuntimeCapture();
getRuntime().exit.mockImplementation(() => {});
});
it("forwards parent-captured --target-id on `browser cookies set`", async () => {
@ -143,37 +145,37 @@ describe("browser state option collisions", () => {
await runBrowserCommand(["set", "offline", "maybe"]);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
expect(mocks.runtime.error).toHaveBeenCalledWith(expect.stringContaining("Expected on|off"));
expect(mocks.runtime.exit).toHaveBeenCalledWith(1);
expect(getRuntime().error).toHaveBeenCalledWith(expect.stringContaining("Expected on|off"));
expect(getRuntime().exit).toHaveBeenCalledWith(1);
});
it("errors when set media receives an invalid value", async () => {
await runBrowserCommand(["set", "media", "sepia"]);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
expect(mocks.runtime.error).toHaveBeenCalledWith(
expect(getRuntime().error).toHaveBeenCalledWith(
expect.stringContaining("Expected dark|light|none"),
);
expect(mocks.runtime.exit).toHaveBeenCalledWith(1);
expect(getRuntime().exit).toHaveBeenCalledWith(1);
});
it("errors when headers JSON is missing", async () => {
await runBrowserCommand(["set", "headers"]);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
expect(mocks.runtime.error).toHaveBeenCalledWith(
expect(getRuntime().error).toHaveBeenCalledWith(
expect.stringContaining("Missing headers JSON"),
);
expect(mocks.runtime.exit).toHaveBeenCalledWith(1);
expect(getRuntime().exit).toHaveBeenCalledWith(1);
});
it("errors when headers JSON is not an object", async () => {
await runBrowserCommand(["set", "headers", "--json", "[]"]);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
expect(mocks.runtime.error).toHaveBeenCalledWith(
expect(getRuntime().error).toHaveBeenCalledWith(
expect.stringContaining("Headers JSON must be a JSON object"),
);
expect(mocks.runtime.exit).toHaveBeenCalledWith(1);
expect(getRuntime().exit).toHaveBeenCalledWith(1);
});
});

View File

@ -1,7 +1,9 @@
import { Command } from "commander";
import { describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
const CRON_CLI_TEST_TIMEOUT_MS = 15_000;
const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
const defaultGatewayMock = async (
method: string,
@ -25,24 +27,9 @@ vi.mock("./gateway-rpc.js", async () => {
};
});
vi.mock("../runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../runtime.js")>();
const log = vi.fn();
return {
...actual,
defaultRuntime: {
...actual.defaultRuntime,
log,
error: vi.fn(),
writeStdout: (value: string) => log(value.endsWith("\n") ? value.slice(0, -1) : value),
writeJson: (value: unknown, space = 2) =>
log(JSON.stringify(value, null, space > 0 ? space : undefined)),
exit: (code: number) => {
throw new Error(`__exit__:${code}`);
},
},
};
});
vi.mock("../runtime.js", () => ({
defaultRuntime,
}));
const { registerCronCli } = await import("./cron-cli.js");
@ -85,6 +72,7 @@ function buildProgram() {
function resetGatewayMock() {
callGatewayFromCli.mockClear();
callGatewayFromCli.mockImplementation(defaultGatewayMock);
resetRuntimeCapture();
}
async function runCronCommand(args: string[]): Promise<void> {

View File

@ -3,9 +3,9 @@ import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { makeTempWorkspace } from "../../test-helpers/workspace.js";
import { captureEnv } from "../../test-utils/env.js";
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
const runtimeLogs: string[] = [];
const runtimeErrors: string[] = [];
const { runtimeLogs, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
const serviceMock = vi.hoisted(() => ({
label: "Gateway",
@ -24,24 +24,9 @@ vi.mock("../../daemon/service.js", () => ({
resolveGatewayService: () => serviceMock,
}));
vi.mock("../../runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../runtime.js")>();
return {
...actual,
defaultRuntime: {
...actual.defaultRuntime,
log: (message: string) => runtimeLogs.push(message),
error: (message: string) => runtimeErrors.push(message),
writeStdout: (value: string) =>
runtimeLogs.push(value.endsWith("\n") ? value.slice(0, -1) : value),
writeJson: (value: unknown, space = 2) =>
runtimeLogs.push(JSON.stringify(value, null, space > 0 ? space : undefined)),
exit: (code: number) => {
throw new Error(`__exit__:${code}`);
},
},
};
});
vi.mock("../../runtime.js", () => ({
defaultRuntime,
}));
const { runDaemonInstall } = await import("./install.js");
const { clearConfigCache } = await import("../../config/config.js");
@ -78,9 +63,8 @@ describe("runDaemonInstall integration", () => {
});
beforeEach(async () => {
runtimeLogs.length = 0;
runtimeErrors.length = 0;
vi.clearAllMocks();
resetRuntimeCapture();
// Keep these defined-but-empty so dotenv won't repopulate from local .env.
process.env.OPENCLAW_GATEWAY_TOKEN = "";
process.env.CLAWDBOT_GATEWAY_TOKEN = "";

View File

@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { captureFullEnv } from "../../test-utils/env.js";
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
import type { DaemonActionResponse } from "./response.js";
const resolveNodeStartupTlsEnvironmentMock = vi.hoisted(() => vi.fn());
@ -125,19 +126,9 @@ vi.mock("./response.js", () => ({
installDaemonServiceAndEmit: installDaemonServiceAndEmitMock,
}));
const runtimeLogs: string[] = [];
const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("../../runtime.js", () => ({
defaultRuntime: {
log: (message: string) => runtimeLogs.push(message),
writeStdout: (value: string) => {
runtimeLogs.push(value.endsWith("\n") ? value.slice(0, -1) : value);
},
writeJson: (value: unknown, space = 2) => {
runtimeLogs.push(JSON.stringify(value, null, space));
},
error: vi.fn(),
exit: vi.fn(),
},
defaultRuntime,
}));
function expectFirstInstallPlanCallOmitsToken() {
@ -176,7 +167,7 @@ describe("runDaemonInstall", () => {
isGatewayDaemonRuntimeMock.mockReset();
installDaemonServiceAndEmitMock.mockReset();
service.isLoaded.mockReset();
runtimeLogs.length = 0;
resetRuntimeCapture();
actionState.warnings.length = 0;
actionState.emitted.length = 0;
actionState.failed.length = 0;

View File

@ -1,6 +1,9 @@
import { Command } from "commander";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture();
runtime.exit.mockImplementation(() => {});
const callGateway = vi.fn();
const buildGatewayConnectionDetails = vi.fn(() => ({
url: "ws://127.0.0.1:18789",
@ -11,18 +14,6 @@ const listDevicePairing = vi.fn();
const approveDevicePairing = vi.fn();
const summarizeDeviceTokens = vi.fn();
const withProgress = vi.fn(async (_opts: unknown, fn: () => Promise<unknown>) => await fn());
const runtime = {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn((value: string) => {
runtime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
runtime.log(JSON.stringify(value, null, space > 0 ? space : undefined));
}),
exit: vi.fn(),
};
vi.mock("../gateway/call.js", () => ({
callGateway,
buildGatewayConnectionDetails,
@ -59,6 +50,11 @@ async function runDevicesCommand(argv: string[]) {
await program.parseAsync(["devices", ...argv], { from: "user" });
}
function readRuntimeCallText(call: unknown[] | undefined): string {
const value = call?.[0];
return typeof value === "string" ? value : "";
}
describe("devices cli approve", () => {
it("approves an explicit request id without listing", async () => {
callGateway.mockResolvedValueOnce({ device: { deviceId: "device-1" } });
@ -312,13 +308,14 @@ describe("devices cli list", () => {
await runDevicesCommand(["list"]);
const output = runtime.log.mock.calls.map((entry) => String(entry[0] ?? "")).join("\n");
const output = runtime.log.mock.calls.map((entry) => readRuntimeCallText(entry)).join("\n");
expect(output).toContain("Scopes");
expect(output).toContain("operator.admin, operator.read");
});
});
afterEach(() => {
resetRuntimeCapture();
callGateway.mockClear();
buildGatewayConnectionDetails.mockClear();
buildGatewayConnectionDetails.mockReturnValue({

View File

@ -1,6 +1,16 @@
import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerDirectoryCli } from "./directory-cli.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
const runtimeState = vi.hoisted(() => ({ capture: null as CliRuntimeCapture | null }));
function getRuntimeCapture(): CliRuntimeCapture {
if (!runtimeState.capture) {
throw new Error("runtime capture not initialized");
}
return runtimeState.capture;
}
const mocks = vi.hoisted(() => ({
loadConfig: vi.fn(),
@ -9,9 +19,6 @@ const mocks = vi.hoisted(() => ({
resolveMessageChannelSelection: vi.fn(),
getChannelPlugin: vi.fn(),
resolveChannelDefaultAccountId: vi.fn(),
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
}));
vi.mock("../config/config.js", () => ({
@ -35,25 +42,16 @@ vi.mock("../channels/plugins/helpers.js", () => ({
resolveChannelDefaultAccountId: mocks.resolveChannelDefaultAccountId,
}));
vi.mock("../runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../runtime.js")>();
return {
...actual,
defaultRuntime: {
...actual.defaultRuntime,
log: (...args: unknown[]) => mocks.log(...args),
error: (...args: unknown[]) => mocks.error(...args),
writeStdout: (value: string) => mocks.log(value.endsWith("\n") ? value.slice(0, -1) : value),
writeJson: (value: unknown, space = 2) =>
mocks.log(JSON.stringify(value, null, space > 0 ? space : undefined)),
exit: (...args: unknown[]) => mocks.exit(...args),
},
};
vi.mock("../runtime.js", async () => {
const { createCliRuntimeCapture } = await import("./test-runtime-capture.js");
runtimeState.capture ??= createCliRuntimeCapture();
return { defaultRuntime: runtimeState.capture.defaultRuntime };
});
describe("registerDirectoryCli", () => {
beforeEach(() => {
vi.clearAllMocks();
getRuntimeCapture().resetRuntimeCapture();
mocks.loadConfig.mockReturnValue({ channels: {} });
mocks.writeConfigFile.mockResolvedValue(undefined);
mocks.resolveChannelDefaultAccountId.mockReturnValue("default");
@ -62,8 +60,8 @@ describe("registerDirectoryCli", () => {
configured: ["slack"],
source: "explicit",
});
mocks.exit.mockImplementation((code?: number) => {
throw new Error(`exit:${code ?? 0}`);
getRuntimeCapture().defaultRuntime.exit.mockImplementation((code: number) => {
throw new Error(`exit:${code}`);
});
});
@ -105,9 +103,9 @@ describe("registerDirectoryCli", () => {
accountId: "default",
}),
);
expect(mocks.log).toHaveBeenCalledWith(
expect(getRuntimeCapture().defaultRuntime.log).toHaveBeenCalledWith(
JSON.stringify({ id: "self-1", name: "Family Phone" }, null, 2),
);
expect(mocks.error).not.toHaveBeenCalled();
expect(getRuntimeCapture().defaultRuntime.error).not.toHaveBeenCalled();
});
});

View File

@ -4,28 +4,15 @@ import path from "node:path";
import { Command } from "commander";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempHome } from "../config/home-env.test-harness.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
const mockLog = vi.fn();
const mockError = vi.fn();
const mockExit = vi.fn((code: number) => {
throw new Error(`__exit__:${code}`);
});
const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
const mockLog = defaultRuntime.log;
const mockError = defaultRuntime.error;
vi.mock("../runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../runtime.js")>();
return {
...actual,
defaultRuntime: {
...actual.defaultRuntime,
log: (...args: unknown[]) => mockLog(...args),
error: (...args: unknown[]) => mockError(...args),
writeStdout: (value: string) => mockLog(value.endsWith("\n") ? value.slice(0, -1) : value),
writeJson: (value: unknown, space = 2) =>
mockLog(JSON.stringify(value, null, space > 0 ? space : undefined)),
exit: (code: number) => mockExit(code),
},
};
});
vi.mock("../runtime.js", () => ({
defaultRuntime,
}));
const tempDirs: string[] = [];
@ -52,6 +39,7 @@ describe("mcp cli", () => {
beforeEach(() => {
vi.clearAllMocks();
resetRuntimeCapture();
});
afterEach(async () => {

View File

@ -1,5 +1,6 @@
import { Command } from "commander";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
const agentCliCommandMock = vi.fn();
const agentsAddCommandMock = vi.fn();
@ -12,17 +13,7 @@ const agentsUnbindCommandMock = vi.fn();
const setVerboseMock = vi.fn();
const createDefaultDepsMock = vi.fn(() => ({ deps: true }));
const runtime = {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn((value: string) => {
runtime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
runtime.log(JSON.stringify(value, null, space));
}),
exit: vi.fn(),
};
const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("../../commands/agent-via-gateway.js", () => ({
agentCliCommand: agentCliCommandMock,
@ -80,6 +71,8 @@ describe("registerAgentCommands", () => {
beforeEach(() => {
vi.clearAllMocks();
resetRuntimeCapture();
runtime.exit.mockImplementation(() => {});
agentCliCommandMock.mockResolvedValue(undefined);
agentsAddCommandMock.mockResolvedValue(undefined);
agentsBindingsCommandMock.mockResolvedValue(undefined);

View File

@ -1,20 +1,11 @@
import { Command } from "commander";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
const backupCreateCommand = vi.fn();
const backupVerifyCommand = vi.fn();
const runtime = {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn((value: string) => {
runtime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
runtime.log(JSON.stringify(value, null, space));
}),
exit: vi.fn(),
};
const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("../../commands/backup.js", () => ({
backupCreateCommand,
@ -56,6 +47,7 @@ describe("registerBackupCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
resetRuntimeCapture();
backupCreateCommand.mockResolvedValue(undefined);
backupVerifyCommand.mockResolvedValue(undefined);
});

View File

@ -1,5 +1,6 @@
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
const statusCommand = vi.fn();
const healthCommand = vi.fn();
@ -7,17 +8,7 @@ const sessionsCommand = vi.fn();
const sessionsCleanupCommand = vi.fn();
const setVerbose = vi.fn();
const runtime = {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn((value: string) => {
runtime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
runtime.log(JSON.stringify(value, null, space));
}),
exit: vi.fn(),
};
const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("../../commands/status.js", () => ({
statusCommand,
@ -58,6 +49,8 @@ describe("registerStatusHealthSessionsCommands", () => {
beforeEach(() => {
vi.clearAllMocks();
resetRuntimeCapture();
runtime.exit.mockImplementation(() => {});
statusCommand.mockResolvedValue(undefined);
healthCommand.mockResolvedValue(undefined);
sessionsCommand.mockResolvedValue(undefined);

View File

@ -1,21 +1,12 @@
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { encodePairingSetupCode } from "../pairing/setup-code.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
const runtimeCapture = createCliRuntimeCapture();
const runtime = runtimeCapture.defaultRuntime;
const mocks = vi.hoisted(() => ({
runtime: {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn((value: string) => {
mocks.runtime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
mocks.runtime.log(JSON.stringify(value, null, space > 0 ? space : undefined));
}),
exit: vi.fn(() => {
throw new Error("exit");
}),
},
loadConfig: vi.fn(),
runCommandWithTimeout: vi.fn(),
resolveCommandSecretRefsViaGateway: vi.fn(async ({ config }: { config: unknown }) => ({
@ -27,10 +18,7 @@ const mocks = vi.hoisted(() => ({
}),
}));
vi.mock("../runtime.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../runtime.js")>()),
defaultRuntime: mocks.runtime,
}));
vi.mock("../runtime.js", () => ({ defaultRuntime: runtime }));
vi.mock("../config/config.js", () => ({ loadConfig: mocks.loadConfig }));
vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: mocks.runCommandWithTimeout }));
vi.mock("./command-secret-gateway.js", () => ({
@ -48,7 +36,6 @@ vi.mock("qrcode-terminal", () => ({
},
}));
const runtime = mocks.runtime;
const loadConfig = mocks.loadConfig;
const runCommandWithTimeout = mocks.runCommandWithTimeout;
const resolveCommandSecretRefsViaGateway = mocks.resolveCommandSecretRefsViaGateway;
@ -135,8 +122,17 @@ describe("registerQrCli", () => {
await expect(runQr(args)).rejects.toThrow("exit");
}
function readRuntimeCallText(call: unknown[] | undefined): string {
const value = call?.[0];
if (typeof value === "string") {
return value;
}
return value === undefined ? "" : JSON.stringify(value);
}
function parseLastLoggedQrJson() {
return JSON.parse(String(runtime.log.mock.calls.at(-1)?.[0] ?? "{}")) as {
const raw = runtime.log.mock.calls.at(-1)?.[0];
return JSON.parse(typeof raw === "string" ? raw : "{}") as {
setupCode?: string;
gatewayUrl?: string;
auth?: string;
@ -166,10 +162,14 @@ describe("registerQrCli", () => {
beforeEach(() => {
vi.clearAllMocks();
runtimeCapture.resetRuntimeCapture();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", "");
vi.stubEnv("CLAWDBOT_GATEWAY_PASSWORD", "");
runtime.exit.mockImplementation(() => {
throw new Error("exit");
});
});
afterEach(() => {
@ -208,7 +208,7 @@ describe("registerQrCli", () => {
await runQr([]);
expect(qrGenerate).toHaveBeenCalledTimes(1);
const output = runtime.log.mock.calls.map((call) => String(call[0] ?? "")).join("\n");
const output = runtime.log.mock.calls.map((call) => readRuntimeCallText(call)).join("\n");
expect(output).toContain("Pairing QR");
expect(output).toContain("ASCII-QR");
expect(output).toContain("Gateway:");
@ -316,7 +316,7 @@ describe("registerQrCli", () => {
});
await expectQrExit(["--setup-code-only"]);
const output = runtime.error.mock.calls.map((call) => String(call[0] ?? "")).join("\n");
const output = runtime.error.mock.calls.map((call) => readRuntimeCallText(call)).join("\n");
expect(output).toContain("gateway.auth.mode is unset");
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
});
@ -331,7 +331,7 @@ describe("registerQrCli", () => {
await expectQrExit([]);
const output = runtime.error.mock.calls.map((call) => String(call[0] ?? "")).join("\n");
const output = runtime.error.mock.calls.map((call) => readRuntimeCallText(call)).join("\n");
expect(output).toContain("only bound to loopback");
});
@ -363,7 +363,7 @@ describe("registerQrCli", () => {
expect(
runtime.log.mock.calls.some((call) =>
String(call[0] ?? "").includes("gateway.remote.token inactive"),
readRuntimeCallText(call).includes("gateway.remote.token inactive"),
),
).toBe(true);
});
@ -379,7 +379,7 @@ describe("registerQrCli", () => {
expect(
runtime.error.mock.calls.some((call) =>
String(call[0] ?? "").includes("gateway.remote.token inactive"),
readRuntimeCallText(call).includes("gateway.remote.token inactive"),
),
).toBe(true);
const expected = encodePairingSetupCode({
@ -419,7 +419,7 @@ describe("registerQrCli", () => {
expect(payload.gatewayUrl).toBe("wss://remote.example.com:444");
expect(
runtime.error.mock.calls.some((call) =>
String(call[0] ?? "").includes("gateway.remote.password inactive"),
readRuntimeCallText(call).includes("gateway.remote.password inactive"),
),
).toBe(true);
});
@ -434,7 +434,7 @@ describe("registerQrCli", () => {
});
await expectQrExit(["--remote"]);
const output = runtime.error.mock.calls.map((call) => String(call[0] ?? "")).join("\n");
const output = runtime.error.mock.calls.map((call) => readRuntimeCallText(call)).join("\n");
expect(output).toContain("qr --remote requires");
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
});
@ -461,7 +461,8 @@ describe("registerQrCli", () => {
await runQr(["--json", "--remote"]);
const payload = JSON.parse(String(runtime.log.mock.calls.at(-1)?.[0] ?? "{}")) as {
const raw = runtime.log.mock.calls.at(-1)?.[0];
const payload = JSON.parse(typeof raw === "string" ? raw : "{}") as {
gatewayUrl?: string;
auth?: string;
urlSource?: string;

View File

@ -1,22 +1,13 @@
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runRegisteredCli } from "../test-utils/command-runner.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
const updateCommand = vi.fn(async (_opts: unknown) => {});
const updateStatusCommand = vi.fn(async (_opts: unknown) => {});
const updateWizardCommand = vi.fn(async (_opts: unknown) => {});
const defaultRuntime = {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn((value: string) => {
defaultRuntime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
defaultRuntime.log(JSON.stringify(value, null, space));
}),
exit: vi.fn(),
};
const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("./update-cli/update-command.js", () => ({
updateCommand: (opts: unknown) => updateCommand(opts),
@ -45,6 +36,7 @@ describe("update cli option collisions", () => {
updateCommand.mockClear();
updateStatusCommand.mockClear();
updateWizardCommand.mockClear();
resetRuntimeCapture();
defaultRuntime.log.mockClear();
defaultRuntime.error.mockClear();
defaultRuntime.writeStdout.mockClear();

View File

@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/types.openclaw.js";
import type { UpdateRunResult } from "../infra/update-runner.js";
import { withEnvAsync } from "../test-utils/env.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
const confirm = vi.fn();
const select = vi.fn();
@ -25,6 +26,7 @@ const formatPortDiagnostics = vi.fn();
const pathExists = vi.fn();
const syncPluginsForUpdateChannel = vi.fn();
const updateNpmInstalledPlugins = vi.fn();
const { defaultRuntime: runtimeCapture, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("@clack/prompts", () => ({
confirm,
@ -129,22 +131,9 @@ vi.mock("./daemon-cli.js", () => ({
}));
// Mock the runtime
vi.mock("../runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../runtime.js")>();
const log = vi.fn();
return {
...actual,
defaultRuntime: {
...actual.defaultRuntime,
log,
error: vi.fn(),
writeStdout: (value: string) => log(value.endsWith("\n") ? value.slice(0, -1) : value),
writeJson: (value: unknown, space = 2) =>
log(JSON.stringify(value, null, space > 0 ? space : undefined)),
exit: vi.fn(),
},
};
});
vi.mock("../runtime.js", () => ({
defaultRuntime: runtimeCapture,
}));
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
@ -298,6 +287,8 @@ describe("update-cli", () => {
beforeEach(() => {
vi.clearAllMocks();
resetRuntimeCapture();
vi.mocked(defaultRuntime.exit).mockImplementation(() => {});
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(process.cwd());
vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
vi.mocked(fetchNpmTagVersion).mockResolvedValue({