From 3f1d6fe14784fe193c09e95e76ce1d349f0728fc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 31 Mar 2026 02:12:23 +0100 Subject: [PATCH] test: speed up cli and command suites --- src/cli/acp-cli.option-collisions.test.ts | 36 ++- src/cli/banner.test.ts | 11 +- src/cli/channel-options.test.ts | 10 +- src/cli/cli-utils.test.ts | 3 +- src/cli/command-secret-gateway.test.ts | 18 +- src/cli/cron-cli.test.ts | 41 ++- src/cli/daemon-cli.coverage.test.ts | 36 ++- src/cli/daemon-cli/probe.test.ts | 3 +- src/cli/daemon-cli/status.gather.test.ts | 3 +- src/cli/daemon-cli/status.print.test.ts | 3 +- src/cli/devices-cli.test.ts | 77 +++--- src/cli/directory-cli.test.ts | 54 ++-- src/cli/exec-approvals-cli.test.ts | 62 +++-- src/cli/gateway-cli.coverage.test.ts | 56 +++-- .../register.option-collisions.test.ts | 43 ++-- src/cli/logs-cli.test.ts | 10 +- src/cli/mcp-cli.test.ts | 36 ++- src/cli/models-cli.test.ts | 66 +++-- src/cli/nodes-camera.test.ts | 10 +- src/cli/nodes-cli.coverage.test.ts | 48 +++- ....invoke.approval-transport-timeout.test.ts | 16 +- src/cli/pairing-cli.test.ts | 72 +++--- src/cli/plugin-registry.test.ts | 11 +- src/cli/plugin-registry.ts | 6 + src/cli/plugins-install-config.test.ts | 14 +- src/cli/program.nodes-basic.e2e.test.ts | 17 +- src/cli/program.smoke.test.ts | 10 +- src/cli/program/action-reparse.test.ts | 7 +- src/cli/program/build-program.test.ts | 13 +- .../build-program.version-alias.test.ts | 3 +- src/cli/program/command-registry.test.ts | 14 +- src/cli/program/config-guard.test.ts | 28 +-- src/cli/program/context.test.ts | 5 +- src/cli/program/help.test.ts | 13 +- src/cli/program/preaction.test.ts | 18 +- src/cli/program/register.agent.test.ts | 87 +++---- src/cli/program/register.backup.test.ts | 45 ++-- src/cli/program/register.configure.test.ts | 38 ++- src/cli/program/register.maintenance.test.ts | 55 ++-- src/cli/program/register.message.test.ts | 98 ++++---- src/cli/program/register.onboard.test.ts | 45 ++-- src/cli/program/register.setup.test.ts | 48 ++-- .../register.status-health-sessions.test.ts | 77 +++--- src/cli/program/register.subclis.test.ts | 19 +- src/cli/program/root-help.test.ts | 3 +- src/cli/prompt.test.ts | 54 ++-- src/cli/run-main.exit.test.ts | 3 +- src/cli/secrets-cli.test.ts | 79 ++++-- src/cli/security-cli.test.ts | 65 +++-- src/cli/skills-cli.commands.test.ts | 84 +++++-- src/cli/update-cli.option-collisions.test.ts | 36 +-- .../update-cli/shared.command-runner.test.ts | 5 +- src/commands/agent.delivery.test.ts | 10 +- src/commands/agent/session.test.ts | 10 +- src/commands/agents.bind.test-support.ts | 6 +- src/commands/backup.atomic.test.ts | 3 +- src/commands/channels.add.test.ts | 8 +- src/commands/channels.remove.test.ts | 8 +- src/commands/channels.resolve.test.ts | 3 +- .../channels.status.command-flow.test.ts | 3 +- src/commands/config-validation.test.ts | 14 +- src/commands/configure.daemon.test.ts | 3 +- src/commands/dashboard.links.test.ts | 7 +- src/commands/dashboard.test.ts | 6 +- src/commands/doctor-workspace-status.test.ts | 38 +-- src/commands/doctor.matrix-migration.test.ts | 8 +- .../shared/allowlist-policy-repair.test.ts | 7 +- src/commands/gateway-install-token.test.ts | 3 +- src/commands/gateway-status.test.ts | 237 +++++++++--------- src/commands/health.snapshot.test.ts | 1 - src/commands/message.default-agent.test.ts | 4 +- src/commands/message.test.ts | 1 - .../list.list-command.forward-compat.test.ts | 1 - src/commands/models/shared.test.ts | 8 +- .../local/daemon-install.test.ts | 3 +- src/commands/onboard-remote.test.ts | 3 +- src/commands/onboard-search.providers.test.ts | 1 - src/commands/onboard.test.ts | 3 +- src/commands/reset.test.ts | 10 +- src/commands/sandbox-explain.test.ts | 3 +- src/commands/status-json.test.ts | 3 +- src/commands/status.summary.test.ts | 19 +- src/commands/tasks.test.ts | 67 ++--- 83 files changed, 1161 insertions(+), 1054 deletions(-) diff --git a/src/cli/acp-cli.option-collisions.test.ts b/src/cli/acp-cli.option-collisions.test.ts index 820467ba5a4..7bef752f1e5 100644 --- a/src/cli/acp-cli.option-collisions.test.ts +++ b/src/cli/acp-cli.option-collisions.test.ts @@ -1,36 +1,38 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { runRegisteredCli } from "../test-utils/command-runner.js"; import { withTempSecretFiles } from "../test-utils/secret-file-fixture.js"; +import { registerAcpCli } from "./acp-cli.js"; -const runAcpClientInteractive = vi.fn(async (_opts: unknown) => {}); -const serveAcpGateway = vi.fn(async (_opts: unknown) => {}); +const mocks = vi.hoisted(() => ({ + runAcpClientInteractive: vi.fn(async (_opts: unknown) => {}), + serveAcpGateway: vi.fn(async (_opts: unknown) => {}), + defaultRuntime: { + log: vi.fn(), + error: vi.fn(), + writeStdout: vi.fn(), + writeJson: vi.fn(), + exit: vi.fn(), + }, +})); -const defaultRuntime = { - log: vi.fn(), - error: vi.fn(), - writeStdout: vi.fn(), - writeJson: vi.fn(), - exit: vi.fn(), -}; +const { runAcpClientInteractive, serveAcpGateway, defaultRuntime } = mocks; const passwordKey = () => ["pass", "word"].join(""); vi.mock("../acp/client.js", () => ({ - runAcpClientInteractive: (opts: unknown) => runAcpClientInteractive(opts), + runAcpClientInteractive: (opts: unknown) => mocks.runAcpClientInteractive(opts), })); vi.mock("../acp/server.js", () => ({ - serveAcpGateway: (opts: unknown) => serveAcpGateway(opts), + serveAcpGateway: (opts: unknown) => mocks.serveAcpGateway(opts), })); vi.mock("../runtime.js", () => ({ - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); describe("acp cli option collisions", () => { - let registerAcpCli: typeof import("./acp-cli.js").registerAcpCli; - function createAcpProgram() { const program = new Command(); registerAcpCli(program); @@ -48,10 +50,6 @@ describe("acp cli option collisions", () => { expect(defaultRuntime.exit).toHaveBeenCalledWith(1); } - beforeAll(async () => { - ({ registerAcpCli } = await import("./acp-cli.js")); - }); - beforeEach(() => { runAcpClientInteractive.mockClear(); serveAcpGateway.mockClear(); diff --git a/src/cli/banner.test.ts b/src/cli/banner.test.ts index 722a574f49f..c5432159371 100644 --- a/src/cli/banner.test.ts +++ b/src/cli/banner.test.ts @@ -1,17 +1,12 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { formatCliBannerLine } from "./banner.js"; -const readCliBannerTaglineModeMock = vi.fn(); +const readCliBannerTaglineModeMock = vi.hoisted(() => vi.fn()); vi.mock("./banner-config-lite.js", () => ({ readCliBannerTaglineMode: readCliBannerTaglineModeMock, })); -let formatCliBannerLine: typeof import("./banner.js").formatCliBannerLine; - -beforeAll(async () => { - ({ formatCliBannerLine } = await import("./banner.js")); -}); - beforeEach(() => { readCliBannerTaglineModeMock.mockReset(); readCliBannerTaglineModeMock.mockReturnValue(undefined); diff --git a/src/cli/channel-options.test.ts b/src/cli/channel-options.test.ts index 7bd38043498..1a37f02e92b 100644 --- a/src/cli/channel-options.test.ts +++ b/src/cli/channel-options.test.ts @@ -1,4 +1,5 @@ -import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { __testing, resolveCliChannelOptions } from "./channel-options.js"; const readFileSyncMock = vi.hoisted(() => vi.fn()); @@ -19,13 +20,6 @@ vi.mock("../channels/registry.js", () => ({ CHAT_CHANNEL_ORDER: ["telegram", "discord"], })); -let resolveCliChannelOptions: typeof import("./channel-options.js").resolveCliChannelOptions; -let __testing: typeof import("./channel-options.js").__testing; - -beforeAll(async () => { - ({ resolveCliChannelOptions, __testing } = await import("./channel-options.js")); -}); - describe("resolveCliChannelOptions", () => { afterEach(() => { __testing.resetPrecomputedChannelOptionsForTests(); diff --git a/src/cli/cli-utils.test.ts b/src/cli/cli-utils.test.ts index 76247f54347..2f49382a00a 100644 --- a/src/cli/cli-utils.test.ts +++ b/src/cli/cli-utils.test.ts @@ -1,13 +1,12 @@ import { Command } from "commander"; import { describe, expect, it, vi } from "vitest"; +import { registerDnsCli } from "./dns-cli.js"; import { parseCanvasSnapshotPayload } from "./nodes-canvas.js"; import { parseByteSize } from "./parse-bytes.js"; import { parseDurationMs } from "./parse-duration.js"; import { shouldSkipRespawnForArgv } from "./respawn-policy.js"; import { waitForever } from "./wait.js"; -const { registerDnsCli } = await import("./dns-cli.js"); - describe("waitForever", () => { it("creates an unref'ed interval and returns a pending promise", () => { const setIntervalSpy = vi.spyOn(global, "setInterval"); diff --git a/src/cli/command-secret-gateway.test.ts b/src/cli/command-secret-gateway.test.ts index bb29a8a626a..0852179361c 100644 --- a/src/cli/command-secret-gateway.test.ts +++ b/src/cli/command-secret-gateway.test.ts @@ -1,10 +1,15 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { resolveCommandSecretRefsViaGateway } from "./command-secret-gateway.js"; -const callGateway = vi.fn(); +const mocks = vi.hoisted(() => ({ + callGateway: vi.fn(), +})); + +const { callGateway } = mocks; vi.mock("../gateway/call.js", () => ({ - callGateway, + callGateway: mocks.callGateway, })); vi.mock("../secrets/runtime-web-tools.js", () => ({ @@ -16,13 +21,6 @@ vi.mock("../utils/message-channel.js", () => ({ GATEWAY_CLIENT_NAMES: { CLI: "cli" }, })); -let resolveCommandSecretRefsViaGateway: typeof import("./command-secret-gateway.js").resolveCommandSecretRefsViaGateway; - -beforeAll(async () => { - vi.resetModules(); - ({ resolveCommandSecretRefsViaGateway } = await import("./command-secret-gateway.js")); -}); - beforeEach(() => { callGateway.mockReset(); }); diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts index c82d0b404d8..fc2ed26b96a 100644 --- a/src/cli/cron-cli.test.ts +++ b/src/cli/cron-cli.test.ts @@ -1,9 +1,29 @@ import { Command } from "commander"; import { describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { registerCronCli } from "./cron-cli.js"; const CRON_CLI_TEST_TIMEOUT_MS = 15_000; -const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture(); +const mocks = vi.hoisted(() => { + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { + defaultRuntime, + callGatewayFromCli: vi.fn(), + }; +}); + +const { defaultRuntime, callGatewayFromCli } = mocks; const defaultGatewayMock = async ( method: string, @@ -16,23 +36,21 @@ const defaultGatewayMock = async ( } return { ok: true, params }; }; -const callGatewayFromCli = vi.fn(defaultGatewayMock); +callGatewayFromCli.mockImplementation(defaultGatewayMock); vi.mock("./gateway-rpc.js", async () => { const actual = await vi.importActual("./gateway-rpc.js"); return { ...actual, callGatewayFromCli: (method: string, opts: unknown, params?: unknown, extra?: unknown) => - callGatewayFromCli(method, opts, params, extra as number | undefined), + mocks.callGatewayFromCli(method, opts, params, extra as number | undefined), }; }); vi.mock("../runtime.js", () => ({ - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); -const { registerCronCli } = await import("./cron-cli.js"); - type CronUpdatePatch = { patch?: { schedule?: { kind?: string; expr?: string; tz?: string; staggerMs?: number }; @@ -72,7 +90,11 @@ function buildProgram() { function resetGatewayMock() { callGatewayFromCli.mockClear(); callGatewayFromCli.mockImplementation(defaultGatewayMock); - resetRuntimeCapture(); + defaultRuntime.log.mockClear(); + defaultRuntime.error.mockClear(); + defaultRuntime.writeStdout.mockClear(); + defaultRuntime.writeJson.mockClear(); + defaultRuntime.exit.mockClear(); } async function runCronCommand(args: string[]): Promise { @@ -176,8 +198,7 @@ async function runCronRunAndCaptureExit(params: { }, ); - const runtimeModule = await import("../runtime.js"); - const runtime = runtimeModule.defaultRuntime as { exit: (code: number) => void }; + const runtime = defaultRuntime as { exit: (code: number) => void }; const originalExit = runtime.exit; const exitSpy = vi.fn(); runtime.exit = exitSpy; diff --git a/src/cli/daemon-cli.coverage.test.ts b/src/cli/daemon-cli.coverage.test.ts index f91ac285c20..b42c931d59d 100644 --- a/src/cli/daemon-cli.coverage.test.ts +++ b/src/cli/daemon-cli.coverage.test.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { captureEnv } from "../test-utils/env.js"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { registerDaemonCli } from "./daemon-cli.js"; const probeGatewayStatus = vi.fn(async (..._args: unknown[]) => ({ ok: true })); const resolveGatewayProgramArguments = vi.fn(async (_opts?: unknown) => ({ @@ -34,7 +34,28 @@ const buildGatewayInstallPlan = vi.fn( }), ); -const { runtimeLogs, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture(); +const mocks = vi.hoisted(() => { + const runtimeLogs: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime = { + log: vi.fn((...args: unknown[]) => { + runtimeLogs.push(stringifyArgs(args)); + }), + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { runtimeLogs, defaultRuntime }; +}); + +const { runtimeLogs } = mocks; vi.mock("./daemon-cli/probe.js", () => ({ probeGatewayStatus: (opts: unknown) => probeGatewayStatus(opts), @@ -85,7 +106,7 @@ vi.mock("../infra/ports.js", () => ({ vi.mock("../runtime.js", async (importOriginal) => ({ ...(await importOriginal()), - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); vi.mock("../commands/daemon-install-helpers.js", () => ({ @@ -101,7 +122,6 @@ vi.mock("./progress.js", () => ({ withProgress: async (_opts: unknown, fn: () => Promise) => await fn(), })); -const { registerDaemonCli } = await import("./daemon-cli.js"); let daemonProgram: Command; function createDaemonProgram() { @@ -145,7 +165,7 @@ describe("daemon-cli coverage", () => { }); it("probes gateway status by default", async () => { - resetRuntimeCapture(); + runtimeLogs.length = 0; probeGatewayStatus.mockClear(); await runDaemonCommand(["daemon", "status"]); @@ -159,7 +179,7 @@ describe("daemon-cli coverage", () => { }); it("derives probe URL from service args + env (json)", async () => { - resetRuntimeCapture(); + runtimeLogs.length = 0; probeGatewayStatus.mockClear(); inspectPortUsage.mockClear(); @@ -208,7 +228,7 @@ describe("daemon-cli coverage", () => { }); it("installs the daemon (json output)", async () => { - resetRuntimeCapture(); + runtimeLogs.length = 0; serviceIsLoaded.mockResolvedValueOnce(false); serviceInstall.mockClear(); @@ -234,7 +254,7 @@ describe("daemon-cli coverage", () => { }); it("starts and stops daemon (json output)", async () => { - resetRuntimeCapture(); + runtimeLogs.length = 0; serviceRestart.mockClear(); serviceStop.mockClear(); serviceIsLoaded.mockResolvedValue(true); diff --git a/src/cli/daemon-cli/probe.test.ts b/src/cli/daemon-cli/probe.test.ts index 35e6b9e8fa6..21ae436f1b4 100644 --- a/src/cli/daemon-cli/probe.test.ts +++ b/src/cli/daemon-cli/probe.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { probeGatewayStatus } from "./probe.js"; const callGatewayMock = vi.hoisted(() => vi.fn()); const probeGatewayMock = vi.hoisted(() => vi.fn()); @@ -15,8 +16,6 @@ vi.mock("../progress.js", () => ({ withProgress: async (_opts: unknown, fn: () => Promise) => await fn(), })); -const { probeGatewayStatus } = await import("./probe.js"); - describe("probeGatewayStatus", () => { it("uses lightweight token-only probing for daemon status", async () => { callGatewayMock.mockReset(); diff --git a/src/cli/daemon-cli/status.gather.test.ts b/src/cli/daemon-cli/status.gather.test.ts index 3b971413103..38dad085d4c 100644 --- a/src/cli/daemon-cli/status.gather.test.ts +++ b/src/cli/daemon-cli/status.gather.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createMockGatewayService } from "../../daemon/service.test-helpers.js"; import { captureEnv } from "../../test-utils/env.js"; import type { GatewayRestartSnapshot } from "./restart-health.js"; +import { gatherDaemonStatus } from "./status.gather.js"; const callGatewayStatusProbe = vi.fn< (opts?: unknown) => Promise<{ ok: boolean; url?: string; error?: string | null }> @@ -136,8 +137,6 @@ vi.mock("./restart-health.js", () => ({ inspectGatewayRestart: (opts: unknown) => inspectGatewayRestart(opts), })); -const { gatherDaemonStatus } = await import("./status.gather.js"); - describe("gatherDaemonStatus", () => { let envSnapshot: ReturnType; diff --git a/src/cli/daemon-cli/status.print.test.ts b/src/cli/daemon-cli/status.print.test.ts index f7c289492a2..0582d2759ae 100644 --- a/src/cli/daemon-cli/status.print.test.ts +++ b/src/cli/daemon-cli/status.print.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { formatCliCommand } from "../command-format.js"; +import { printDaemonStatus } from "./status.print.js"; const runtime = vi.hoisted(() => ({ log: vi.fn<(line: string) => void>(), @@ -69,8 +70,6 @@ vi.mock("./status.gather.js", () => ({ resolvePortListeningAddresses: () => ["127.0.0.1:18789"], })); -const { printDaemonStatus } = await import("./status.print.js"); - describe("printDaemonStatus", () => { beforeEach(() => { runtime.log.mockReset(); diff --git a/src/cli/devices-cli.test.ts b/src/cli/devices-cli.test.ts index 3dbb75b13ad..657be89ed92 100644 --- a/src/cli/devices-cli.test.ts +++ b/src/cli/devices-cli.test.ts @@ -1,37 +1,53 @@ import { Command } from "commander"; -import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { registerDevicesCli } from "./devices-cli.js"; -const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture(); -runtime.exit.mockImplementation(() => {}); -const callGateway = vi.fn(); -const buildGatewayConnectionDetails = vi.fn(() => ({ - url: "ws://127.0.0.1:18789", - urlSource: "local loopback", - message: "", +const mocks = vi.hoisted(() => ({ + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + writeJson: vi.fn(), + }, + callGateway: vi.fn(), + buildGatewayConnectionDetails: vi.fn(() => ({ + url: "ws://127.0.0.1:18789", + urlSource: "local loopback", + message: "", + })), + listDevicePairing: vi.fn(), + approveDevicePairing: vi.fn(), + summarizeDeviceTokens: vi.fn(), + withProgress: vi.fn(async (_opts: unknown, fn: () => Promise) => await fn()), })); -const listDevicePairing = vi.fn(); -const approveDevicePairing = vi.fn(); -const summarizeDeviceTokens = vi.fn(); -const withProgress = vi.fn(async (_opts: unknown, fn: () => Promise) => await fn()); -vi.mock("../gateway/call.js", () => ({ + +const { + runtime, callGateway, buildGatewayConnectionDetails, -})); - -vi.mock("./progress.js", () => ({ - withProgress, -})); - -vi.mock("../infra/device-pairing.js", () => ({ listDevicePairing, approveDevicePairing, summarizeDeviceTokens, + withProgress, +} = mocks; + +vi.mock("../gateway/call.js", () => ({ + callGateway: mocks.callGateway, + buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails, })); -vi.mock("../runtime.js", async (importOriginal) => ({ - ...(await importOriginal()), - defaultRuntime: runtime, +vi.mock("./progress.js", () => ({ + withProgress: mocks.withProgress, +})); + +vi.mock("../infra/device-pairing.js", () => ({ + listDevicePairing: mocks.listDevicePairing, + approveDevicePairing: mocks.approveDevicePairing, + summarizeDeviceTokens: mocks.summarizeDeviceTokens, +})); + +vi.mock("../runtime.js", () => ({ + defaultRuntime: mocks.runtime, writeRuntimeJson: ( targetRuntime: { log: (...args: unknown[]) => void }, value: unknown, @@ -39,12 +55,6 @@ vi.mock("../runtime.js", async (importOriginal) => ({ ) => targetRuntime.log(JSON.stringify(value, null, space > 0 ? space : undefined)), })); -let registerDevicesCli: typeof import("./devices-cli.js").registerDevicesCli; - -beforeAll(async () => { - ({ registerDevicesCli } = await import("./devices-cli.js")); -}); - async function runDevicesApprove(argv: string[]) { await runDevicesCommand(["approve", ...argv]); } @@ -321,9 +331,12 @@ describe("devices cli list", () => { }); }); +beforeEach(() => { + vi.clearAllMocks(); + runtime.exit.mockImplementation(() => {}); +}); + afterEach(() => { - resetRuntimeCapture(); - callGateway.mockClear(); buildGatewayConnectionDetails.mockClear(); buildGatewayConnectionDetails.mockReturnValue({ url: "ws://127.0.0.1:18789", diff --git a/src/cli/directory-cli.test.ts b/src/cli/directory-cli.test.ts index 563350c4791..b7b1e2bc801 100644 --- a/src/cli/directory-cli.test.ts +++ b/src/cli/directory-cli.test.ts @@ -1,16 +1,30 @@ 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 runtimeState = vi.hoisted(() => { + const runtimeLogs: string[] = []; + const runtimeErrors: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime = { + log: vi.fn((...args: unknown[]) => { + runtimeLogs.push(stringifyArgs(args)); + }), + error: vi.fn((...args: unknown[]) => { + runtimeErrors.push(stringifyArgs(args)); + }), + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + return { defaultRuntime, runtimeLogs, runtimeErrors }; +}); const mocks = vi.hoisted(() => ({ loadConfig: vi.fn(), @@ -49,16 +63,15 @@ vi.mock("../channels/plugins/helpers.js", () => ({ resolveChannelDefaultAccountId: mocks.resolveChannelDefaultAccountId, })); -vi.mock("../runtime.js", async () => { - const { createCliRuntimeCapture } = await import("./test-runtime-capture.js"); - runtimeState.capture ??= createCliRuntimeCapture(); - return { defaultRuntime: runtimeState.capture.defaultRuntime }; -}); +vi.mock("../runtime.js", () => ({ + defaultRuntime: runtimeState.defaultRuntime, +})); describe("registerDirectoryCli", () => { beforeEach(() => { vi.clearAllMocks(); - getRuntimeCapture().resetRuntimeCapture(); + runtimeState.runtimeLogs.length = 0; + runtimeState.runtimeErrors.length = 0; mocks.loadConfig.mockReturnValue({ channels: {} }); mocks.readConfigFileSnapshot.mockResolvedValue({ hash: "config-1" }); mocks.applyPluginAutoEnable.mockImplementation(({ config }) => ({ config, changes: [] })); @@ -69,7 +82,12 @@ describe("registerDirectoryCli", () => { configured: ["demo-channel"], source: "explicit", }); - getRuntimeCapture().defaultRuntime.exit.mockImplementation((code: number) => { + runtimeState.defaultRuntime.log.mockClear(); + runtimeState.defaultRuntime.error.mockClear(); + runtimeState.defaultRuntime.writeStdout.mockClear(); + runtimeState.defaultRuntime.writeJson.mockClear(); + runtimeState.defaultRuntime.exit.mockClear(); + runtimeState.defaultRuntime.exit.mockImplementation((code: number) => { throw new Error(`exit:${code}`); }); }); @@ -113,10 +131,10 @@ describe("registerDirectoryCli", () => { accountId: "default", }), ); - expect(getRuntimeCapture().defaultRuntime.log).toHaveBeenCalledWith( + expect(runtimeState.defaultRuntime.log).toHaveBeenCalledWith( JSON.stringify({ id: "self-1", name: "Family Phone" }, null, 2), ); - expect(getRuntimeCapture().defaultRuntime.error).not.toHaveBeenCalled(); + expect(runtimeState.defaultRuntime.error).not.toHaveBeenCalled(); }); it("uses the auto-enabled config snapshot for omitted channel selection", async () => { diff --git a/src/cli/exec-approvals-cli.test.ts b/src/cli/exec-approvals-cli.test.ts index c11deec8bd8..235a3cc00f0 100644 --- a/src/cli/exec-approvals-cli.test.ts +++ b/src/cli/exec-approvals-cli.test.ts @@ -1,20 +1,44 @@ import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import * as execApprovals from "../infra/exec-approvals.js"; +import { registerExecApprovalsCli } from "./exec-approvals-cli.js"; -const callGatewayFromCli = vi.fn(async (method: string, _opts: unknown, params?: unknown) => { - if (method.endsWith(".get")) { - return { - path: "/tmp/exec-approvals.json", - exists: true, - hash: "hash-1", - file: { version: 1, agents: {} }, - }; - } - return { method, params }; +const mocks = vi.hoisted(() => { + const runtimeErrors: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime = { + log: vi.fn(), + error: vi.fn((...args: unknown[]) => { + runtimeErrors.push(stringifyArgs(args)); + }), + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { + callGatewayFromCli: vi.fn(async (method: string, _opts: unknown, params?: unknown) => { + if (method.endsWith(".get")) { + return { + path: "/tmp/exec-approvals.json", + exists: true, + hash: "hash-1", + file: { version: 1, agents: {} }, + }; + } + return { method, params }; + }), + defaultRuntime, + runtimeErrors, + }; }); -const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture(); +const { callGatewayFromCli, defaultRuntime, runtimeErrors } = mocks; const localSnapshot = { path: "/tmp/local-exec-approvals.json", @@ -30,7 +54,7 @@ function resetLocalSnapshot() { vi.mock("./gateway-rpc.js", () => ({ callGatewayFromCli: (method: string, opts: unknown, params?: unknown) => - callGatewayFromCli(method, opts, params), + mocks.callGatewayFromCli(method, opts, params), })); vi.mock("./nodes-cli/rpc.js", async () => { @@ -42,7 +66,7 @@ vi.mock("./nodes-cli/rpc.js", async () => { }); vi.mock("../runtime.js", () => ({ - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); vi.mock("../infra/exec-approvals.js", async () => { @@ -56,9 +80,6 @@ vi.mock("../infra/exec-approvals.js", async () => { }; }); -const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js"); -const execApprovals = await import("../infra/exec-approvals.js"); - describe("exec approvals CLI", () => { const createProgram = () => { const program = new Command(); @@ -74,8 +95,13 @@ describe("exec approvals CLI", () => { beforeEach(() => { resetLocalSnapshot(); - resetRuntimeCapture(); + runtimeErrors.length = 0; callGatewayFromCli.mockClear(); + defaultRuntime.log.mockClear(); + defaultRuntime.error.mockClear(); + defaultRuntime.writeStdout.mockClear(); + defaultRuntime.writeJson.mockClear(); + defaultRuntime.exit.mockClear(); }); it("routes get command to local, gateway, and node modes", async () => { diff --git a/src/cli/gateway-cli.coverage.test.ts b/src/cli/gateway-cli.coverage.test.ts index 878934b5b51..df3b667f423 100644 --- a/src/cli/gateway-cli.coverage.test.ts +++ b/src/cli/gateway-cli.coverage.test.ts @@ -2,7 +2,7 @@ import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { withEnvOverride } from "../config/test-helpers.js"; import { GatewayLockError } from "../infra/gateway-lock.js"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { registerGatewayCli } from "./gateway-cli.js"; type DiscoveredBeacon = Awaited< ReturnType @@ -30,8 +30,31 @@ const gatewayStatusCommand = vi.fn<(opts: unknown) => Promise>(async () => const inspectPortUsage = vi.fn(async (_port: number) => ({ status: "free" as const })); const formatPortDiagnostics = vi.fn((_diagnostics: unknown) => [] as string[]); -const { runtimeLogs, runtimeErrors, defaultRuntime, resetRuntimeCapture } = - createCliRuntimeCapture(); +const mocks = vi.hoisted(() => { + const runtimeLogs: string[] = []; + const runtimeErrors: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime = { + log: vi.fn((...args: unknown[]) => { + runtimeLogs.push(stringifyArgs(args)); + }), + error: vi.fn((...args: unknown[]) => { + runtimeErrors.push(stringifyArgs(args)); + }), + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { runtimeLogs, runtimeErrors, defaultRuntime }; +}); + +const { runtimeLogs, runtimeErrors, defaultRuntime } = mocks; vi.mock( new URL("../../gateway/call.ts", new URL("./gateway-cli/call.ts", import.meta.url)).href, @@ -53,7 +76,7 @@ vi.mock("../globals.js", () => ({ vi.mock("../runtime.js", async (importOriginal) => ({ ...(await importOriginal()), - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); vi.mock("./ports.js", () => ({ @@ -96,7 +119,6 @@ vi.mock("../infra/ports.js", () => ({ formatPortDiagnostics: (diagnostics: unknown) => formatPortDiagnostics(diagnostics), })); -const { registerGatewayCli } = await import("./gateway-cli.js"); let gatewayProgram: Command; function createGatewayProgram() { @@ -117,12 +139,18 @@ async function expectGatewayExit(args: string[]) { describe("gateway-cli coverage", () => { beforeEach(() => { gatewayProgram = createGatewayProgram(); + runtimeLogs.length = 0; + runtimeErrors.length = 0; + defaultRuntime.log.mockClear(); + defaultRuntime.error.mockClear(); + defaultRuntime.writeStdout.mockClear(); + defaultRuntime.writeJson.mockClear(); + defaultRuntime.exit.mockClear(); inspectPortUsage.mockClear(); formatPortDiagnostics.mockClear(); }); it("registers call/health commands and routes to callGateway", async () => { - resetRuntimeCapture(); callGateway.mockClear(); await runGatewayCommand(["gateway", "call", "health", "--params", '{"x":1}', "--json"]); @@ -132,7 +160,6 @@ describe("gateway-cli coverage", () => { }); it("registers gateway probe and routes to gatewayStatusCommand", async () => { - resetRuntimeCapture(); gatewayStatusCommand.mockClear(); await runGatewayCommand(["gateway", "probe", "--json"]); @@ -141,7 +168,6 @@ describe("gateway-cli coverage", () => { }); it("registers gateway discover and prints json output", async () => { - resetRuntimeCapture(); discoverGatewayBeacons.mockClear(); discoverGatewayBeacons.mockResolvedValueOnce([ { @@ -166,7 +192,6 @@ describe("gateway-cli coverage", () => { }); it("validates gateway discover timeout", async () => { - resetRuntimeCapture(); discoverGatewayBeacons.mockClear(); await expectGatewayExit(["gateway", "discover", "--timeout", "0"]); @@ -175,7 +200,6 @@ describe("gateway-cli coverage", () => { }); it("fails gateway call on invalid params JSON", async () => { - resetRuntimeCapture(); callGateway.mockClear(); await expectGatewayExit(["gateway", "call", "status", "--params", "not-json"]); @@ -184,8 +208,6 @@ describe("gateway-cli coverage", () => { }); it("validates gateway ports and handles force/start errors", async () => { - resetRuntimeCapture(); - // Invalid port await expectGatewayExit(["gateway", "--port", "0", "--token", "test-token"]); @@ -243,7 +265,6 @@ describe("gateway-cli coverage", () => { OPENCLAW_SERVICE_KIND: undefined, }, async () => { - resetRuntimeCapture(); serviceIsLoaded.mockResolvedValue(true); startGatewayServer.mockRejectedValueOnce( new GatewayLockError("another gateway instance is already listening"), @@ -260,7 +281,8 @@ describe("gateway-cli coverage", () => { }); it("keeps exit 1 for gateway bind failures wrapped as GatewayLockError", async () => { - resetRuntimeCapture(); + runtimeLogs.length = 0; + runtimeErrors.length = 0; serviceIsLoaded.mockResolvedValue(true); startGatewayServer.mockRejectedValueOnce( new GatewayLockError("failed to bind gateway socket on ws://127.0.0.1:18789: Error: boom"), @@ -272,7 +294,8 @@ describe("gateway-cli coverage", () => { }); it("keeps exit 1 for gateway lock acquisition failures", async () => { - resetRuntimeCapture(); + runtimeLogs.length = 0; + runtimeErrors.length = 0; serviceIsLoaded.mockResolvedValue(true); startGatewayServer.mockRejectedValueOnce( new GatewayLockError("failed to acquire gateway lock at /tmp/openclaw/gateway.lock"), @@ -285,7 +308,8 @@ describe("gateway-cli coverage", () => { it("uses env/config port when --port is omitted", async () => { await withEnvOverride({ OPENCLAW_GATEWAY_PORT: "19001" }, async () => { - resetRuntimeCapture(); + runtimeLogs.length = 0; + runtimeErrors.length = 0; startGatewayServer.mockClear(); startGatewayServer.mockRejectedValueOnce(new Error("nope")); diff --git a/src/cli/gateway-cli/register.option-collisions.test.ts b/src/cli/gateway-cli/register.option-collisions.test.ts index d8ffe706b10..7786ab6d8bc 100644 --- a/src/cli/gateway-cli/register.option-collisions.test.ts +++ b/src/cli/gateway-cli/register.option-collisions.test.ts @@ -1,13 +1,22 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "../test-runtime-capture.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerGatewayCli } from "./register.js"; -const callGatewayCli = vi.fn(async (_method: string, _opts: unknown, _params?: unknown) => ({ - ok: true, +const mocks = vi.hoisted(() => ({ + callGatewayCli: vi.fn(async (_method: string, _opts: unknown, _params?: unknown) => ({ + ok: true, + })), + gatewayStatusCommand: vi.fn(async (_opts: unknown, _runtime: unknown) => {}), + defaultRuntime: { + log: vi.fn(), + error: vi.fn(), + writeStdout: vi.fn(), + writeJson: vi.fn(), + exit: vi.fn(), + }, })); -const gatewayStatusCommand = vi.fn(async (_opts: unknown, _runtime: unknown) => {}); -const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture(); +const { callGatewayCli, gatewayStatusCommand, defaultRuntime } = mocks; vi.mock("../cli-utils.js", () => ({ runCommandWithRuntime: async ( @@ -25,11 +34,12 @@ vi.mock("../cli-utils.js", () => ({ vi.mock("../../runtime.js", async (importOriginal) => ({ ...(await importOriginal()), - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); vi.mock("../../commands/gateway-status.js", () => ({ - gatewayStatusCommand: (opts: unknown, runtime: unknown) => gatewayStatusCommand(opts, runtime), + gatewayStatusCommand: (opts: unknown, runtime: unknown) => + mocks.gatewayStatusCommand(opts, runtime), })); vi.mock("./call.js", () => ({ @@ -42,7 +52,7 @@ vi.mock("./call.js", () => ({ .option("--expect-final", "Wait for final response (agent)", false) .option("--json", "Output JSON", false), callGatewayCli: (method: string, opts: unknown, params?: unknown) => - callGatewayCli(method, opts, params), + mocks.callGatewayCli(method, opts, params), })); vi.mock("./run.js", () => ({ @@ -113,20 +123,21 @@ vi.mock("./discover.js", () => ({ })); describe("gateway register option collisions", () => { - let registerGatewayCli: typeof import("./register.js").registerGatewayCli; - let sharedProgram: Command; + let sharedProgram: Command = new Command(); - beforeAll(async () => { - ({ registerGatewayCli } = await import("./register.js")); - sharedProgram = new Command(); + if (sharedProgram.commands.length === 0) { sharedProgram.exitOverride(); registerGatewayCli(sharedProgram); - }); + } beforeEach(() => { - resetRuntimeCapture(); callGatewayCli.mockClear(); gatewayStatusCommand.mockClear(); + defaultRuntime.log.mockClear(); + defaultRuntime.error.mockClear(); + defaultRuntime.writeStdout.mockClear(); + defaultRuntime.writeJson.mockClear(); + defaultRuntime.exit.mockClear(); }); it.each([ diff --git a/src/cli/logs-cli.test.ts b/src/cli/logs-cli.test.ts index 0d3a0fed8c6..6552efb4550 100644 --- a/src/cli/logs-cli.test.ts +++ b/src/cli/logs-cli.test.ts @@ -1,6 +1,6 @@ -import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { runRegisteredCli } from "../test-utils/command-runner.js"; -import { formatLogTimestamp } from "./logs-cli.js"; +import { formatLogTimestamp, registerLogsCli } from "./logs-cli.js"; const callGatewayFromCli = vi.fn(); @@ -12,12 +12,6 @@ vi.mock("./gateway-rpc.js", async () => { }; }); -let registerLogsCli: typeof import("./logs-cli.js").registerLogsCli; - -beforeAll(async () => { - ({ registerLogsCli } = await import("./logs-cli.js")); -}); - async function runLogsCli(argv: string[]) { await runRegisteredCli({ register: registerLogsCli as (program: import("commander").Command) => void, diff --git a/src/cli/mcp-cli.test.ts b/src/cli/mcp-cli.test.ts index 6531636bc37..68b402f9e48 100644 --- a/src/cli/mcp-cli.test.ts +++ b/src/cli/mcp-cli.test.ts @@ -2,21 +2,38 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Command } from "commander"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { withTempHome } from "../config/home-env.test-harness.js"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { registerMcpCli } from "./mcp-cli.js"; -const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture(); +const mocks = vi.hoisted(() => { + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + writeJson: vi.fn((value: unknown, space = 2) => { + runtime.log(JSON.stringify(value, null, space > 0 ? space : undefined)); + }), + }; + return { + runtime, + serveOpenClawChannelMcp: vi.fn(), + }; +}); + +const defaultRuntime = mocks.runtime; const mockLog = defaultRuntime.log; const mockError = defaultRuntime.error; -const serveOpenClawChannelMcp = vi.fn(); +const serveOpenClawChannelMcp = mocks.serveOpenClawChannelMcp; vi.mock("../runtime.js", () => ({ - defaultRuntime, + defaultRuntime: mocks.runtime, })); vi.mock("../mcp/channel-server.js", () => ({ - serveOpenClawChannelMcp, + serveOpenClawChannelMcp: mocks.serveOpenClawChannelMcp, })); const tempDirs: string[] = []; @@ -27,7 +44,6 @@ async function createWorkspace(): Promise { return dir; } -let registerMcpCli: typeof import("./mcp-cli.js").registerMcpCli; let sharedProgram: Command; async function runMcpCommand(args: string[]) { @@ -35,16 +51,14 @@ async function runMcpCommand(args: string[]) { } describe("mcp cli", () => { - beforeAll(async () => { - ({ registerMcpCli } = await import("./mcp-cli.js")); + if (!sharedProgram) { sharedProgram = new Command(); sharedProgram.exitOverride(); registerMcpCli(sharedProgram); - }, 300_000); + } beforeEach(() => { vi.clearAllMocks(); - resetRuntimeCapture(); }); afterEach(async () => { diff --git a/src/cli/models-cli.test.ts b/src/cli/models-cli.test.ts index 208b74fd09d..3d3b5d4bf95 100644 --- a/src/cli/models-cli.test.ts +++ b/src/cli/models-cli.test.ts @@ -1,45 +1,43 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { runRegisteredCli } from "../test-utils/command-runner.js"; +import { registerModelsCli } from "./models-cli.js"; -const modelsStatusCommand = vi.fn().mockResolvedValue(undefined); -const noopAsync = vi.fn(async () => undefined); -const modelsAuthLoginCommand = vi.fn().mockResolvedValue(undefined); +const mocks = vi.hoisted(() => ({ + modelsStatusCommand: vi.fn().mockResolvedValue(undefined), + noopAsync: vi.fn(async () => undefined), + modelsAuthLoginCommand: vi.fn().mockResolvedValue(undefined), +})); + +const { modelsStatusCommand, modelsAuthLoginCommand } = mocks; vi.mock("../commands/models.js", () => ({ - modelsStatusCommand, - modelsAliasesAddCommand: noopAsync, - modelsAliasesListCommand: noopAsync, - modelsAliasesRemoveCommand: noopAsync, - modelsAuthAddCommand: noopAsync, - modelsAuthLoginCommand, - modelsAuthOrderClearCommand: noopAsync, - modelsAuthOrderGetCommand: noopAsync, - modelsAuthOrderSetCommand: noopAsync, - modelsAuthPasteTokenCommand: noopAsync, - modelsAuthSetupTokenCommand: noopAsync, - modelsFallbacksAddCommand: noopAsync, - modelsFallbacksClearCommand: noopAsync, - modelsFallbacksListCommand: noopAsync, - modelsFallbacksRemoveCommand: noopAsync, - modelsImageFallbacksAddCommand: noopAsync, - modelsImageFallbacksClearCommand: noopAsync, - modelsImageFallbacksListCommand: noopAsync, - modelsImageFallbacksRemoveCommand: noopAsync, - modelsListCommand: noopAsync, - modelsScanCommand: noopAsync, - modelsSetCommand: noopAsync, - modelsSetImageCommand: noopAsync, + modelsStatusCommand: mocks.modelsStatusCommand, + modelsAliasesAddCommand: mocks.noopAsync, + modelsAliasesListCommand: mocks.noopAsync, + modelsAliasesRemoveCommand: mocks.noopAsync, + modelsAuthAddCommand: mocks.noopAsync, + modelsAuthLoginCommand: mocks.modelsAuthLoginCommand, + modelsAuthOrderClearCommand: mocks.noopAsync, + modelsAuthOrderGetCommand: mocks.noopAsync, + modelsAuthOrderSetCommand: mocks.noopAsync, + modelsAuthPasteTokenCommand: mocks.noopAsync, + modelsAuthSetupTokenCommand: mocks.noopAsync, + modelsFallbacksAddCommand: mocks.noopAsync, + modelsFallbacksClearCommand: mocks.noopAsync, + modelsFallbacksListCommand: mocks.noopAsync, + modelsFallbacksRemoveCommand: mocks.noopAsync, + modelsImageFallbacksAddCommand: mocks.noopAsync, + modelsImageFallbacksClearCommand: mocks.noopAsync, + modelsImageFallbacksListCommand: mocks.noopAsync, + modelsImageFallbacksRemoveCommand: mocks.noopAsync, + modelsListCommand: mocks.noopAsync, + modelsScanCommand: mocks.noopAsync, + modelsSetCommand: mocks.noopAsync, + modelsSetImageCommand: mocks.noopAsync, })); describe("models cli", () => { - let registerModelsCli: (typeof import("./models-cli.js"))["registerModelsCli"]; - - beforeAll(async () => { - // Load once; vi.mock above ensures command handlers are already mocked. - ({ registerModelsCli } = await import("./models-cli.js")); - }); - beforeEach(() => { modelsAuthLoginCommand.mockClear(); modelsStatusCommand.mockClear(); diff --git a/src/cli/nodes-camera.test.ts b/src/cli/nodes-camera.test.ts index 268f036e4a3..2af49fc1711 100644 --- a/src/cli/nodes-camera.test.ts +++ b/src/cli/nodes-camera.test.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { readFileUtf8AndCleanup, stubFetchResponse, @@ -35,9 +35,7 @@ async function withCameraTempDir(run: (dir: string) => Promise): Promise { - beforeEach(async () => { - vi.resetModules(); - vi.clearAllMocks(); + beforeAll(async () => { ({ cameraTempPath, parseCameraClipPayload, @@ -49,6 +47,10 @@ describe("nodes camera helpers", () => { ({ parseScreenRecordPayload, screenRecordTempPath } = await import("./nodes-screen.js")); }); + beforeEach(() => { + vi.clearAllMocks(); + }); + it("parses camera.snap payload", () => { expect( parseCameraSnapPayload({ diff --git a/src/cli/nodes-cli.coverage.test.ts b/src/cli/nodes-cli.coverage.test.ts index 3cd9fee440b..67751d54d00 100644 --- a/src/cli/nodes-cli.coverage.test.ts +++ b/src/cli/nodes-cli.coverage.test.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerNodesCli } from "./nodes-cli.js"; type NodeInvokeCall = { method?: string; @@ -46,7 +46,31 @@ const callGateway = vi.fn(async (opts: NodeInvokeCall) => { const randomIdempotencyKey = vi.fn(() => "rk_test"); -const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture(); +const mocks = vi.hoisted(() => { + const runtimeErrors: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime = { + log: vi.fn(), + error: vi.fn((...args: unknown[]) => { + runtimeErrors.push(stringifyArgs(args)); + }), + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { + runtimeErrors, + defaultRuntime, + }; +}); + +const { runtimeErrors, defaultRuntime } = mocks; vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGateway(opts as NodeInvokeCall), @@ -55,12 +79,11 @@ vi.mock("../gateway/call.js", () => ({ vi.mock("../runtime.js", async (importOriginal) => ({ ...(await importOriginal()), - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); describe("nodes-cli coverage", () => { - let registerNodesCli: (program: Command) => void; - let sharedProgram: Command; + let sharedProgram: Command = new Command(); const getNodeInvokeCall = () => { const last = lastNodeInvokeCall; @@ -75,17 +98,20 @@ describe("nodes-cli coverage", () => { return getNodeInvokeCall(); }; - beforeAll(async () => { - ({ registerNodesCli } = await import("./nodes-cli.js")); - sharedProgram = new Command(); + if (sharedProgram.commands.length === 0) { sharedProgram.exitOverride(); registerNodesCli(sharedProgram); - }); + } beforeEach(() => { - resetRuntimeCapture(); + runtimeErrors.length = 0; callGateway.mockClear(); randomIdempotencyKey.mockClear(); + defaultRuntime.log.mockClear(); + defaultRuntime.error.mockClear(); + defaultRuntime.writeStdout.mockClear(); + defaultRuntime.writeJson.mockClear(); + defaultRuntime.exit.mockClear(); lastNodeInvokeCall = null; }); diff --git a/src/cli/nodes-cli/register.invoke.approval-transport-timeout.test.ts b/src/cli/nodes-cli/register.invoke.approval-transport-timeout.test.ts index f83bbf7215d..850e6cd0eab 100644 --- a/src/cli/nodes-cli/register.invoke.approval-transport-timeout.test.ts +++ b/src/cli/nodes-cli/register.invoke.approval-transport-timeout.test.ts @@ -1,6 +1,7 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_EXEC_APPROVAL_TIMEOUT_MS } from "../../infra/exec-approvals.js"; import { parseTimeoutMs } from "../parse-timeout.js"; +import { callGatewayCli } from "./rpc.js"; /** * Regression test for #12098: @@ -18,9 +19,11 @@ import { parseTimeoutMs } from "../parse-timeout.js"; * least approvalTimeoutMs + 10_000. */ -const callGatewaySpy = vi.fn< - (opts: Record) => Promise<{ decision: "allow-once" }> ->(async () => ({ decision: "allow-once" })); +const { callGatewaySpy } = vi.hoisted(() => ({ + callGatewaySpy: vi.fn<(opts: Record) => Promise<{ decision: "allow-once" }>>( + async () => ({ decision: "allow-once" }), + ), +})); vi.mock("../../gateway/call.js", () => ({ callGateway: callGatewaySpy, @@ -32,13 +35,8 @@ vi.mock("../progress.js", () => ({ })); describe("exec approval transport timeout (#12098)", () => { - let callGatewayCli: typeof import("./rpc.js").callGatewayCli; const approvalTransportFloorMs = DEFAULT_EXEC_APPROVAL_TIMEOUT_MS + 10_000; - beforeAll(async () => { - ({ callGatewayCli } = await import("./rpc.js")); - }); - beforeEach(() => { callGatewaySpy.mockClear(); callGatewaySpy.mockResolvedValue({ decision: "allow-once" }); diff --git a/src/cli/pairing-cli.test.ts b/src/cli/pairing-cli.test.ts index 8e8310bfbf9..d0ac8095d59 100644 --- a/src/cli/pairing-cli.test.ts +++ b/src/cli/pairing-cli.test.ts @@ -1,43 +1,56 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerPairingCli } from "./pairing-cli.js"; + +const mocks = vi.hoisted(() => ({ + listChannelPairingRequests: vi.fn(), + approveChannelPairingCode: vi.fn(), + notifyPairingApproved: vi.fn(), + normalizeChannelId: vi.fn((raw: string) => { + if (!raw) { + return null; + } + if (raw === "imsg") { + return "imessage"; + } + if (["telegram", "discord", "imessage"].includes(raw)) { + return raw; + } + return null; + }), + getPairingAdapter: vi.fn((channel: string) => ({ + idLabel: pairingIdLabels[channel] ?? "userId", + })), + listPairingChannels: vi.fn(() => ["telegram", "discord", "imessage"]), +})); + +const { + listChannelPairingRequests, + approveChannelPairingCode, + notifyPairingApproved, + normalizeChannelId, + getPairingAdapter, + listPairingChannels, +} = mocks; -const listChannelPairingRequests = vi.fn(); -const approveChannelPairingCode = vi.fn(); -const notifyPairingApproved = vi.fn(); const pairingIdLabels: Record = { telegram: "telegramUserId", discord: "discordUserId", }; -const normalizeChannelId = vi.fn((raw: string) => { - if (!raw) { - return null; - } - if (raw === "imsg") { - return "imessage"; - } - if (["telegram", "discord", "imessage"].includes(raw)) { - return raw; - } - return null; -}); -const getPairingAdapter = vi.fn((channel: string) => ({ - idLabel: pairingIdLabels[channel] ?? "userId", -})); -const listPairingChannels = vi.fn(() => ["telegram", "discord", "imessage"]); vi.mock("../pairing/pairing-store.js", () => ({ - listChannelPairingRequests, - approveChannelPairingCode, + listChannelPairingRequests: mocks.listChannelPairingRequests, + approveChannelPairingCode: mocks.approveChannelPairingCode, })); vi.mock("../channels/plugins/pairing.js", () => ({ - listPairingChannels, - notifyPairingApproved, - getPairingAdapter, + listPairingChannels: mocks.listPairingChannels, + notifyPairingApproved: mocks.notifyPairingApproved, + getPairingAdapter: mocks.getPairingAdapter, })); vi.mock("../channels/plugins/index.js", () => ({ - normalizeChannelId, + normalizeChannelId: mocks.normalizeChannelId, })); vi.mock("../config/config.js", () => ({ @@ -45,13 +58,6 @@ vi.mock("../config/config.js", () => ({ })); describe("pairing cli", () => { - let registerPairingCli: typeof import("./pairing-cli.js").registerPairingCli; - - beforeAll(async () => { - vi.resetModules(); - ({ registerPairingCli } = await import("./pairing-cli.js")); - }); - beforeEach(() => { listChannelPairingRequests.mockClear(); listChannelPairingRequests.mockResolvedValue([]); diff --git a/src/cli/plugin-registry.test.ts b/src/cli/plugin-registry.test.ts index dae2d240ad5..8e017a6b796 100644 --- a/src/cli/plugin-registry.test.ts +++ b/src/cli/plugin-registry.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { __testing, ensurePluginRegistryLoaded } from "./plugin-registry.js"; const mocks = vi.hoisted(() => ({ applyPluginAutoEnable: vi.fn(), @@ -37,8 +38,8 @@ vi.mock("../plugins/runtime.js", () => ({ describe("ensurePluginRegistryLoaded", () => { beforeEach(() => { - vi.resetModules(); vi.clearAllMocks(); + __testing.resetPluginRegistryLoadedForTests(); mocks.getActivePluginRegistry.mockReturnValue({ plugins: [], channels: [], @@ -73,8 +74,6 @@ describe("ensurePluginRegistryLoaded", () => { diagnostics: [], }); - const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js"); - ensurePluginRegistryLoaded({ scope: "configured-channels" }); expect(mocks.applyPluginAutoEnable).toHaveBeenCalledWith({ @@ -127,8 +126,6 @@ describe("ensurePluginRegistryLoaded", () => { tools: [], }); - const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js"); - ensurePluginRegistryLoaded({ scope: "configured-channels" }); ensurePluginRegistryLoaded({ scope: "channels" }); @@ -160,8 +157,6 @@ describe("ensurePluginRegistryLoaded", () => { tools: [], }); - const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js"); - ensurePluginRegistryLoaded({ scope: "all" }); expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(1); @@ -188,8 +183,6 @@ describe("ensurePluginRegistryLoaded", () => { tools: [{ pluginId: "demo-tool" }], }); - const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js"); - ensurePluginRegistryLoaded({ scope: "configured-channels" }); expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(1); diff --git a/src/cli/plugin-registry.ts b/src/cli/plugin-registry.ts index dc44449435f..428bb57a28b 100644 --- a/src/cli/plugin-registry.ts +++ b/src/cli/plugin-registry.ts @@ -107,3 +107,9 @@ export function ensurePluginRegistryLoaded(options?: { scope?: PluginRegistrySco }); pluginRegistryLoaded = scope; } + +export const __testing = { + resetPluginRegistryLoadedForTests(): void { + pluginRegistryLoaded = "none"; + }, +}; diff --git a/src/cli/plugins-install-config.test.ts b/src/cli/plugins-install-config.test.ts index 185cb311779..614b6bc047a 100644 --- a/src/cli/plugins-install-config.test.ts +++ b/src/cli/plugins-install-config.test.ts @@ -2,10 +2,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { bundledPluginRootAt, repoInstallSpec } from "../../test/helpers/bundled-plugin-paths.js"; import type { OpenClawConfig } from "../config/config.js"; import type { ConfigFileSnapshot } from "../config/types.openclaw.js"; +import { loadConfigForInstall } from "./plugins-install-command.js"; -const loadConfigMock = vi.fn<() => OpenClawConfig>(); -const readConfigFileSnapshotMock = vi.fn<() => Promise>(); -const cleanStaleMatrixPluginConfigMock = vi.fn(); +const hoisted = vi.hoisted(() => ({ + loadConfigMock: vi.fn<() => OpenClawConfig>(), + readConfigFileSnapshotMock: vi.fn<() => Promise>(), + cleanStaleMatrixPluginConfigMock: vi.fn(), +})); + +const loadConfigMock = hoisted.loadConfigMock; +const readConfigFileSnapshotMock = hoisted.readConfigFileSnapshotMock; +const cleanStaleMatrixPluginConfigMock = hoisted.cleanStaleMatrixPluginConfigMock; vi.mock("../config/config.js", () => ({ loadConfig: () => loadConfigMock(), @@ -16,7 +23,6 @@ vi.mock("../commands/doctor/providers/matrix.js", () => ({ cleanStaleMatrixPluginConfig: (cfg: OpenClawConfig) => cleanStaleMatrixPluginConfigMock(cfg), })); -const { loadConfigForInstall } = await import("./plugins-install-command.js"); const MATRIX_REPO_INSTALL_SPEC = repoInstallSpec("matrix"); function makeSnapshot(overrides: Partial = {}): ConfigFileSnapshot { diff --git a/src/cli/program.nodes-basic.e2e.test.ts b/src/cli/program.nodes-basic.e2e.test.ts index 9c82db4efbb..c02514dcc79 100644 --- a/src/cli/program.nodes-basic.e2e.test.ts +++ b/src/cli/program.nodes-basic.e2e.test.ts @@ -1,10 +1,10 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerNodesCli } from "./nodes-cli.js"; import { createIosNodeListResponse } from "./program.nodes-test-helpers.js"; import { callGateway, installBaseProgramMocks, runtime } from "./program.test-mocks.js"; installBaseProgramMocks(); -let registerNodesCli: (program: Command) => void; function formatRuntimeLogCallArg(value: unknown): string { if (typeof value === "string") { @@ -26,12 +26,12 @@ function formatRuntimeLogCallArg(value: unknown): string { describe("cli program (nodes basics)", () => { let program: Command; - beforeAll(async () => { - ({ registerNodesCli } = await import("./nodes-cli.js")); - program = new Command(); - program.exitOverride(); - registerNodesCli(program); - }); + function createProgram() { + const next = new Command(); + next.exitOverride(); + registerNodesCli(next); + return next; + } async function runProgram(argv: string[]) { runtime.log.mockClear(); @@ -57,6 +57,7 @@ describe("cli program (nodes basics)", () => { beforeEach(() => { vi.clearAllMocks(); + program = createProgram(); }); it("runs nodes list --connected and filters to connected nodes", async () => { diff --git a/src/cli/program.smoke.test.ts b/src/cli/program.smoke.test.ts index 90b581b49f1..12b0b872833 100644 --- a/src/cli/program.smoke.test.ts +++ b/src/cli/program.smoke.test.ts @@ -1,4 +1,5 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { buildProgram } from "./program.js"; import { configureCommand, ensureConfigReady, @@ -23,8 +24,6 @@ vi.mock("./config-cli.js", () => ({ runConfigUnset: vi.fn(), })); -const { buildProgram } = await import("./program.js"); - describe("cli program (smoke)", () => { let program = createProgram(); @@ -36,11 +35,8 @@ describe("cli program (smoke)", () => { await program.parseAsync(argv, { from: "user" }); } - beforeAll(() => { - program = createProgram(); - }); - beforeEach(() => { + program = createProgram(); vi.clearAllMocks(); runTui.mockResolvedValue(undefined); ensureConfigReady.mockResolvedValue(undefined); diff --git a/src/cli/program/action-reparse.test.ts b/src/cli/program/action-reparse.test.ts index c742c781788..24360a2e0d6 100644 --- a/src/cli/program/action-reparse.test.ts +++ b/src/cli/program/action-reparse.test.ts @@ -1,8 +1,9 @@ import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { reparseProgramFromActionArgs } from "./action-reparse.js"; -const buildParseArgvMock = vi.fn(); -const resolveActionArgsMock = vi.fn(); +const buildParseArgvMock = vi.hoisted(() => vi.fn()); +const resolveActionArgsMock = vi.hoisted(() => vi.fn()); vi.mock("../argv.js", () => ({ buildParseArgv: buildParseArgvMock, @@ -12,8 +13,6 @@ vi.mock("./helpers.js", () => ({ resolveActionArgs: resolveActionArgsMock, })); -const { reparseProgramFromActionArgs } = await import("./action-reparse.js"); - describe("reparseProgramFromActionArgs", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/cli/program/build-program.test.ts b/src/cli/program/build-program.test.ts index 1589f9c93f5..08b5fd825cb 100644 --- a/src/cli/program/build-program.test.ts +++ b/src/cli/program/build-program.test.ts @@ -1,13 +1,14 @@ import process from "node:process"; import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { buildProgram } from "./build-program.js"; import type { ProgramContext } from "./context.js"; -const registerProgramCommandsMock = vi.fn(); -const createProgramContextMock = vi.fn(); -const configureProgramHelpMock = vi.fn(); -const registerPreActionHooksMock = vi.fn(); -const setProgramContextMock = vi.fn(); +const registerProgramCommandsMock = vi.hoisted(() => vi.fn()); +const createProgramContextMock = vi.hoisted(() => vi.fn()); +const configureProgramHelpMock = vi.hoisted(() => vi.fn()); +const registerPreActionHooksMock = vi.hoisted(() => vi.fn()); +const setProgramContextMock = vi.hoisted(() => vi.fn()); vi.mock("./command-registry.js", () => ({ registerProgramCommands: registerProgramCommandsMock, @@ -29,8 +30,6 @@ vi.mock("./program-context.js", () => ({ setProgramContext: setProgramContextMock, })); -const { buildProgram } = await import("./build-program.js"); - describe("buildProgram", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/cli/program/build-program.version-alias.test.ts b/src/cli/program/build-program.version-alias.test.ts index 1d2ef41b8d4..98ff282beb5 100644 --- a/src/cli/program/build-program.version-alias.test.ts +++ b/src/cli/program/build-program.version-alias.test.ts @@ -1,7 +1,6 @@ import process from "node:process"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const { buildProgram } = await import("./build-program.js"); +import { buildProgram } from "./build-program.js"; describe("buildProgram version alias handling", () => { let originalArgv: string[]; diff --git a/src/cli/program/command-registry.test.ts b/src/cli/program/command-registry.test.ts index 7736f67fcfe..0601ff6ba8e 100644 --- a/src/cli/program/command-registry.test.ts +++ b/src/cli/program/command-registry.test.ts @@ -27,13 +27,6 @@ vi.mock("./register.maintenance.js", () => ({ }, })); -const { - getCoreCliCommandNames, - getCoreCliCommandsWithSubcommands, - registerCoreCliByName, - registerCoreCliCommands, -} = await import("./command-registry.js"); - vi.mock("./register.status-health-sessions.js", () => ({ registerStatusHealthSessionsCommands: (program: Command) => { program.command("status"); @@ -44,6 +37,13 @@ vi.mock("./register.status-health-sessions.js", () => ({ }, })); +import { + getCoreCliCommandNames, + getCoreCliCommandsWithSubcommands, + registerCoreCliByName, + registerCoreCliCommands, +} from "./command-registry.js"; + const testProgramContext: ProgramContext = { programVersion: "0.0.0-test", channelOptions: [], diff --git a/src/cli/program/config-guard.test.ts b/src/cli/program/config-guard.test.ts index b0f092b44ae..b017c51426a 100644 --- a/src/cli/program/config-guard.test.ts +++ b/src/cli/program/config-guard.test.ts @@ -1,5 +1,5 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { RuntimeEnv } from "../../runtime.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ensureConfigReady, __test__ } from "./config-guard.js"; const loadAndMaybeMigrateDoctorConfigMock = vi.hoisted(() => vi.fn()); const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn()); @@ -12,8 +12,6 @@ vi.mock("../../config/config.js", () => ({ readConfigFileSnapshot: readConfigFileSnapshotMock, })); -const mockedModuleIds = ["../../commands/doctor-config-preflight.js", "../../config/config.js"]; - function makeSnapshot() { return { exists: false, @@ -46,13 +44,7 @@ async function withCapturedStdout(run: () => Promise): Promise { } describe("ensureConfigReady", () => { - let ensureConfigReady: (params: { - runtime: RuntimeEnv; - commandPath?: string[]; - suppressDoctorStdout?: boolean; - allowInvalid?: boolean; - }) => Promise; - let resetConfigGuardStateForTests: () => void; + const resetConfigGuardStateForTests = __test__.resetConfigGuardStateForTests; async function runEnsureConfigReady(commandPath: string[], suppressDoctorStdout = false) { const runtime = makeRuntime(); @@ -75,20 +67,6 @@ describe("ensureConfigReady", () => { }); } - beforeAll(async () => { - ({ - ensureConfigReady, - __test__: { resetConfigGuardStateForTests }, - } = await import("./config-guard.js")); - }); - - afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); - }); - beforeEach(() => { vi.clearAllMocks(); resetConfigGuardStateForTests(); diff --git a/src/cli/program/context.test.ts b/src/cli/program/context.test.ts index 1fd90f844f9..2e839b08246 100644 --- a/src/cli/program/context.test.ts +++ b/src/cli/program/context.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; +import { createProgramContext } from "./context.js"; -const resolveCliChannelOptionsMock = vi.fn(() => ["telegram", "whatsapp"]); +const resolveCliChannelOptionsMock = vi.hoisted(() => vi.fn(() => ["telegram", "whatsapp"])); vi.mock("../../version.js", () => ({ VERSION: "9.9.9-test", @@ -10,8 +11,6 @@ vi.mock("../channel-options.js", () => ({ resolveCliChannelOptions: resolveCliChannelOptionsMock, })); -const { createProgramContext } = await import("./context.js"); - describe("createProgramContext", () => { it("builds program context from version and resolved channel options", () => { resolveCliChannelOptionsMock.mockClear().mockReturnValue(["telegram", "whatsapp"]); diff --git a/src/cli/program/help.test.ts b/src/cli/program/help.test.ts index 07b6a8d8f90..d0c1a55a7a5 100644 --- a/src/cli/program/help.test.ts +++ b/src/cli/program/help.test.ts @@ -1,11 +1,14 @@ import { Command } from "commander"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ProgramContext } from "./context.js"; +import { configureProgramHelp } from "./help.js"; -const hasEmittedCliBannerMock = vi.fn(() => false); -const formatCliBannerLineMock = vi.fn(() => "BANNER-LINE"); -const formatDocsLinkMock = vi.fn((_path: string, full: string) => `https://${full}`); -const resolveCommitHashMock = vi.fn<() => string | null>(() => "abc1234"); +const hasEmittedCliBannerMock = vi.hoisted(() => vi.fn(() => false)); +const formatCliBannerLineMock = vi.hoisted(() => vi.fn(() => "BANNER-LINE")); +const formatDocsLinkMock = vi.hoisted(() => + vi.fn((_path: string, full: string) => `https://${full}`), +); +const resolveCommitHashMock = vi.hoisted(() => vi.fn<() => string | null>(() => "abc1234")); vi.mock("../../terminal/links.js", () => ({ formatDocsLink: formatDocsLinkMock, @@ -44,8 +47,6 @@ vi.mock("./register.subclis.js", () => ({ getSubCliCommandsWithSubcommands: () => ["gateway"], })); -const { configureProgramHelp } = await import("./help.js"); - const testProgramContext: ProgramContext = { programVersion: "9.9.9-test", channelOptions: ["telegram"], diff --git a/src/cli/program/preaction.test.ts b/src/cli/program/preaction.test.ts index bf218d8a2e7..1a26ae8693a 100644 --- a/src/cli/program/preaction.test.ts +++ b/src/cli/program/preaction.test.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { repoInstallSpec } from "../../../test/helpers/bundled-plugin-paths.js"; import { loggingState } from "../../logging/state.js"; import { setCommandJsonMode } from "./json-mode.js"; @@ -48,15 +48,6 @@ vi.mock("../plugin-registry.js", () => ({ ensurePluginRegistryLoaded: ensurePluginRegistryLoadedMock, })); -const mockedModuleIds = [ - "../../globals.js", - "../../runtime.js", - "../banner.js", - "../cli-name.js", - "./config-guard.js", - "../plugin-registry.js", -]; - let registerPreActionHooks: typeof import("./preaction.js").registerPreActionHooks; let originalProcessArgv: string[]; let originalProcessTitle: string; @@ -70,13 +61,6 @@ beforeAll(async () => { ({ registerPreActionHooks } = await import("./preaction.js")); }); -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - beforeEach(() => { vi.clearAllMocks(); originalProcessArgv = [...process.argv]; diff --git a/src/cli/program/register.agent.test.ts b/src/cli/program/register.agent.test.ts index 0d0593f3e88..f6bc70373d9 100644 --- a/src/cli/program/register.agent.test.ts +++ b/src/cli/program/register.agent.test.ts @@ -1,67 +1,63 @@ import { Command } from "commander"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "../test-runtime-capture.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerAgentCommands } from "./register.agent.js"; -const agentCliCommandMock = vi.fn(); -const agentsAddCommandMock = vi.fn(); -const agentsBindingsCommandMock = vi.fn(); -const agentsBindCommandMock = vi.fn(); -const agentsDeleteCommandMock = vi.fn(); -const agentsListCommandMock = vi.fn(); -const agentsSetIdentityCommandMock = vi.fn(); -const agentsUnbindCommandMock = vi.fn(); -const setVerboseMock = vi.fn(); -const createDefaultDepsMock = vi.fn(() => ({ deps: true })); +const mocks = vi.hoisted(() => ({ + agentCliCommandMock: vi.fn(), + agentsAddCommandMock: vi.fn(), + agentsBindingsCommandMock: vi.fn(), + agentsBindCommandMock: vi.fn(), + agentsDeleteCommandMock: vi.fn(), + agentsListCommandMock: vi.fn(), + agentsSetIdentityCommandMock: vi.fn(), + agentsUnbindCommandMock: vi.fn(), + setVerboseMock: vi.fn(), + createDefaultDepsMock: vi.fn(() => ({ deps: true })), + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }, +})); -const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture(); +const agentCliCommandMock = mocks.agentCliCommandMock; +const agentsAddCommandMock = mocks.agentsAddCommandMock; +const agentsBindingsCommandMock = mocks.agentsBindingsCommandMock; +const agentsBindCommandMock = mocks.agentsBindCommandMock; +const agentsDeleteCommandMock = mocks.agentsDeleteCommandMock; +const agentsListCommandMock = mocks.agentsListCommandMock; +const agentsSetIdentityCommandMock = mocks.agentsSetIdentityCommandMock; +const agentsUnbindCommandMock = mocks.agentsUnbindCommandMock; +const setVerboseMock = mocks.setVerboseMock; +const createDefaultDepsMock = mocks.createDefaultDepsMock; +const runtime = mocks.runtime; vi.mock("../../commands/agent-via-gateway.js", () => ({ - agentCliCommand: agentCliCommandMock, + agentCliCommand: mocks.agentCliCommandMock, })); vi.mock("../../commands/agents.js", () => ({ - agentsAddCommand: agentsAddCommandMock, - agentsBindingsCommand: agentsBindingsCommandMock, - agentsBindCommand: agentsBindCommandMock, - agentsDeleteCommand: agentsDeleteCommandMock, - agentsListCommand: agentsListCommandMock, - agentsSetIdentityCommand: agentsSetIdentityCommandMock, - agentsUnbindCommand: agentsUnbindCommandMock, + agentsAddCommand: mocks.agentsAddCommandMock, + agentsBindingsCommand: mocks.agentsBindingsCommandMock, + agentsBindCommand: mocks.agentsBindCommandMock, + agentsDeleteCommand: mocks.agentsDeleteCommandMock, + agentsListCommand: mocks.agentsListCommandMock, + agentsSetIdentityCommand: mocks.agentsSetIdentityCommandMock, + agentsUnbindCommand: mocks.agentsUnbindCommandMock, })); vi.mock("../../globals.js", () => ({ - setVerbose: setVerboseMock, + setVerbose: mocks.setVerboseMock, })); vi.mock("../deps.js", () => ({ - createDefaultDeps: createDefaultDepsMock, + createDefaultDeps: mocks.createDefaultDepsMock, })); vi.mock("../../runtime.js", () => ({ - defaultRuntime: runtime, + defaultRuntime: mocks.runtime, })); -const mockedModuleIds = [ - "../../commands/agent-via-gateway.js", - "../../commands/agents.js", - "../../globals.js", - "../deps.js", - "../../runtime.js", -]; - -let registerAgentCommands: typeof import("./register.agent.js").registerAgentCommands; - -beforeAll(async () => { - ({ registerAgentCommands } = await import("./register.agent.js")); -}); - -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - describe("registerAgentCommands", () => { async function runCli(args: string[]) { const program = new Command(); @@ -71,7 +67,6 @@ describe("registerAgentCommands", () => { beforeEach(() => { vi.clearAllMocks(); - resetRuntimeCapture(); runtime.exit.mockImplementation(() => {}); agentCliCommandMock.mockResolvedValue(undefined); agentsAddCommandMock.mockResolvedValue(undefined); diff --git a/src/cli/program/register.backup.test.ts b/src/cli/program/register.backup.test.ts index 1f80074a0af..3c305581fef 100644 --- a/src/cli/program/register.backup.test.ts +++ b/src/cli/program/register.backup.test.ts @@ -1,43 +1,33 @@ import { Command } from "commander"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "../test-runtime-capture.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerBackupCommand } from "./register.backup.js"; -const backupCreateCommand = vi.fn(); -const backupVerifyCommand = vi.fn(); +const mocks = vi.hoisted(() => ({ + backupCreateCommand: vi.fn(), + backupVerifyCommand: vi.fn(), + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }, +})); -const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture(); +const backupCreateCommand = mocks.backupCreateCommand; +const backupVerifyCommand = mocks.backupVerifyCommand; +const runtime = mocks.runtime; vi.mock("../../commands/backup.js", () => ({ - backupCreateCommand, + backupCreateCommand: mocks.backupCreateCommand, })); vi.mock("../../commands/backup-verify.js", () => ({ - backupVerifyCommand, + backupVerifyCommand: mocks.backupVerifyCommand, })); vi.mock("../../runtime.js", () => ({ - defaultRuntime: runtime, + defaultRuntime: mocks.runtime, })); -const mockedModuleIds = [ - "../../commands/backup.js", - "../../commands/backup-verify.js", - "../../runtime.js", -]; - -let registerBackupCommand: typeof import("./register.backup.js").registerBackupCommand; - -beforeAll(async () => { - ({ registerBackupCommand } = await import("./register.backup.js")); -}); - -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - describe("registerBackupCommand", () => { async function runCli(args: string[]) { const program = new Command(); @@ -47,7 +37,6 @@ describe("registerBackupCommand", () => { beforeEach(() => { vi.clearAllMocks(); - resetRuntimeCapture(); backupCreateCommand.mockResolvedValue(undefined); backupVerifyCommand.mockResolvedValue(undefined); }); diff --git a/src/cli/program/register.configure.test.ts b/src/cli/program/register.configure.test.ts index 7ec29d862de..e1a9e34728b 100644 --- a/src/cli/program/register.configure.test.ts +++ b/src/cli/program/register.configure.test.ts @@ -1,37 +1,27 @@ import { Command } from "commander"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerConfigureCommand } from "./register.configure.js"; -const configureCommandFromSectionsArgMock = vi.fn(); -const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), -}; +const mocks = vi.hoisted(() => ({ + configureCommandFromSectionsArgMock: vi.fn(), + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }, +})); + +const { configureCommandFromSectionsArgMock, runtime } = mocks; vi.mock("../../commands/configure.js", () => ({ CONFIGURE_WIZARD_SECTIONS: ["auth", "channels", "gateway", "agent"], - configureCommandFromSectionsArg: configureCommandFromSectionsArgMock, + configureCommandFromSectionsArg: mocks.configureCommandFromSectionsArgMock, })); vi.mock("../../runtime.js", () => ({ - defaultRuntime: runtime, + defaultRuntime: mocks.runtime, })); -const mockedModuleIds = ["../../commands/configure.js", "../../runtime.js"]; - -let registerConfigureCommand: typeof import("./register.configure.js").registerConfigureCommand; - -beforeAll(async () => { - ({ registerConfigureCommand } = await import("./register.configure.js")); -}); - -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - describe("registerConfigureCommand", () => { async function runCli(args: string[]) { const program = new Command(); diff --git a/src/cli/program/register.maintenance.test.ts b/src/cli/program/register.maintenance.test.ts index 9ca77dd8d00..4a6392b6cc7 100644 --- a/src/cli/program/register.maintenance.test.ts +++ b/src/cli/program/register.maintenance.test.ts @@ -1,58 +1,41 @@ import { Command } from "commander"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerMaintenanceCommands } from "./register.maintenance.js"; -const doctorCommand = vi.fn(); -const dashboardCommand = vi.fn(); -const resetCommand = vi.fn(); -const uninstallCommand = vi.fn(); +const mocks = vi.hoisted(() => ({ + doctorCommand: vi.fn(), + dashboardCommand: vi.fn(), + resetCommand: vi.fn(), + uninstallCommand: vi.fn(), + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }, +})); -const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), -}; +const { doctorCommand, dashboardCommand, resetCommand, uninstallCommand, runtime } = mocks; vi.mock("../../commands/doctor.js", () => ({ - doctorCommand, + doctorCommand: mocks.doctorCommand, })); vi.mock("../../commands/dashboard.js", () => ({ - dashboardCommand, + dashboardCommand: mocks.dashboardCommand, })); vi.mock("../../commands/reset.js", () => ({ - resetCommand, + resetCommand: mocks.resetCommand, })); vi.mock("../../commands/uninstall.js", () => ({ - uninstallCommand, + uninstallCommand: mocks.uninstallCommand, })); vi.mock("../../runtime.js", () => ({ - defaultRuntime: runtime, + defaultRuntime: mocks.runtime, })); -const mockedModuleIds = [ - "../../commands/doctor.js", - "../../commands/dashboard.js", - "../../commands/reset.js", - "../../commands/uninstall.js", - "../../runtime.js", -]; - -let registerMaintenanceCommands: typeof import("./register.maintenance.js").registerMaintenanceCommands; - -beforeAll(async () => { - ({ registerMaintenanceCommands } = await import("./register.maintenance.js")); -}); - -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - describe("registerMaintenanceCommands doctor action", () => { async function runMaintenanceCli(args: string[]) { const program = new Command(); diff --git a/src/cli/program/register.message.test.ts b/src/cli/program/register.message.test.ts index a39078ec291..7612b725a51 100644 --- a/src/cli/program/register.message.test.ts +++ b/src/cli/program/register.message.test.ts @@ -1,94 +1,84 @@ import { Command } from "commander"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ProgramContext } from "./context.js"; +import { registerMessageCommands } from "./register.message.js"; -const createMessageCliHelpersMock = vi.fn(() => ({ helper: true })); -const registerMessageSendCommandMock = vi.fn(); -const registerMessageBroadcastCommandMock = vi.fn(); -const registerMessagePollCommandMock = vi.fn(); -const registerMessageReactionsCommandsMock = vi.fn(); -const registerMessageReadEditDeleteCommandsMock = vi.fn(); -const registerMessagePinCommandsMock = vi.fn(); -const registerMessagePermissionsCommandMock = vi.fn(); -const registerMessageSearchCommandMock = vi.fn(); -const registerMessageThreadCommandsMock = vi.fn(); -const registerMessageEmojiCommandsMock = vi.fn(); -const registerMessageStickerCommandsMock = vi.fn(); -const registerMessageDiscordAdminCommandsMock = vi.fn(); +const mocks = vi.hoisted(() => ({ + createMessageCliHelpersMock: vi.fn(() => ({ helper: true })), + registerMessageSendCommandMock: vi.fn(), + registerMessageBroadcastCommandMock: vi.fn(), + registerMessagePollCommandMock: vi.fn(), + registerMessageReactionsCommandsMock: vi.fn(), + registerMessageReadEditDeleteCommandsMock: vi.fn(), + registerMessagePinCommandsMock: vi.fn(), + registerMessagePermissionsCommandMock: vi.fn(), + registerMessageSearchCommandMock: vi.fn(), + registerMessageThreadCommandsMock: vi.fn(), + registerMessageEmojiCommandsMock: vi.fn(), + registerMessageStickerCommandsMock: vi.fn(), + registerMessageDiscordAdminCommandsMock: vi.fn(), +})); + +const createMessageCliHelpersMock = mocks.createMessageCliHelpersMock; +const registerMessageSendCommandMock = mocks.registerMessageSendCommandMock; +const registerMessageBroadcastCommandMock = mocks.registerMessageBroadcastCommandMock; +const registerMessagePollCommandMock = mocks.registerMessagePollCommandMock; +const registerMessageReactionsCommandsMock = mocks.registerMessageReactionsCommandsMock; +const registerMessageReadEditDeleteCommandsMock = mocks.registerMessageReadEditDeleteCommandsMock; +const registerMessagePinCommandsMock = mocks.registerMessagePinCommandsMock; +const registerMessagePermissionsCommandMock = mocks.registerMessagePermissionsCommandMock; +const registerMessageSearchCommandMock = mocks.registerMessageSearchCommandMock; +const registerMessageThreadCommandsMock = mocks.registerMessageThreadCommandsMock; +const registerMessageEmojiCommandsMock = mocks.registerMessageEmojiCommandsMock; +const registerMessageStickerCommandsMock = mocks.registerMessageStickerCommandsMock; +const registerMessageDiscordAdminCommandsMock = mocks.registerMessageDiscordAdminCommandsMock; vi.mock("./message/helpers.js", () => ({ - createMessageCliHelpers: createMessageCliHelpersMock, + createMessageCliHelpers: mocks.createMessageCliHelpersMock, })); vi.mock("./message/register.send.js", () => ({ - registerMessageSendCommand: registerMessageSendCommandMock, + registerMessageSendCommand: mocks.registerMessageSendCommandMock, })); vi.mock("./message/register.broadcast.js", () => ({ - registerMessageBroadcastCommand: registerMessageBroadcastCommandMock, + registerMessageBroadcastCommand: mocks.registerMessageBroadcastCommandMock, })); vi.mock("./message/register.poll.js", () => ({ - registerMessagePollCommand: registerMessagePollCommandMock, + registerMessagePollCommand: mocks.registerMessagePollCommandMock, })); vi.mock("./message/register.reactions.js", () => ({ - registerMessageReactionsCommands: registerMessageReactionsCommandsMock, + registerMessageReactionsCommands: mocks.registerMessageReactionsCommandsMock, })); vi.mock("./message/register.read-edit-delete.js", () => ({ - registerMessageReadEditDeleteCommands: registerMessageReadEditDeleteCommandsMock, + registerMessageReadEditDeleteCommands: mocks.registerMessageReadEditDeleteCommandsMock, })); vi.mock("./message/register.pins.js", () => ({ - registerMessagePinCommands: registerMessagePinCommandsMock, + registerMessagePinCommands: mocks.registerMessagePinCommandsMock, })); vi.mock("./message/register.permissions-search.js", () => ({ - registerMessagePermissionsCommand: registerMessagePermissionsCommandMock, - registerMessageSearchCommand: registerMessageSearchCommandMock, + registerMessagePermissionsCommand: mocks.registerMessagePermissionsCommandMock, + registerMessageSearchCommand: mocks.registerMessageSearchCommandMock, })); vi.mock("./message/register.thread.js", () => ({ - registerMessageThreadCommands: registerMessageThreadCommandsMock, + registerMessageThreadCommands: mocks.registerMessageThreadCommandsMock, })); vi.mock("./message/register.emoji-sticker.js", () => ({ - registerMessageEmojiCommands: registerMessageEmojiCommandsMock, - registerMessageStickerCommands: registerMessageStickerCommandsMock, + registerMessageEmojiCommands: mocks.registerMessageEmojiCommandsMock, + registerMessageStickerCommands: mocks.registerMessageStickerCommandsMock, })); vi.mock("./message/register.discord-admin.js", () => ({ - registerMessageDiscordAdminCommands: registerMessageDiscordAdminCommandsMock, + registerMessageDiscordAdminCommands: mocks.registerMessageDiscordAdminCommandsMock, })); -const mockedModuleIds = [ - "./message/helpers.js", - "./message/register.send.js", - "./message/register.broadcast.js", - "./message/register.poll.js", - "./message/register.reactions.js", - "./message/register.read-edit-delete.js", - "./message/register.pins.js", - "./message/register.permissions-search.js", - "./message/register.thread.js", - "./message/register.emoji-sticker.js", - "./message/register.discord-admin.js", -]; - -let registerMessageCommands: typeof import("./register.message.js").registerMessageCommands; - -beforeAll(async () => { - ({ registerMessageCommands } = await import("./register.message.js")); -}); - -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - describe("registerMessageCommands", () => { const ctx: ProgramContext = { programVersion: "9.9.9-test", diff --git a/src/cli/program/register.onboard.test.ts b/src/cli/program/register.onboard.test.ts index d6876797d39..31183470334 100644 --- a/src/cli/program/register.onboard.test.ts +++ b/src/cli/program/register.onboard.test.ts @@ -1,13 +1,18 @@ import { Command } from "commander"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerOnboardCommand } from "./register.onboard.js"; -const setupWizardCommandMock = vi.fn(); +const mocks = vi.hoisted(() => ({ + setupWizardCommandMock: vi.fn(), + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }, +})); -const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), -}; +const setupWizardCommandMock = mocks.setupWizardCommandMock; +const runtime = mocks.runtime; vi.mock("../../commands/auth-choice-options.static.js", () => ({ formatStaticAuthChoiceChoicesForCli: () => "token|oauth", @@ -38,35 +43,13 @@ vi.mock("../../plugins/provider-auth-choices.js", () => ({ })); vi.mock("../../commands/onboard.js", () => ({ - setupWizardCommand: setupWizardCommandMock, + setupWizardCommand: mocks.setupWizardCommandMock, })); vi.mock("../../runtime.js", () => ({ - defaultRuntime: runtime, + defaultRuntime: mocks.runtime, })); -const mockedModuleIds = [ - "../../commands/auth-choice-options.static.js", - "../../commands/auth-choice-options.js", - "../../commands/onboard-core-auth-flags.js", - "../../plugins/provider-auth-choices.js", - "../../commands/onboard.js", - "../../runtime.js", -]; - -let registerOnboardCommand: typeof import("./register.onboard.js").registerOnboardCommand; - -beforeAll(async () => { - ({ registerOnboardCommand } = await import("./register.onboard.js")); -}); - -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - describe("registerOnboardCommand", () => { async function runCli(args: string[]) { const program = new Command(); diff --git a/src/cli/program/register.setup.test.ts b/src/cli/program/register.setup.test.ts index 197ba4e85ed..c8b7ceacacf 100644 --- a/src/cli/program/register.setup.test.ts +++ b/src/cli/program/register.setup.test.ts @@ -1,45 +1,33 @@ import { Command } from "commander"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerSetupCommand } from "./register.setup.js"; -const setupCommandMock = vi.fn(); -const setupWizardCommandMock = vi.fn(); -const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), -}; +const mocks = vi.hoisted(() => ({ + setupCommandMock: vi.fn(), + setupWizardCommandMock: vi.fn(), + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }, +})); + +const setupCommandMock = mocks.setupCommandMock; +const setupWizardCommandMock = mocks.setupWizardCommandMock; +const runtime = mocks.runtime; vi.mock("../../commands/setup.js", () => ({ - setupCommand: setupCommandMock, + setupCommand: mocks.setupCommandMock, })); vi.mock("../../commands/onboard.js", () => ({ - setupWizardCommand: setupWizardCommandMock, + setupWizardCommand: mocks.setupWizardCommandMock, })); vi.mock("../../runtime.js", () => ({ - defaultRuntime: runtime, + defaultRuntime: mocks.runtime, })); -const mockedModuleIds = [ - "../../commands/setup.js", - "../../commands/onboard.js", - "../../runtime.js", -]; - -let registerSetupCommand: typeof import("./register.setup.js").registerSetupCommand; - -beforeAll(async () => { - ({ registerSetupCommand } = await import("./register.setup.js")); -}); - -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - describe("registerSetupCommand", () => { async function runCli(args: string[]) { const program = new Command(); diff --git a/src/cli/program/register.status-health-sessions.test.ts b/src/cli/program/register.status-health-sessions.test.ts index 1b7cf57c854..f5cc4495ab4 100644 --- a/src/cli/program/register.status-health-sessions.test.ts +++ b/src/cli/program/register.status-health-sessions.test.ts @@ -1,60 +1,72 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "../test-runtime-capture.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerStatusHealthSessionsCommands } from "./register.status-health-sessions.js"; -const statusCommand = vi.fn(); -const healthCommand = vi.fn(); -const sessionsCommand = vi.fn(); -const sessionsCleanupCommand = vi.fn(); -const tasksListCommand = vi.fn(); -const tasksAuditCommand = vi.fn(); -const tasksMaintenanceCommand = vi.fn(); -const tasksShowCommand = vi.fn(); -const tasksNotifyCommand = vi.fn(); -const tasksCancelCommand = vi.fn(); -const setVerbose = vi.fn(); +const mocks = vi.hoisted(() => ({ + statusCommand: vi.fn(), + healthCommand: vi.fn(), + sessionsCommand: vi.fn(), + sessionsCleanupCommand: vi.fn(), + tasksListCommand: vi.fn(), + tasksAuditCommand: vi.fn(), + tasksMaintenanceCommand: vi.fn(), + tasksShowCommand: vi.fn(), + tasksNotifyCommand: vi.fn(), + tasksCancelCommand: vi.fn(), + setVerbose: vi.fn(), + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }, +})); -const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture(); +const statusCommand = mocks.statusCommand; +const healthCommand = mocks.healthCommand; +const sessionsCommand = mocks.sessionsCommand; +const sessionsCleanupCommand = mocks.sessionsCleanupCommand; +const tasksListCommand = mocks.tasksListCommand; +const tasksAuditCommand = mocks.tasksAuditCommand; +const tasksMaintenanceCommand = mocks.tasksMaintenanceCommand; +const tasksShowCommand = mocks.tasksShowCommand; +const tasksNotifyCommand = mocks.tasksNotifyCommand; +const tasksCancelCommand = mocks.tasksCancelCommand; +const setVerbose = mocks.setVerbose; +const runtime = mocks.runtime; vi.mock("../../commands/status.js", () => ({ - statusCommand, + statusCommand: mocks.statusCommand, })); vi.mock("../../commands/health.js", () => ({ - healthCommand, + healthCommand: mocks.healthCommand, })); vi.mock("../../commands/sessions.js", () => ({ - sessionsCommand, + sessionsCommand: mocks.sessionsCommand, })); vi.mock("../../commands/sessions-cleanup.js", () => ({ - sessionsCleanupCommand, + sessionsCleanupCommand: mocks.sessionsCleanupCommand, })); vi.mock("../../commands/tasks.js", () => ({ - tasksListCommand, - tasksAuditCommand, - tasksMaintenanceCommand, - tasksShowCommand, - tasksNotifyCommand, - tasksCancelCommand, + tasksListCommand: mocks.tasksListCommand, + tasksAuditCommand: mocks.tasksAuditCommand, + tasksMaintenanceCommand: mocks.tasksMaintenanceCommand, + tasksShowCommand: mocks.tasksShowCommand, + tasksNotifyCommand: mocks.tasksNotifyCommand, + tasksCancelCommand: mocks.tasksCancelCommand, })); vi.mock("../../globals.js", () => ({ - setVerbose, + setVerbose: mocks.setVerbose, })); vi.mock("../../runtime.js", () => ({ - defaultRuntime: runtime, + defaultRuntime: mocks.runtime, })); -let registerStatusHealthSessionsCommands: typeof import("./register.status-health-sessions.js").registerStatusHealthSessionsCommands; - -beforeAll(async () => { - ({ registerStatusHealthSessionsCommands } = await import("./register.status-health-sessions.js")); -}); - describe("registerStatusHealthSessionsCommands", () => { async function runCli(args: string[]) { const program = new Command(); @@ -64,7 +76,6 @@ describe("registerStatusHealthSessionsCommands", () => { beforeEach(() => { vi.clearAllMocks(); - resetRuntimeCapture(); runtime.exit.mockImplementation(() => {}); statusCommand.mockResolvedValue(undefined); healthCommand.mockResolvedValue(undefined); diff --git a/src/cli/program/register.subclis.test.ts b/src/cli/program/register.subclis.test.ts index f2d01f7b22f..c85f48c6d73 100644 --- a/src/cli/program/register.subclis.test.ts +++ b/src/cli/program/register.subclis.test.ts @@ -1,5 +1,10 @@ import { Command } from "commander"; -import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + loadValidatedConfigForPluginRegistration, + registerSubCliByName, + registerSubCliCommands, +} from "./register.subclis.js"; const { acpAction, registerAcpCli } = vi.hoisted(() => { const action = vi.fn(); @@ -27,18 +32,6 @@ vi.mock("../acp-cli.js", () => ({ registerAcpCli })); vi.mock("../nodes-cli.js", () => ({ registerNodesCli })); vi.mock("../../config/config.js", () => configModule); -const mockedModuleIds = ["../acp-cli.js", "../nodes-cli.js", "../../config/config.js"]; - -const { loadValidatedConfigForPluginRegistration, registerSubCliByName, registerSubCliCommands } = - await import("./register.subclis.js"); - -afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); -}); - describe("registerSubCliCommands", () => { const originalArgv = process.argv; const originalDisableLazySubcommands = process.env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS; diff --git a/src/cli/program/root-help.test.ts b/src/cli/program/root-help.test.ts index 237d4d7d25b..a2dabfdde8c 100644 --- a/src/cli/program/root-help.test.ts +++ b/src/cli/program/root-help.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { renderRootHelpText } from "./root-help.js"; vi.mock("./core-command-descriptors.js", () => ({ getCoreCliCommandDescriptors: () => [ @@ -32,8 +33,6 @@ vi.mock("../../plugins/cli.js", () => ({ ], })); -const { renderRootHelpText } = await import("./root-help.js"); - describe("root help", () => { it("includes plugin CLI descriptors alongside core and sub-CLI commands", async () => { const text = await renderRootHelpText(); diff --git a/src/cli/prompt.test.ts b/src/cli/prompt.test.ts index ee68e646700..b28eb83d7b3 100644 --- a/src/cli/prompt.test.ts +++ b/src/cli/prompt.test.ts @@ -1,34 +1,26 @@ +import readline from "node:readline/promises"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { isYes, setVerbose, setYes } from "../globals.js"; +import { promptYesNo } from "./prompt.js"; -type ReadlineMock = { - default: { - createInterface: () => { - question: ReturnType; - close: ReturnType; - }; - }; -}; +const readlineState = vi.hoisted(() => { + const question = vi.fn(async () => ""); + const close = vi.fn(); + const createInterface = vi.fn(() => ({ question, close })); + return { question, close, createInterface }; +}); -type PromptModule = typeof import("./prompt.js"); -type GlobalsModule = typeof import("../globals.js"); +vi.mock("node:readline/promises", () => ({ + default: { createInterface: readlineState.createInterface }, +})); -let promptYesNo: PromptModule["promptYesNo"]; -let readline: ReadlineMock; -let isYes: GlobalsModule["isYes"]; -let setVerbose: GlobalsModule["setVerbose"]; -let setYes: GlobalsModule["setYes"]; - -beforeEach(async () => { - vi.resetModules(); - vi.doMock("node:readline/promises", () => { - const question = vi.fn(async () => ""); - const close = vi.fn(); - const createInterface = vi.fn(() => ({ question, close })); - return { default: { createInterface } }; - }); - ({ promptYesNo } = await import("./prompt.js")); - ({ isYes, setVerbose, setYes } = await import("../globals.js")); - readline = (await import("node:readline/promises")) as unknown as ReadlineMock; +beforeEach(() => { + setYes(false); + setVerbose(false); + readlineState.question.mockReset(); + readlineState.question.mockResolvedValue(""); + readlineState.close.mockClear(); + readlineState.createInterface.mockClear(); }); describe("promptYesNo", () => { @@ -43,16 +35,16 @@ describe("promptYesNo", () => { it("asks the question and respects default", async () => { setYes(false); setVerbose(false); - const { question: questionMock } = readline.default.createInterface(); - questionMock.mockResolvedValueOnce(""); + expect(readline).toBeTruthy(); + readlineState.question.mockResolvedValueOnce(""); const resultDefaultYes = await promptYesNo("Continue?", true); expect(resultDefaultYes).toBe(true); - questionMock.mockResolvedValueOnce("n"); + readlineState.question.mockResolvedValueOnce("n"); const resultNo = await promptYesNo("Continue?", true); expect(resultNo).toBe(false); - questionMock.mockResolvedValueOnce("y"); + readlineState.question.mockResolvedValueOnce("y"); const resultYes = await promptYesNo("Continue?", false); expect(resultYes).toBe(true); }); diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index 12a2c72ff1e..dd29b76bb1d 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -1,5 +1,6 @@ import process from "node:process"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { runCli } from "./run-main.js"; const tryRouteCliMock = vi.hoisted(() => vi.fn()); const loadDotEnvMock = vi.hoisted(() => vi.fn()); @@ -67,8 +68,6 @@ vi.mock("./program.js", () => ({ buildProgram: buildProgramMock, })); -const { runCli } = await import("./run-main.js"); - describe("runCli exit behavior", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/cli/secrets-cli.test.ts b/src/cli/secrets-cli.test.ts index 4acbe2ad699..d7eb94d97a0 100644 --- a/src/cli/secrets-cli.test.ts +++ b/src/cli/secrets-cli.test.ts @@ -3,48 +3,83 @@ import os from "node:os"; import path from "node:path"; import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { registerSecretsCli } from "./secrets-cli.js"; -const callGatewayFromCli = vi.fn(); -const runSecretsAudit = vi.fn(); -const resolveSecretsAuditExitCode = vi.fn(); -const runSecretsConfigureInteractive = vi.fn(); -const runSecretsApply = vi.fn(); -const confirm = vi.fn(); +const mocks = vi.hoisted(() => { + const runtimeLogs: string[] = []; + const runtimeErrors: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime = { + log: vi.fn((...args: unknown[]) => { + runtimeLogs.push(stringifyArgs(args)); + }), + error: vi.fn((...args: unknown[]) => { + runtimeErrors.push(stringifyArgs(args)); + }), + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { + callGatewayFromCli: vi.fn(), + runSecretsAudit: vi.fn(), + resolveSecretsAuditExitCode: vi.fn(), + runSecretsConfigureInteractive: vi.fn(), + runSecretsApply: vi.fn(), + confirm: vi.fn(), + defaultRuntime, + runtimeLogs, + runtimeErrors, + }; +}); -const { defaultRuntime, runtimeLogs, runtimeErrors, resetRuntimeCapture } = - createCliRuntimeCapture(); +const { + callGatewayFromCli, + runSecretsAudit, + resolveSecretsAuditExitCode, + runSecretsConfigureInteractive, + runSecretsApply, + confirm, + defaultRuntime, + runtimeLogs, + runtimeErrors, +} = mocks; vi.mock("./gateway-rpc.js", () => ({ addGatewayClientOptions: (cmd: Command) => cmd, callGatewayFromCli: (method: string, opts: unknown, params?: unknown, extra?: unknown) => - callGatewayFromCli(method, opts, params, extra), + mocks.callGatewayFromCli(method, opts, params, extra), })); vi.mock("../runtime.js", () => ({ - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); vi.mock("../secrets/audit.js", () => ({ - runSecretsAudit: (options: unknown) => runSecretsAudit(options), + runSecretsAudit: (options: unknown) => mocks.runSecretsAudit(options), resolveSecretsAuditExitCode: (report: unknown, check: boolean) => - resolveSecretsAuditExitCode(report, check), + mocks.resolveSecretsAuditExitCode(report, check), })); vi.mock("../secrets/configure.js", () => ({ - runSecretsConfigureInteractive: (options: unknown) => runSecretsConfigureInteractive(options), + runSecretsConfigureInteractive: (options: unknown) => + mocks.runSecretsConfigureInteractive(options), })); vi.mock("../secrets/apply.js", () => ({ - runSecretsApply: (options: unknown) => runSecretsApply(options), + runSecretsApply: (options: unknown) => mocks.runSecretsApply(options), })); vi.mock("@clack/prompts", () => ({ - confirm: (options: unknown) => confirm(options), + confirm: (options: unknown) => mocks.confirm(options), })); -const { registerSecretsCli } = await import("./secrets-cli.js"); - function createManualSecretsPlan() { return { version: 1, @@ -126,13 +161,19 @@ describe("secrets CLI", () => { }; beforeEach(() => { - resetRuntimeCapture(); + runtimeLogs.length = 0; + runtimeErrors.length = 0; callGatewayFromCli.mockReset(); runSecretsAudit.mockReset(); resolveSecretsAuditExitCode.mockReset(); runSecretsConfigureInteractive.mockReset(); runSecretsApply.mockReset(); confirm.mockReset(); + defaultRuntime.log.mockClear(); + defaultRuntime.error.mockClear(); + defaultRuntime.writeStdout.mockClear(); + defaultRuntime.writeJson.mockClear(); + defaultRuntime.exit.mockClear(); }); it("calls secrets.reload and prints human output", async () => { diff --git a/src/cli/security-cli.test.ts b/src/cli/security-cli.test.ts index c7264beb001..bf4b945e04f 100644 --- a/src/cli/security-cli.test.ts +++ b/src/cli/security-cli.test.ts @@ -1,43 +1,72 @@ import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { registerSecurityCli } from "./security-cli.js"; -const loadConfig = vi.fn(); -const runSecurityAudit = vi.fn(); -const fixSecurityFootguns = vi.fn(); -const resolveCommandSecretRefsViaGateway = vi.fn(); -const getSecurityAuditCommandSecretTargetIds = vi.fn( - () => new Set(["gateway.auth.token", "gateway.auth.password"]), -); +const mocks = vi.hoisted(() => { + const runtimeLogs: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime = { + log: vi.fn((...args: unknown[]) => { + runtimeLogs.push(stringifyArgs(args)); + }), + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { + loadConfig: vi.fn(), + runSecurityAudit: vi.fn(), + fixSecurityFootguns: vi.fn(), + resolveCommandSecretRefsViaGateway: vi.fn(), + getSecurityAuditCommandSecretTargetIds: vi.fn( + () => new Set(["gateway.auth.token", "gateway.auth.password"]), + ), + defaultRuntime, + runtimeLogs, + }; +}); -const { defaultRuntime, runtimeLogs, resetRuntimeCapture } = createCliRuntimeCapture(); +const { + loadConfig, + runSecurityAudit, + fixSecurityFootguns, + resolveCommandSecretRefsViaGateway, + getSecurityAuditCommandSecretTargetIds, + runtimeLogs, +} = mocks; vi.mock("../config/config.js", () => ({ - loadConfig: () => loadConfig(), + loadConfig: () => mocks.loadConfig(), })); vi.mock("../runtime.js", () => ({ - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); vi.mock("../security/audit.js", () => ({ - runSecurityAudit: (opts: unknown) => runSecurityAudit(opts), + runSecurityAudit: (opts: unknown) => mocks.runSecurityAudit(opts), })); vi.mock("../security/fix.js", () => ({ - fixSecurityFootguns: () => fixSecurityFootguns(), + fixSecurityFootguns: () => mocks.fixSecurityFootguns(), })); vi.mock("./command-secret-gateway.js", () => ({ - resolveCommandSecretRefsViaGateway: (opts: unknown) => resolveCommandSecretRefsViaGateway(opts), + resolveCommandSecretRefsViaGateway: (opts: unknown) => + mocks.resolveCommandSecretRefsViaGateway(opts), })); vi.mock("./command-secret-targets.js", () => ({ - getSecurityAuditCommandSecretTargetIds: () => getSecurityAuditCommandSecretTargetIds(), + getSecurityAuditCommandSecretTargetIds: () => mocks.getSecurityAuditCommandSecretTargetIds(), })); -const { registerSecurityCli } = await import("./security-cli.js"); - function createProgram() { const program = new Command(); program.exitOverride(); @@ -63,7 +92,7 @@ function primeDeepAuditConfig(sourceConfig = { gateway: { mode: "local" } }) { describe("security CLI", () => { beforeEach(() => { - resetRuntimeCapture(); + runtimeLogs.length = 0; loadConfig.mockReset(); runSecurityAudit.mockReset(); fixSecurityFootguns.mockReset(); diff --git a/src/cli/skills-cli.commands.test.ts b/src/cli/skills-cli.commands.test.ts index 58bae163b7b..d77a59fa338 100644 --- a/src/cli/skills-cli.commands.test.ts +++ b/src/cli/skills-cli.commands.test.ts @@ -1,40 +1,76 @@ import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { registerSkillsCli } from "./skills-cli.js"; -const loadConfigMock = vi.fn(() => ({})); -const resolveDefaultAgentIdMock = vi.fn(() => "main"); -const resolveAgentWorkspaceDirMock = vi.fn(() => "/tmp/workspace"); -const searchSkillsFromClawHubMock = vi.fn(); -const installSkillFromClawHubMock = vi.fn(); -const updateSkillsFromClawHubMock = vi.fn(); -const readTrackedClawHubSkillSlugsMock = vi.fn(); +const mocks = vi.hoisted(() => { + const runtimeLogs: string[] = []; + const runtimeErrors: string[] = []; + const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); + const defaultRuntime = { + log: vi.fn((...args: unknown[]) => { + runtimeLogs.push(stringifyArgs(args)); + }), + error: vi.fn((...args: unknown[]) => { + runtimeErrors.push(stringifyArgs(args)); + }), + 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 > 0 ? space : undefined)); + }), + exit: vi.fn((code: number) => { + throw new Error(`__exit__:${code}`); + }), + }; + return { + loadConfigMock: vi.fn(() => ({})), + resolveDefaultAgentIdMock: vi.fn(() => "main"), + resolveAgentWorkspaceDirMock: vi.fn(() => "/tmp/workspace"), + searchSkillsFromClawHubMock: vi.fn(), + installSkillFromClawHubMock: vi.fn(), + updateSkillsFromClawHubMock: vi.fn(), + readTrackedClawHubSkillSlugsMock: vi.fn(), + defaultRuntime, + runtimeLogs, + runtimeErrors, + }; +}); -const { defaultRuntime, runtimeLogs, runtimeErrors, resetRuntimeCapture } = - createCliRuntimeCapture(); +const { + loadConfigMock, + resolveDefaultAgentIdMock, + resolveAgentWorkspaceDirMock, + searchSkillsFromClawHubMock, + installSkillFromClawHubMock, + updateSkillsFromClawHubMock, + readTrackedClawHubSkillSlugsMock, + defaultRuntime, + runtimeLogs, + runtimeErrors, +} = mocks; vi.mock("../runtime.js", () => ({ - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); vi.mock("../config/config.js", () => ({ - loadConfig: () => loadConfigMock(), + loadConfig: () => mocks.loadConfigMock(), })); vi.mock("../agents/agent-scope.js", () => ({ - resolveDefaultAgentId: () => resolveDefaultAgentIdMock(), - resolveAgentWorkspaceDir: () => resolveAgentWorkspaceDirMock(), + resolveDefaultAgentId: () => mocks.resolveDefaultAgentIdMock(), + resolveAgentWorkspaceDir: () => mocks.resolveAgentWorkspaceDirMock(), })); vi.mock("../agents/skills-clawhub.js", () => ({ - searchSkillsFromClawHub: (...args: unknown[]) => searchSkillsFromClawHubMock(...args), - installSkillFromClawHub: (...args: unknown[]) => installSkillFromClawHubMock(...args), - updateSkillsFromClawHub: (...args: unknown[]) => updateSkillsFromClawHubMock(...args), - readTrackedClawHubSkillSlugs: (...args: unknown[]) => readTrackedClawHubSkillSlugsMock(...args), + searchSkillsFromClawHub: (...args: unknown[]) => mocks.searchSkillsFromClawHubMock(...args), + installSkillFromClawHub: (...args: unknown[]) => mocks.installSkillFromClawHubMock(...args), + updateSkillsFromClawHub: (...args: unknown[]) => mocks.updateSkillsFromClawHubMock(...args), + readTrackedClawHubSkillSlugs: (...args: unknown[]) => + mocks.readTrackedClawHubSkillSlugsMock(...args), })); -const { registerSkillsCli } = await import("./skills-cli.js"); - describe("skills cli commands", () => { const createProgram = () => { const program = new Command(); @@ -46,7 +82,8 @@ describe("skills cli commands", () => { const runCommand = (argv: string[]) => createProgram().parseAsync(argv, { from: "user" }); beforeEach(() => { - resetRuntimeCapture(); + runtimeLogs.length = 0; + runtimeErrors.length = 0; loadConfigMock.mockReset(); resolveDefaultAgentIdMock.mockReset(); resolveAgentWorkspaceDirMock.mockReset(); @@ -65,6 +102,11 @@ describe("skills cli commands", () => { }); updateSkillsFromClawHubMock.mockResolvedValue([]); readTrackedClawHubSkillSlugsMock.mockResolvedValue([]); + defaultRuntime.log.mockClear(); + defaultRuntime.error.mockClear(); + defaultRuntime.writeStdout.mockClear(); + defaultRuntime.writeJson.mockClear(); + defaultRuntime.exit.mockClear(); }); it("searches ClawHub skills from the native CLI", async () => { diff --git a/src/cli/update-cli.option-collisions.test.ts b/src/cli/update-cli.option-collisions.test.ts index f455e83cf42..1929009b4b2 100644 --- a/src/cli/update-cli.option-collisions.test.ts +++ b/src/cli/update-cli.option-collisions.test.ts @@ -1,42 +1,44 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { runRegisteredCli } from "../test-utils/command-runner.js"; -import { createCliRuntimeCapture } from "./test-runtime-capture.js"; +import { registerUpdateCli } from "./update-cli.js"; -const updateCommand = vi.fn(async (_opts: unknown) => {}); -const updateStatusCommand = vi.fn(async (_opts: unknown) => {}); -const updateWizardCommand = vi.fn(async (_opts: unknown) => {}); +const mocks = vi.hoisted(() => ({ + updateCommand: vi.fn(async (_opts: unknown) => {}), + updateStatusCommand: vi.fn(async (_opts: unknown) => {}), + updateWizardCommand: vi.fn(async (_opts: unknown) => {}), + defaultRuntime: { + log: vi.fn(), + error: vi.fn(), + writeStdout: vi.fn(), + writeJson: vi.fn(), + exit: vi.fn(), + }, +})); -const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture(); +const { updateCommand, updateStatusCommand, updateWizardCommand, defaultRuntime } = mocks; vi.mock("./update-cli/update-command.js", () => ({ - updateCommand: (opts: unknown) => updateCommand(opts), + updateCommand: (opts: unknown) => mocks.updateCommand(opts), })); vi.mock("./update-cli/status.js", () => ({ - updateStatusCommand: (opts: unknown) => updateStatusCommand(opts), + updateStatusCommand: (opts: unknown) => mocks.updateStatusCommand(opts), })); vi.mock("./update-cli/wizard.js", () => ({ - updateWizardCommand: (opts: unknown) => updateWizardCommand(opts), + updateWizardCommand: (opts: unknown) => mocks.updateWizardCommand(opts), })); vi.mock("../runtime.js", () => ({ - defaultRuntime, + defaultRuntime: mocks.defaultRuntime, })); describe("update cli option collisions", () => { - let registerUpdateCli: typeof import("./update-cli.js").registerUpdateCli; - - beforeAll(async () => { - ({ registerUpdateCli } = await import("./update-cli.js")); - }); - beforeEach(() => { updateCommand.mockClear(); updateStatusCommand.mockClear(); updateWizardCommand.mockClear(); - resetRuntimeCapture(); defaultRuntime.log.mockClear(); defaultRuntime.error.mockClear(); defaultRuntime.writeStdout.mockClear(); diff --git a/src/cli/update-cli/shared.command-runner.test.ts b/src/cli/update-cli/shared.command-runner.test.ts index 678a8a3d6ac..cc34766a7ef 100644 --- a/src/cli/update-cli/shared.command-runner.test.ts +++ b/src/cli/update-cli/shared.command-runner.test.ts @@ -1,13 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createGlobalCommandRunner } from "./shared.js"; -const runCommandWithTimeout = vi.fn(); +const runCommandWithTimeout = vi.hoisted(() => vi.fn()); vi.mock("../../process/exec.js", () => ({ runCommandWithTimeout, })); -const { createGlobalCommandRunner } = await import("./shared.js"); - describe("createGlobalCommandRunner", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/commands/agent.delivery.test.ts b/src/commands/agent.delivery.test.ts index ed097cfe575..64797367431 100644 --- a/src/commands/agent.delivery.test.ts +++ b/src/commands/agent.delivery.test.ts @@ -1,9 +1,10 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ReplyPayload } from "../auto-reply/types.js"; import type { CliDeps } from "../cli/deps.js"; import type { OpenClawConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; import type { RuntimeEnv } from "../runtime.js"; +import { deliverAgentCommandResult } from "./agent/delivery.js"; const mocks = vi.hoisted(() => ({ deliverOutboundPayloads: vi.fn(async () => []), @@ -30,14 +31,7 @@ vi.mock("../infra/outbound/targets.js", async () => { }; }); -let deliverAgentCommandResult: typeof import("./agent/delivery.js").deliverAgentCommandResult; - describe("deliverAgentCommandResult", () => { - beforeAll(async () => { - vi.resetModules(); - ({ deliverAgentCommandResult } = await import("./agent/delivery.js")); - }); - function createRuntime(): RuntimeEnv { return { log: vi.fn(), diff --git a/src/commands/agent/session.test.ts b/src/commands/agent/session.test.ts index af7b6fa02ca..cb1e0702653 100644 --- a/src/commands/agent/session.test.ts +++ b/src/commands/agent/session.test.ts @@ -1,5 +1,6 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; +import { resolveSessionKeyForRequest } from "./session.js"; const mocks = vi.hoisted(() => ({ loadSessionStore: vi.fn(), @@ -22,8 +23,6 @@ vi.mock("../../agents/agent-scope.js", () => ({ listAgentIds: mocks.listAgentIds, })); -let resolveSessionKeyForRequest: typeof import("./session.js").resolveSessionKeyForRequest; - describe("resolveSessionKeyForRequest", () => { const MAIN_STORE_PATH = "/tmp/main-store.json"; const MYBOT_STORE_PATH = "/tmp/mybot-store.json"; @@ -46,11 +45,6 @@ describe("resolveSessionKeyForRequest", () => { mocks.loadSessionStore.mockImplementation((storePath: string) => stores[storePath] ?? {}); }; - beforeAll(async () => { - vi.resetModules(); - ({ resolveSessionKeyForRequest } = await import("./session.js")); - }); - beforeEach(() => { vi.clearAllMocks(); mocks.listAgentIds.mockReturnValue(["main"]); diff --git a/src/commands/agents.bind.test-support.ts b/src/commands/agents.bind.test-support.ts index 39d6965da39..20b65abfa85 100644 --- a/src/commands/agents.bind.test-support.ts +++ b/src/commands/agents.bind.test-support.ts @@ -34,9 +34,11 @@ vi.mock("../config/config.js", async (importOriginal) => { export const runtime = createTestRuntime(); +let agentsCommandModulePromise: Promise | undefined; + export async function loadFreshAgentsCommandModuleForTest() { - vi.resetModules(); - return await import("./agents.js"); + agentsCommandModulePromise ??= import("./agents.js"); + return await agentsCommandModulePromise; } export function resetAgentsBindTestHarness(): void { diff --git a/src/commands/backup.atomic.test.ts b/src/commands/backup.atomic.test.ts index 715984319c5..b528c154b07 100644 --- a/src/commands/backup.atomic.test.ts +++ b/src/commands/backup.atomic.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js"; +import { backupCreateCommand } from "./backup.js"; const tarCreateMock = vi.hoisted(() => vi.fn()); const backupVerifyCommandMock = vi.hoisted(() => vi.fn()); @@ -15,8 +16,6 @@ vi.mock("./backup-verify.js", () => ({ backupVerifyCommand: backupVerifyCommandMock, })); -const { backupCreateCommand } = await import("./backup.js"); - describe("backupCreateCommand atomic archive write", () => { let tempHome: TempHomeEnv; diff --git a/src/commands/channels.add.test.ts b/src/commands/channels.add.test.ts index e35bcd189c7..77af981b212 100644 --- a/src/commands/channels.add.test.ts +++ b/src/commands/channels.add.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; @@ -8,6 +8,7 @@ import { ensureChannelSetupPluginInstalled, loadChannelSetupPluginRegistrySnapshotForChannel, } from "./channel-setup/plugin-install.js"; +import { channelsAddCommand } from "./channels.js"; import { configMocks, offsetMocks } from "./channels.mock-harness.js"; import { createMSTeamsCatalogEntry, @@ -47,7 +48,6 @@ vi.mock("./channel-setup/plugin-install.js", async (importOriginal) => { }); const runtime = createTestRuntime(); -let channelsAddCommand: typeof import("./channels.js").channelsAddCommand; function listConfiguredAccountIds( channelConfig: { accounts?: Record; botToken?: string } | undefined, @@ -219,10 +219,6 @@ async function runSignalAddCommand(afterAccountConfigWritten: SignalAfterAccount } describe("channelsAddCommand", () => { - beforeAll(async () => { - ({ channelsAddCommand } = await import("./channels.js")); - }); - beforeEach(async () => { configMocks.readConfigFileSnapshot.mockClear(); configMocks.writeConfigFile.mockClear(); diff --git a/src/commands/channels.remove.test.ts b/src/commands/channels.remove.test.ts index d9b2a4e075e..8b22835b8ff 100644 --- a/src/commands/channels.remove.test.ts +++ b/src/commands/channels.remove.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; @@ -6,6 +6,7 @@ import { ensureChannelSetupPluginInstalled, loadChannelSetupPluginRegistrySnapshotForChannel, } from "./channel-setup/plugin-install.js"; +import { channelsRemoveCommand } from "./channels.js"; import { configMocks } from "./channels.mock-harness.js"; import { createMSTeamsCatalogEntry, @@ -33,13 +34,8 @@ vi.mock("./channel-setup/plugin-install.js", async (importOriginal) => { }); const runtime = createTestRuntime(); -let channelsRemoveCommand: typeof import("./channels.js").channelsRemoveCommand; describe("channelsRemoveCommand", () => { - beforeAll(async () => { - ({ channelsRemoveCommand } = await import("./channels.js")); - }); - beforeEach(() => { configMocks.readConfigFileSnapshot.mockClear(); configMocks.writeConfigFile.mockClear(); diff --git a/src/commands/channels.resolve.test.ts b/src/commands/channels.resolve.test.ts index 014fa2da059..0a44af519ed 100644 --- a/src/commands/channels.resolve.test.ts +++ b/src/commands/channels.resolve.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { channelsResolveCommand } from "./channels/resolve.js"; const mocks = vi.hoisted(() => ({ resolveCommandSecretRefsViaGateway: vi.fn(), @@ -42,8 +43,6 @@ vi.mock("../channels/plugins/index.js", () => ({ getChannelPlugin: mocks.getChannelPlugin, })); -const { channelsResolveCommand } = await import("./channels/resolve.js"); - describe("channelsResolveCommand", () => { const runtime = { log: vi.fn(), diff --git a/src/commands/channels.status.command-flow.test.ts b/src/commands/channels.status.command-flow.test.ts index 29363b2f2a9..5c9bea70862 100644 --- a/src/commands/channels.status.command-flow.test.ts +++ b/src/commands/channels.status.command-flow.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; +import { channelsStatusCommand } from "./channels/status.js"; const resolveDefaultAccountId = () => DEFAULT_ACCOUNT_ID; @@ -39,8 +40,6 @@ vi.mock("../cli/progress.js", () => ({ withProgress: (opts: unknown, run: () => Promise) => withProgress(opts, run), })); -const { channelsStatusCommand } = await import("./channels/status.js"); - function createTokenOnlyPlugin() { return { id: "discord", diff --git a/src/commands/config-validation.test.ts b/src/commands/config-validation.test.ts index 5bf0870009f..20bd6476b6b 100644 --- a/src/commands/config-validation.test.ts +++ b/src/commands/config-validation.test.ts @@ -1,11 +1,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { PluginCompatibilityNotice } from "../plugins/status.js"; import { createCompatibilityNotice } from "../plugins/status.test-helpers.js"; +import { requireValidConfigSnapshot } from "./config-validation.js"; -const readConfigFileSnapshot = vi.fn(); -const buildPluginCompatibilityNotices = vi.fn<(_params?: unknown) => PluginCompatibilityNotice[]>( - () => [], -); +const { readConfigFileSnapshot, buildPluginCompatibilityNotices } = vi.hoisted(() => ({ + readConfigFileSnapshot: vi.fn(), + buildPluginCompatibilityNotices: vi.fn<(_params?: unknown) => PluginCompatibilityNotice[]>( + () => [], + ), +})); vi.mock("../config/config.js", () => ({ readConfigFileSnapshot, @@ -46,7 +49,6 @@ describe("requireValidConfigSnapshot", () => { createValidSnapshot(); const runtime = createRuntime(); - const { requireValidConfigSnapshot } = await import("./config-validation.js"); const config = await requireValidConfigSnapshot(runtime); expect(config).toEqual({ plugins: {} }); @@ -60,7 +62,6 @@ describe("requireValidConfigSnapshot", () => { createValidSnapshot(); const runtime = createRuntime(); - const { requireValidConfigSnapshot } = await import("./config-validation.js"); const config = await requireValidConfigSnapshot(runtime, { includeCompatibilityAdvisory: true, }); @@ -83,7 +84,6 @@ describe("requireValidConfigSnapshot", () => { }); const runtime = createRuntime(); - const { requireValidConfigSnapshot } = await import("./config-validation.js"); const config = await requireValidConfigSnapshot(runtime, { includeCompatibilityAdvisory: true, }); diff --git a/src/commands/configure.daemon.test.ts b/src/commands/configure.daemon.test.ts index 11b54dc6b19..ff918d936a2 100644 --- a/src/commands/configure.daemon.test.ts +++ b/src/commands/configure.daemon.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { maybeInstallDaemon } from "./configure.daemon.js"; const progressSetLabel = vi.hoisted(() => vi.fn()); const withProgress = vi.hoisted(() => @@ -69,8 +70,6 @@ vi.mock("./systemd-linger.js", () => ({ ensureSystemdUserLingerInteractive, })); -const { maybeInstallDaemon } = await import("./configure.daemon.js"); - describe("maybeInstallDaemon", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/commands/dashboard.links.test.ts b/src/commands/dashboard.links.test.ts index b14996adca1..c8f067da82d 100644 --- a/src/commands/dashboard.links.test.ts +++ b/src/commands/dashboard.links.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { dashboardCommand } from "./dashboard.js"; const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn()); const resolveGatewayPortMock = vi.hoisted(() => vi.fn()); @@ -29,8 +30,6 @@ vi.mock("../secrets/resolve.js", () => ({ resolveSecretRefValues: resolveSecretRefValuesMock, })); -let dashboardCommand: typeof import("./dashboard.js").dashboardCommand; - const runtime = { log: vi.fn(), error: vi.fn(), @@ -63,9 +62,7 @@ function mockSnapshot(token: unknown = "abc") { } describe("dashboardCommand", () => { - beforeEach(async () => { - vi.resetModules(); - ({ dashboardCommand } = await import("./dashboard.js")); + beforeEach(() => { resetRuntime(); readConfigFileSnapshotMock.mockClear(); resolveGatewayPortMock.mockClear(); diff --git a/src/commands/dashboard.test.ts b/src/commands/dashboard.test.ts index dfab6d700b0..e5c1852ccd0 100644 --- a/src/commands/dashboard.test.ts +++ b/src/commands/dashboard.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { GatewayBindMode } from "../config/types.gateway.js"; +import { dashboardCommand } from "./dashboard.js"; const mocks = vi.hoisted(() => ({ readConfigFileSnapshot: vi.fn(), @@ -29,7 +30,6 @@ const runtime = { error: vi.fn(), exit: vi.fn(), }; -let dashboardCommand: typeof import("./dashboard.js").dashboardCommand; function mockSnapshot(params?: { token?: string; @@ -62,9 +62,7 @@ function mockSnapshot(params?: { } describe("dashboardCommand bind selection", () => { - beforeEach(async () => { - vi.resetModules(); - ({ dashboardCommand } = await import("./dashboard.js")); + beforeEach(() => { mocks.readConfigFileSnapshot.mockClear(); mocks.resolveGatewayPort.mockClear(); mocks.resolveControlUiLinks.mockClear(); diff --git a/src/commands/doctor-workspace-status.test.ts b/src/commands/doctor-workspace-status.test.ts index 031405d92f1..d3803b22ccf 100644 --- a/src/commands/doctor-workspace-status.test.ts +++ b/src/commands/doctor-workspace-status.test.ts @@ -5,45 +5,47 @@ import { createTypedHook, } from "../plugins/status.test-helpers.js"; import * as noteModule from "../terminal/note.js"; +import { noteWorkspaceStatus } from "./doctor-workspace-status.js"; -const resolveAgentWorkspaceDirMock = vi.fn(); -const resolveDefaultAgentIdMock = vi.fn(); -const buildWorkspaceSkillStatusMock = vi.fn(); -const buildPluginStatusReportMock = vi.fn(); -const buildPluginCompatibilityWarningsMock = vi.fn(); +const mocks = vi.hoisted(() => ({ + resolveAgentWorkspaceDir: vi.fn(), + resolveDefaultAgentId: vi.fn(), + buildWorkspaceSkillStatus: vi.fn(), + buildPluginStatusReport: vi.fn(), + buildPluginCompatibilityWarnings: vi.fn(), +})); vi.mock("../agents/agent-scope.js", () => ({ - resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args), - resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args), + resolveAgentWorkspaceDir: (...args: unknown[]) => mocks.resolveAgentWorkspaceDir(...args), + resolveDefaultAgentId: (...args: unknown[]) => mocks.resolveDefaultAgentId(...args), })); vi.mock("../agents/skills-status.js", () => ({ - buildWorkspaceSkillStatus: (...args: unknown[]) => buildWorkspaceSkillStatusMock(...args), + buildWorkspaceSkillStatus: (...args: unknown[]) => mocks.buildWorkspaceSkillStatus(...args), })); vi.mock("../plugins/status.js", () => ({ - buildPluginStatusReport: (...args: unknown[]) => buildPluginStatusReportMock(...args), + buildPluginStatusReport: (...args: unknown[]) => mocks.buildPluginStatusReport(...args), buildPluginCompatibilityWarnings: (...args: unknown[]) => - buildPluginCompatibilityWarningsMock(...args), + mocks.buildPluginCompatibilityWarnings(...args), })); async function runNoteWorkspaceStatusForTest( loadResult: ReturnType, compatibilityWarnings: string[] = [], ) { - resolveDefaultAgentIdMock.mockReturnValue("default"); - resolveAgentWorkspaceDirMock.mockReturnValue("/workspace"); - buildWorkspaceSkillStatusMock.mockReturnValue({ + mocks.resolveDefaultAgentId.mockReturnValue("default"); + mocks.resolveAgentWorkspaceDir.mockReturnValue("/workspace"); + mocks.buildWorkspaceSkillStatus.mockReturnValue({ skills: [], }); - buildPluginStatusReportMock.mockReturnValue({ + mocks.buildPluginStatusReport.mockReturnValue({ workspaceDir: "/workspace", ...loadResult, }); - buildPluginCompatibilityWarningsMock.mockReturnValue(compatibilityWarnings); + mocks.buildPluginCompatibilityWarnings.mockReturnValue(compatibilityWarnings); const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {}); - const { noteWorkspaceStatus } = await import("./doctor-workspace-status.js"); noteWorkspaceStatus({}); return noteSpy; } @@ -65,7 +67,7 @@ describe("noteWorkspaceStatus", () => { }), ); try { - expect(buildPluginStatusReportMock).toHaveBeenCalledWith({ + expect(mocks.buildPluginStatusReport).toHaveBeenCalledWith({ config: {}, workspaceDir: "/workspace", }); @@ -138,7 +140,7 @@ describe("noteWorkspaceStatus", () => { "legacy-plugin still uses legacy before_agent_start", ]); try { - expect(buildPluginCompatibilityWarningsMock).toHaveBeenCalledWith({ + expect(mocks.buildPluginCompatibilityWarnings).toHaveBeenCalledWith({ config: {}, workspaceDir: "/workspace", report: { diff --git a/src/commands/doctor.matrix-migration.test.ts b/src/commands/doctor.matrix-migration.test.ts index 2709bf282ad..ae9524f51eb 100644 --- a/src/commands/doctor.matrix-migration.test.ts +++ b/src/commands/doctor.matrix-migration.test.ts @@ -1,23 +1,19 @@ -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createDoctorRuntime, mockDoctorConfigSnapshot, runStartupMatrixMigration, } from "./doctor.e2e-harness.js"; import "./doctor.fast-path-mocks.js"; +import { doctorCommand } from "./doctor.js"; vi.mock("../plugins/providers.runtime.js", () => ({ resolvePluginProviders: vi.fn(() => []), })); const DOCTOR_MIGRATION_TIMEOUT_MS = process.platform === "win32" ? 60_000 : 45_000; -let doctorCommand: typeof import("./doctor.js").doctorCommand; describe("doctor command", () => { - beforeAll(async () => { - ({ doctorCommand } = await import("./doctor.js")); - }); - it( "runs Matrix startup migration during repair flows", { timeout: DOCTOR_MIGRATION_TIMEOUT_MS }, diff --git a/src/commands/doctor/shared/allowlist-policy-repair.test.ts b/src/commands/doctor/shared/allowlist-policy-repair.test.ts index 2d60982b7ea..aac58e9914d 100644 --- a/src/commands/doctor/shared/allowlist-policy-repair.test.ts +++ b/src/commands/doctor/shared/allowlist-policy-repair.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { maybeRepairAllowlistPolicyAllowFrom } from "./allowlist-policy-repair.js"; const { readChannelAllowFromStoreMock } = vi.hoisted(() => ({ readChannelAllowFromStoreMock: vi.fn(), @@ -8,12 +9,8 @@ vi.mock("../../../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: readChannelAllowFromStoreMock, })); -let maybeRepairAllowlistPolicyAllowFrom: typeof import("./allowlist-policy-repair.js").maybeRepairAllowlistPolicyAllowFrom; - describe("doctor allowlist-policy repair", () => { - beforeEach(async () => { - vi.resetModules(); - ({ maybeRepairAllowlistPolicyAllowFrom } = await import("./allowlist-policy-repair.js")); + beforeEach(() => { readChannelAllowFromStoreMock.mockReset(); }); diff --git a/src/commands/gateway-install-token.test.ts b/src/commands/gateway-install-token.test.ts index 0f5c555af8c..436338e8efd 100644 --- a/src/commands/gateway-install-token.test.ts +++ b/src/commands/gateway-install-token.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { resolveGatewayInstallToken } from "./gateway-install-token.js"; const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn()); const replaceConfigFileMock = vi.hoisted(() => vi.fn()); @@ -57,8 +58,6 @@ vi.mock("./onboard-helpers.js", () => ({ randomToken: randomTokenMock, })); -const { resolveGatewayInstallToken } = await import("./gateway-install-token.js"); - describe("resolveGatewayInstallToken", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/commands/gateway-status.test.ts b/src/commands/gateway-status.test.ts index 269c4a4d9e6..ba0dadfac4b 100644 --- a/src/commands/gateway-status.test.ts +++ b/src/commands/gateway-status.test.ts @@ -3,147 +3,161 @@ import type { GatewayProbeResult } from "../gateway/probe.js"; import type { GatewayBonjourBeacon } from "../infra/bonjour-discovery.js"; import type { RuntimeEnv } from "../runtime.js"; import { withEnvAsync } from "../test-utils/env.js"; +import { gatewayStatusCommand } from "./gateway-status.js"; -const readBestEffortConfig = vi.fn(async () => ({ - gateway: { - mode: "remote", - remote: { url: "wss://remote.example:18789", token: "rtok" }, - auth: { token: "ltok" }, - }, -})); -const resolveGatewayPort = vi.fn((_cfg?: unknown) => 18789); -const discoverGatewayBeacons = vi.fn( - async (_opts?: unknown): Promise => [], -); -const pickPrimaryTailnetIPv4 = vi.fn(() => "100.64.0.10"); -const sshStop = vi.fn(async () => {}); -const resolveSshConfig = vi.fn( - async ( - _opts?: unknown, - ): Promise<{ - user: string; - host: string; - port: number; - identityFiles: string[]; - } | null> => null, -); -const startSshPortForward = vi.fn(async (_opts?: unknown) => ({ - parsedTarget: { user: "me", host: "studio", port: 22 }, - localPort: 18789, - remotePort: 18789, - pid: 123, - stderr: [], - stop: sshStop, -})); -const probeGateway = vi.fn(async (opts: { url: string }): Promise => { - const { url } = opts; - if (url.includes("127.0.0.1")) { - return { - ok: true, - url, - connectLatencyMs: 12, - error: null, - close: null, - health: { ok: true }, - status: { - linkChannel: { - id: "whatsapp", - label: "WhatsApp", - linked: false, - authAgeMs: null, - }, - sessions: { count: 0 }, - }, - presence: [ - { - mode: "gateway", - reason: "self", - host: "local", - ip: "127.0.0.1", - text: "Gateway: local (127.0.0.1) · app test · mode gateway · reason self", - ts: Date.now(), - }, - ], - configSnapshot: { - path: "/tmp/cfg.json", - exists: true, - valid: true, - config: { - gateway: { mode: "local" }, - }, - issues: [], - legacyIssues: [], - }, - }; - } +const mocks = vi.hoisted(() => { + const sshStop = vi.fn(async () => {}); return { - ok: true, - url, - connectLatencyMs: 34, - error: null, - close: null, - health: { ok: true }, - status: { - linkChannel: { - id: "whatsapp", - label: "WhatsApp", - linked: true, - authAgeMs: 5_000, + readBestEffortConfig: vi.fn(async () => ({ + gateway: { + mode: "remote", + remote: { url: "wss://remote.example:18789", token: "rtok" }, + auth: { token: "ltok" }, }, - sessions: { count: 2 }, - }, - presence: [ - { - mode: "gateway", - reason: "self", - host: "remote", - ip: "100.64.0.2", - text: "Gateway: remote (100.64.0.2) · app test · mode gateway · reason self", - ts: Date.now(), - }, - ], - configSnapshot: { - path: "/tmp/remote.json", - exists: true, - valid: true, - config: { gateway: { mode: "remote" } }, - issues: [], - legacyIssues: [], - }, + })), + resolveGatewayPort: vi.fn((_cfg?: unknown) => 18789), + discoverGatewayBeacons: vi.fn(async (_opts?: unknown): Promise => []), + pickPrimaryTailnetIPv4: vi.fn(() => "100.64.0.10"), + sshStop, + resolveSshConfig: vi.fn( + async ( + _opts?: unknown, + ): Promise<{ + user: string; + host: string; + port: number; + identityFiles: string[]; + } | null> => null, + ), + startSshPortForward: vi.fn(async (_opts?: unknown) => ({ + parsedTarget: { user: "me", host: "studio", port: 22 }, + localPort: 18789, + remotePort: 18789, + pid: 123, + stderr: [], + stop: sshStop, + })), + probeGateway: vi.fn(async (opts: { url: string }): Promise => { + const { url } = opts; + if (url.includes("127.0.0.1")) { + return { + ok: true, + url, + connectLatencyMs: 12, + error: null, + close: null, + health: { ok: true }, + status: { + linkChannel: { + id: "whatsapp", + label: "WhatsApp", + linked: false, + authAgeMs: null, + }, + sessions: { count: 0 }, + }, + presence: [ + { + mode: "gateway", + reason: "self", + host: "local", + ip: "127.0.0.1", + text: "Gateway: local (127.0.0.1) · app test · mode gateway · reason self", + ts: Date.now(), + }, + ], + configSnapshot: { + path: "/tmp/cfg.json", + exists: true, + valid: true, + config: { + gateway: { mode: "local" }, + }, + issues: [], + legacyIssues: [], + }, + }; + } + return { + ok: true, + url, + connectLatencyMs: 34, + error: null, + close: null, + health: { ok: true }, + status: { + linkChannel: { + id: "whatsapp", + label: "WhatsApp", + linked: true, + authAgeMs: 5_000, + }, + sessions: { count: 2 }, + }, + presence: [ + { + mode: "gateway", + reason: "self", + host: "remote", + ip: "100.64.0.2", + text: "Gateway: remote (100.64.0.2) · app test · mode gateway · reason self", + ts: Date.now(), + }, + ], + configSnapshot: { + path: "/tmp/remote.json", + exists: true, + valid: true, + config: { gateway: { mode: "remote" } }, + issues: [], + legacyIssues: [], + }, + }; + }), }; }); -vi.mock("../config/config.js", () => ({ +const { readBestEffortConfig, - resolveGatewayPort, + discoverGatewayBeacons, + pickPrimaryTailnetIPv4, + sshStop, + resolveSshConfig, + startSshPortForward, + probeGateway, +} = mocks; + +vi.mock("../config/config.js", () => ({ + readBestEffortConfig: mocks.readBestEffortConfig, + resolveGatewayPort: mocks.resolveGatewayPort, })); vi.mock("../infra/bonjour-discovery.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - discoverGatewayBeacons, + discoverGatewayBeacons: mocks.discoverGatewayBeacons, }; }); vi.mock("../infra/tailnet.js", () => ({ - pickPrimaryTailnetIPv4, + pickPrimaryTailnetIPv4: mocks.pickPrimaryTailnetIPv4, })); vi.mock("../infra/ssh-tunnel.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - startSshPortForward, + startSshPortForward: mocks.startSshPortForward, }; }); vi.mock("../infra/ssh-config.js", () => ({ - resolveSshConfig, + resolveSshConfig: mocks.resolveSshConfig, })); vi.mock("../gateway/probe.js", () => ({ - probeGateway, + probeGateway: mocks.probeGateway, })); function createRuntimeCapture() { @@ -194,7 +208,6 @@ async function runGatewayStatus( runtime: ReturnType["runtime"], opts: { timeout: string; json?: boolean; ssh?: string; sshAuto?: boolean; sshIdentity?: string }, ) { - const { gatewayStatusCommand } = await import("./gateway-status.js"); await gatewayStatusCommand(opts, asRuntimeEnv(runtime)); } diff --git a/src/commands/health.snapshot.test.ts b/src/commands/health.snapshot.test.ts index 9772c953692..5b128a92e2e 100644 --- a/src/commands/health.snapshot.test.ts +++ b/src/commands/health.snapshot.test.ts @@ -25,7 +25,6 @@ type TelegramHealthAccount = { }; async function loadFreshHealthModulesForTest() { - vi.resetModules(); vi.doMock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { diff --git a/src/commands/message.default-agent.test.ts b/src/commands/message.default-agent.test.ts index 5bae0a55d8a..ed63f181048 100644 --- a/src/commands/message.default-agent.test.ts +++ b/src/commands/message.default-agent.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { CliDeps } from "../cli/outbound-send-deps.js"; import type { RuntimeEnv } from "../runtime.js"; +import { messageCommand } from "./message.js"; let testConfig: Record = {}; @@ -57,9 +58,6 @@ describe("messageCommand agent routing", () => { error: vi.fn(), exit: vi.fn(), }; - - const { messageCommand } = await import("./message.js"); - await messageCommand( { action: "send", diff --git a/src/commands/message.test.ts b/src/commands/message.test.ts index 2b84b0fcbc4..7f86a2fc12f 100644 --- a/src/commands/message.test.ts +++ b/src/commands/message.test.ts @@ -56,7 +56,6 @@ let envSnapshot: ReturnType; const EMPTY_TEST_REGISTRY = createTestRegistry([]); beforeAll(async () => { - vi.resetModules(); ({ messageCommand } = await import("./message.js")); }); diff --git a/src/commands/models/list.list-command.forward-compat.test.ts b/src/commands/models/list.list-command.forward-compat.test.ts index dd839e94cde..7b50305e340 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -177,7 +177,6 @@ function installModelsListCommandForwardCompatMocks() { } beforeAll(async () => { - vi.resetModules(); installModelsListCommandForwardCompatMocks(); ({ modelsListCommand } = await import("./list.list-command.js")); }); diff --git a/src/commands/models/shared.test.ts b/src/commands/models/shared.test.ts index df7d7eaf3d1..09ea65a1aef 100644 --- a/src/commands/models/shared.test.ts +++ b/src/commands/models/shared.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; +import { loadValidConfigOrThrow, updateConfig } from "./shared.js"; const mocks = vi.hoisted(() => ({ readConfigFileSnapshot: vi.fn(), @@ -11,15 +12,10 @@ vi.mock("../../config/config.js", () => ({ replaceConfigFile: (...args: unknown[]) => mocks.replaceConfigFile(...args), })); -let loadValidConfigOrThrow: typeof import("./shared.js").loadValidConfigOrThrow; -let updateConfig: typeof import("./shared.js").updateConfig; - describe("models/shared", () => { - beforeEach(async () => { - vi.resetModules(); + beforeEach(() => { mocks.readConfigFileSnapshot.mockClear(); mocks.replaceConfigFile.mockClear(); - ({ loadValidConfigOrThrow, updateConfig } = await import("./shared.js")); }); it("returns config when snapshot is valid", async () => { diff --git a/src/commands/onboard-non-interactive/local/daemon-install.test.ts b/src/commands/onboard-non-interactive/local/daemon-install.test.ts index d45cf4cafad..7689eb9ddbd 100644 --- a/src/commands/onboard-non-interactive/local/daemon-install.test.ts +++ b/src/commands/onboard-non-interactive/local/daemon-install.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; +import { installGatewayDaemonNonInteractive } from "./daemon-install.js"; const buildGatewayInstallPlan = vi.hoisted(() => vi.fn()); const gatewayInstallErrorHint = vi.hoisted(() => vi.fn(() => "hint")); @@ -36,8 +37,6 @@ vi.mock("../../systemd-linger.js", () => ({ ensureSystemdUserLingerNonInteractive, })); -const { installGatewayDaemonNonInteractive } = await import("./daemon-install.js"); - describe("installGatewayDaemonNonInteractive", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/commands/onboard-remote.test.ts b/src/commands/onboard-remote.test.ts index c382d4019d8..5f6de11dc00 100644 --- a/src/commands/onboard-remote.test.ts +++ b/src/commands/onboard-remote.test.ts @@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/config.js"; import type { GatewayBonjourBeacon } from "../infra/bonjour-discovery.js"; import { captureEnv } from "../test-utils/env.js"; import type { WizardPrompter } from "../wizard/prompts.js"; +import { promptRemoteGatewayConfig } from "./onboard-remote.js"; import { createWizardPrompter } from "./test-wizard-helpers.js"; const discoverGatewayBeacons = vi.hoisted(() => vi.fn<() => Promise>()); @@ -25,8 +26,6 @@ vi.mock("./onboard-helpers.js", () => ({ detectBinary, })); -const { promptRemoteGatewayConfig } = await import("./onboard-remote.js"); - function createPrompter(overrides: Partial): WizardPrompter { return createWizardPrompter(overrides, { defaultSelect: "" }); } diff --git a/src/commands/onboard-search.providers.test.ts b/src/commands/onboard-search.providers.test.ts index 6677871f915..c62360d335c 100644 --- a/src/commands/onboard-search.providers.test.ts +++ b/src/commands/onboard-search.providers.test.ts @@ -98,7 +98,6 @@ describe("onboard-search provider resolution", () => { let mod: typeof import("./onboard-search.js"); beforeAll(async () => { - vi.resetModules(); mod = await import("./onboard-search.js"); }); diff --git a/src/commands/onboard.test.ts b/src/commands/onboard.test.ts index daa0899dd87..3b2005d3b3e 100644 --- a/src/commands/onboard.test.ts +++ b/src/commands/onboard.test.ts @@ -1,6 +1,7 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { RuntimeEnv } from "../runtime.js"; +import { onboardCommand, setupWizardCommand } from "./onboard.js"; const mocks = vi.hoisted(() => ({ runInteractiveSetup: vi.fn(async () => {}), @@ -26,8 +27,6 @@ vi.mock("./onboard-helpers.js", () => ({ handleReset: mocks.handleReset, })); -const { onboardCommand, setupWizardCommand } = await import("./onboard.js"); - function makeRuntime(): RuntimeEnv { return { log: vi.fn(), diff --git a/src/commands/reset.test.ts b/src/commands/reset.test.ts index b97545a4371..06bbaaebfb7 100644 --- a/src/commands/reset.test.ts +++ b/src/commands/reset.test.ts @@ -1,6 +1,5 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createNonExitingRuntime } from "../runtime.js"; - const resolveCleanupPlanFromDisk = vi.fn(); const removePath = vi.fn(); const listAgentSessionDirs = vi.fn(); @@ -22,10 +21,13 @@ vi.mock("./cleanup-utils.js", () => ({ removeWorkspaceDirs, })); -const { resetCommand } = await import("./reset.js"); - describe("resetCommand", () => { const runtime = createNonExitingRuntime(); + let resetCommand: typeof import("./reset.js").resetCommand; + + beforeAll(async () => { + ({ resetCommand } = await import("./reset.js")); + }); beforeEach(() => { vi.clearAllMocks(); diff --git a/src/commands/sandbox-explain.test.ts b/src/commands/sandbox-explain.test.ts index abc67886c19..349f0eb2f27 100644 --- a/src/commands/sandbox-explain.test.ts +++ b/src/commands/sandbox-explain.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { sandboxExplainCommand } from "./sandbox-explain.js"; const SANDBOX_EXPLAIN_TEST_TIMEOUT_MS = process.platform === "win32" ? 45_000 : 30_000; @@ -12,8 +13,6 @@ vi.mock("../config/config.js", async (importOriginal) => { }; }); -const { sandboxExplainCommand } = await import("./sandbox-explain.js"); - describe("sandbox explain command", () => { it("prints JSON shape + fix-it keys", { timeout: SANDBOX_EXPLAIN_TEST_TIMEOUT_MS }, async () => { mockCfg = { diff --git a/src/commands/status-json.test.ts b/src/commands/status-json.test.ts index c51f073d062..06ab265b255 100644 --- a/src/commands/status-json.test.ts +++ b/src/commands/status-json.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { RuntimeEnv } from "../runtime.js"; +import { statusJsonCommand } from "./status-json.js"; const mocks = vi.hoisted(() => ({ scanStatusJsonFast: vi.fn(), @@ -41,8 +42,6 @@ vi.mock("../infra/update-channels.js", () => ({ resolveUpdateChannelDisplay: mocks.resolveUpdateChannelDisplay, })); -const { statusJsonCommand } = await import("./status-json.js"); - function createRuntimeCapture() { const logs: string[] = []; const runtime: RuntimeEnv = { diff --git a/src/commands/status.summary.test.ts b/src/commands/status.summary.test.ts index 23fbf70778c..a5a463ebb8c 100644 --- a/src/commands/status.summary.test.ts +++ b/src/commands/status.summary.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../channels/config-presence.js", () => ({ hasPotentialConfiguredChannels: vi.fn(() => true), @@ -113,21 +113,20 @@ vi.mock("./status.link-channel.js", () => ({ const { hasPotentialConfiguredChannels } = await import("../channels/config-presence.js"); const { buildChannelSummary } = await import("../infra/channel-summary.js"); const { resolveLinkChannelContext } = await import("./status.link-channel.js"); - -async function loadStatusSummaryForTest() { - vi.resetModules(); - const { getStatusSummary } = await import("./status.summary.js"); - const { statusSummaryRuntime } = await import("./status.summary.runtime.js"); - return { getStatusSummary, statusSummaryRuntime }; -} +let getStatusSummary: typeof import("./status.summary.js").getStatusSummary; +let statusSummaryRuntime: typeof import("./status.summary.runtime.js").statusSummaryRuntime; describe("getStatusSummary", () => { + beforeAll(async () => { + ({ getStatusSummary } = await import("./status.summary.js")); + ({ statusSummaryRuntime } = await import("./status.summary.runtime.js")); + }); + beforeEach(() => { vi.clearAllMocks(); }); it("includes runtimeVersion in the status payload", async () => { - const { getStatusSummary } = await loadStatusSummaryForTest(); const summary = await getStatusSummary(); expect(summary.runtimeVersion).toBe("2026.3.8"); @@ -138,7 +137,6 @@ describe("getStatusSummary", () => { }); it("skips channel summary imports when no channels are configured", async () => { - const { getStatusSummary } = await loadStatusSummaryForTest(); vi.mocked(hasPotentialConfiguredChannels).mockReturnValue(false); const summary = await getStatusSummary(); @@ -150,7 +148,6 @@ describe("getStatusSummary", () => { }); it("does not trigger async context warmup while building status summaries", async () => { - const { getStatusSummary, statusSummaryRuntime } = await loadStatusSummaryForTest(); await getStatusSummary(); expect(vi.mocked(statusSummaryRuntime.resolveContextTokensForModel)).toHaveBeenCalledWith( diff --git a/src/commands/tasks.test.ts b/src/commands/tasks.test.ts index 4020787ab43..a419527a467 100644 --- a/src/commands/tasks.test.ts +++ b/src/commands/tasks.test.ts @@ -1,18 +1,41 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createCliRuntimeCapture } from "../cli/test-runtime-capture.js"; +import { + tasksAuditCommand, + tasksCancelCommand, + tasksListCommand, + tasksMaintenanceCommand, + tasksNotifyCommand, + tasksShowCommand, +} from "./tasks.js"; -const reconcileInspectableTasksMock = vi.fn(); -const reconcileTaskLookupTokenMock = vi.fn(); -const listTaskAuditFindingsMock = vi.fn(); -const summarizeTaskAuditFindingsMock = vi.fn(); -const previewTaskRegistryMaintenanceMock = vi.fn(); -const runTaskRegistryMaintenanceMock = vi.fn(); -const getInspectableTaskRegistrySummaryMock = vi.fn(); -const getInspectableTaskAuditSummaryMock = vi.fn(); -const updateTaskNotifyPolicyByIdMock = vi.fn(); -const cancelTaskByIdMock = vi.fn(); -const getTaskByIdMock = vi.fn(); -const loadConfigMock = vi.fn(() => ({ loaded: true })); +const mocks = vi.hoisted(() => ({ + reconcileInspectableTasksMock: vi.fn(), + reconcileTaskLookupTokenMock: vi.fn(), + listTaskAuditFindingsMock: vi.fn(), + summarizeTaskAuditFindingsMock: vi.fn(), + previewTaskRegistryMaintenanceMock: vi.fn(), + runTaskRegistryMaintenanceMock: vi.fn(), + getInspectableTaskRegistrySummaryMock: vi.fn(), + getInspectableTaskAuditSummaryMock: vi.fn(), + updateTaskNotifyPolicyByIdMock: vi.fn(), + cancelTaskByIdMock: vi.fn(), + getTaskByIdMock: vi.fn(), + loadConfigMock: vi.fn(() => ({ loaded: true })), +})); + +const reconcileInspectableTasksMock = mocks.reconcileInspectableTasksMock; +const reconcileTaskLookupTokenMock = mocks.reconcileTaskLookupTokenMock; +const listTaskAuditFindingsMock = mocks.listTaskAuditFindingsMock; +const summarizeTaskAuditFindingsMock = mocks.summarizeTaskAuditFindingsMock; +const previewTaskRegistryMaintenanceMock = mocks.previewTaskRegistryMaintenanceMock; +const runTaskRegistryMaintenanceMock = mocks.runTaskRegistryMaintenanceMock; +const getInspectableTaskRegistrySummaryMock = mocks.getInspectableTaskRegistrySummaryMock; +const getInspectableTaskAuditSummaryMock = mocks.getInspectableTaskAuditSummaryMock; +const updateTaskNotifyPolicyByIdMock = mocks.updateTaskNotifyPolicyByIdMock; +const cancelTaskByIdMock = mocks.cancelTaskByIdMock; +const getTaskByIdMock = mocks.getTaskByIdMock; +const loadConfigMock = mocks.loadConfigMock; vi.mock("../tasks/task-registry.reconcile.js", () => ({ reconcileInspectableTasks: (...args: unknown[]) => reconcileInspectableTasksMock(...args), @@ -51,13 +74,6 @@ const { resetRuntimeCapture, } = createCliRuntimeCapture(); -let tasksListCommand: typeof import("./tasks.js").tasksListCommand; -let tasksShowCommand: typeof import("./tasks.js").tasksShowCommand; -let tasksNotifyCommand: typeof import("./tasks.js").tasksNotifyCommand; -let tasksCancelCommand: typeof import("./tasks.js").tasksCancelCommand; -let tasksAuditCommand: typeof import("./tasks.js").tasksAuditCommand; -let tasksMaintenanceCommand: typeof import("./tasks.js").tasksMaintenanceCommand; - const taskFixture = { taskId: "task-12345678", runtime: "acp", @@ -74,17 +90,6 @@ const taskFixture = { progressSummary: "No output for 60s. It may be waiting for input.", } as const; -beforeAll(async () => { - ({ - tasksListCommand, - tasksShowCommand, - tasksNotifyCommand, - tasksCancelCommand, - tasksAuditCommand, - tasksMaintenanceCommand, - } = await import("./tasks.js")); -}); - describe("tasks commands", () => { beforeEach(() => { vi.clearAllMocks();