From 51eb877a15fec3e7749208e34d1b45eb5a4b80a8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Apr 2026 21:53:02 +0100 Subject: [PATCH] test(ci): stabilize qr cli runtime mocks --- src/cli/qr-cli.test.ts | 41 ++++++++++++++--- src/cli/qr-dashboard.integration.test.ts | 57 ++++-------------------- 2 files changed, 45 insertions(+), 53 deletions(-) diff --git a/src/cli/qr-cli.test.ts b/src/cli/qr-cli.test.ts index 00fd8af6d9c..be383d04ec9 100644 --- a/src/cli/qr-cli.test.ts +++ b/src/cli/qr-cli.test.ts @@ -1,10 +1,40 @@ 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"; +import type { OutputRuntimeEnv } from "../runtime.js"; -const runtimeCapture = createCliRuntimeCapture(); -const runtime = runtimeCapture.defaultRuntime; +const runtimeState = vi.hoisted(() => { + const runtimeLogs: string[] = []; + const runtimeErrors: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime: OutputRuntimeEnv = { + log: vi.fn((...args: unknown[]) => { + runtimeLogs.push(stringifyArgs(args)); + }), + error: vi.fn((...args: unknown[]) => { + runtimeErrors.push(stringifyArgs(args)); + }), + writeStdout: vi.fn((value: string) => { + const normalized = value.endsWith("\n") ? value.slice(0, -1) : value; + defaultRuntime.log(normalized); + }), + writeJson: vi.fn((value: unknown, space = 2) => { + defaultRuntime.log(JSON.stringify(value, null, space > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { + runtimeLogs, + runtimeErrors, + defaultRuntime, + resetRuntimeCapture: () => { + runtimeLogs.length = 0; + runtimeErrors.length = 0; + }, + }; +}); const mocks = vi.hoisted(() => ({ loadConfig: vi.fn(), @@ -17,8 +47,9 @@ const mocks = vi.hoisted(() => ({ cb("ASCII-QR"); }), })); +const runtime = runtimeState.defaultRuntime; -vi.mock("../runtime.js", () => ({ defaultRuntime: runtime })); +vi.mock("../runtime.js", () => ({ defaultRuntime: runtimeState.defaultRuntime })); vi.mock("../config/config.js", () => ({ loadConfig: mocks.loadConfig })); vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: mocks.runCommandWithTimeout })); vi.mock("./command-secret-gateway.js", () => ({ @@ -162,7 +193,7 @@ describe("registerQrCli", () => { beforeEach(() => { vi.clearAllMocks(); - runtimeCapture.resetRuntimeCapture(); + runtimeState.resetRuntimeCapture(); vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", ""); vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", ""); runtime.exit.mockImplementation(() => { diff --git a/src/cli/qr-dashboard.integration.test.ts b/src/cli/qr-dashboard.integration.test.ts index bd0d7cfcc8a..bb489f9bae1 100644 --- a/src/cli/qr-dashboard.integration.test.ts +++ b/src/cli/qr-dashboard.integration.test.ts @@ -89,41 +89,6 @@ async function runCli(args: string[]): Promise { await program.parseAsync(args, { from: "user" }); } -const mockedModuleIds = ["../config/config.js", "../infra/clipboard.js", "../runtime.js"]; - -const unmockedDependencyIds = [ - "../commands/dashboard.js", - "../gateway/resolve-configured-secret-input-string.js", - "../pairing/setup-code.js", - "./command-secret-gateway.js", - "./qr-cli.js", -] as const; - -async function loadCliModules() { - vi.resetModules(); - for (const id of unmockedDependencyIds) { - vi.doUnmock(id); - } - vi.doMock("../config/config.js", async () => { - const actual = - await vi.importActual("../config/config.js"); - return { - ...actual, - loadConfig: loadConfigMock, - readConfigFileSnapshot: readConfigFileSnapshotMock, - resolveGatewayPort: resolveGatewayPortMock, - }; - }); - vi.doMock("../infra/clipboard.js", () => ({ - copyToClipboard: copyToClipboardMock, - })); - vi.doMock("../runtime.js", () => ({ - defaultRuntime: runtime, - })); - ({ dashboardCommand } = await import("../commands/dashboard.js")); - ({ registerQrCli } = await import("./qr-cli.js")); -} - describe("cli integration: qr + dashboard token SecretRef", () => { let envSnapshot: ReturnType; @@ -134,27 +99,19 @@ describe("cli integration: qr + dashboard token SecretRef", () => { "OPENCLAW_GATEWAY_PASSWORD", ]); }); - - afterAll(() => { - envSnapshot.restore(); - vi.restoreAllMocks(); - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - for (const id of unmockedDependencyIds) { - vi.doUnmock(id); - } - vi.resetModules(); + beforeAll(async () => { + ({ dashboardCommand } = await import("../commands/dashboard.js")); + ({ registerQrCli } = await import("./qr-cli.js")); }); - beforeEach(async () => { + beforeEach(() => { runtimeLogs.length = 0; runtimeErrors.length = 0; vi.clearAllMocks(); + runtime.exit.mockImplementation(() => {}); delete process.env.OPENCLAW_GATEWAY_TOKEN; delete process.env.OPENCLAW_GATEWAY_PASSWORD; delete process.env.SHARED_GATEWAY_TOKEN; - await loadCliModules(); }); it("uses the same resolved token SecretRef for qr auth validation and dashboard commands", async () => { @@ -214,4 +171,8 @@ describe("cli integration: qr + dashboard token SecretRef", () => { expect(joined).toContain("Token auto-auth unavailable"); expect(joined).toContain("Set OPENCLAW_GATEWAY_TOKEN"); }); + + afterAll(() => { + envSnapshot.restore(); + }); });