diff --git a/extensions/anthropic-vertex/region.adc.test.ts b/extensions/anthropic-vertex/region.adc.test.ts index 605820c9fce..8c40149efef 100644 --- a/extensions/anthropic-vertex/region.adc.test.ts +++ b/extensions/anthropic-vertex/region.adc.test.ts @@ -5,8 +5,8 @@ const { existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({ readFileSyncMock: vi.fn(), })); -vi.mock("node:fs", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("node:fs", async () => { + const actual = await vi.importActual("node:fs"); existsSyncMock.mockImplementation((pathname) => actual.existsSync(pathname)); readFileSyncMock.mockImplementation((pathname, options) => String(pathname) === "/tmp/vertex-adc.json" diff --git a/extensions/discord/src/test-support/configured-binding-runtime.ts b/extensions/discord/src/test-support/configured-binding-runtime.ts index 4499715c7df..fbfc4149535 100644 --- a/extensions/discord/src/test-support/configured-binding-runtime.ts +++ b/extensions/discord/src/test-support/configured-binding-runtime.ts @@ -14,9 +14,9 @@ export async function createConfiguredBindingConversationRuntimeModuleMock< ...args: Parameters ) => ReturnType; }, - importOriginal: () => Promise, + loadActual: () => Promise, ) { - const actual = await importOriginal(); + const actual = await loadActual(); return { ...actual, ensureConfiguredBindingRouteReady: ( diff --git a/extensions/discord/src/test-support/provider.test-support.ts b/extensions/discord/src/test-support/provider.test-support.ts index 694decead62..07544de33c2 100644 --- a/extensions/discord/src/test-support/provider.test-support.ts +++ b/extensions/discord/src/test-support/provider.test-support.ts @@ -274,8 +274,8 @@ export const baseConfig = (): OpenClawConfig => }, }) as OpenClawConfig; -vi.mock("@buape/carbon", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("@buape/carbon", async () => { + const actual = await vi.importActual("@buape/carbon"); class RateLimitError extends Error { status = 429; discordCode?: number; diff --git a/extensions/feishu/src/lifecycle.test-support.ts b/extensions/feishu/src/lifecycle.test-support.ts index 52fd6fac459..da450d965b2 100644 --- a/extensions/feishu/src/lifecycle.test-support.ts +++ b/extensions/feishu/src/lifecycle.test-support.ts @@ -123,8 +123,10 @@ vi.mock("./send.js", () => ({ sendMessageFeishu: sendMessageFeishuMock, })); -vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/conversation-runtime", + ); return { ...actual, resolveConfiguredBindingRoute: ( diff --git a/extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts b/extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts index f811bdc75d5..1a1d44e3f9f 100644 --- a/extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts @@ -10,8 +10,8 @@ const { downloadMatrixMediaMock } = vi.hoisted(() => ({ downloadMatrixMediaMock: vi.fn(), })); -vi.mock("./media.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("./media.js", async () => { + const actual = await vi.importActual("./media.js"); return { ...actual, downloadMatrixMedia: (...args: unknown[]) => downloadMatrixMediaMock(...args), diff --git a/extensions/telegram/src/bot-message-context.route-test-support.ts b/extensions/telegram/src/bot-message-context.route-test-support.ts index 360088b78b6..61036cff8a7 100644 --- a/extensions/telegram/src/bot-message-context.route-test-support.ts +++ b/extensions/telegram/src/bot-message-context.route-test-support.ts @@ -8,8 +8,10 @@ const hoisted = vi.hoisted((): { recordInboundSessionMock: AsyncUnknownMock } => export const recordInboundSessionMock: AsyncUnknownMock = hoisted.recordInboundSessionMock; -vi.mock("./bot-message-context.session.runtime.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("./bot-message-context.session.runtime.js", async () => { + const actual = await vi.importActual( + "./bot-message-context.session.runtime.js", + ); return { ...actual, recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), diff --git a/extensions/telegram/src/bot.media.e2e-harness.ts b/extensions/telegram/src/bot.media.e2e-harness.ts index 0d6b182800d..1cd604ae0cb 100644 --- a/extensions/telegram/src/bot.media.e2e-harness.ts +++ b/extensions/telegram/src/bot.media.e2e-harness.ts @@ -204,8 +204,10 @@ vi.mock("./telegram-media.runtime.js", () => ({ saveMediaBuffer: (...args: Parameters) => saveMediaBufferSpy(...args), })); -vi.doMock("./bot-message-context.session.runtime.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.doMock("./bot-message-context.session.runtime.js", async () => { + const actual = await vi.importActual( + "./bot-message-context.session.runtime.js", + ); return { ...actual, readSessionUpdatedAt: () => undefined, diff --git a/extensions/telegram/src/target-writeback.test-shared.ts b/extensions/telegram/src/target-writeback.test-shared.ts index ba21a7a77e6..a1ab8f55514 100644 --- a/extensions/telegram/src/target-writeback.test-shared.ts +++ b/extensions/telegram/src/target-writeback.test-shared.ts @@ -10,8 +10,10 @@ export const loadCronStore: AsyncUnknownMock = vi.fn(); export const resolveCronStorePath: UnknownMock = vi.fn(); export const saveCronStore: AsyncUnknownMock = vi.fn(); -vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/config-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/config-runtime", + ); return { ...actual, readConfigFileSnapshotForWrite, diff --git a/extensions/zalo/test-support/monitor-mocks-test-support.ts b/extensions/zalo/test-support/monitor-mocks-test-support.ts index 1f014fc8aef..6f49cc61ff6 100644 --- a/extensions/zalo/test-support/monitor-mocks-test-support.ts +++ b/extensions/zalo/test-support/monitor-mocks-test-support.ts @@ -55,8 +55,8 @@ export const sendPhotoMock = lifecycleMocks.sendPhotoMock; export const getZaloRuntimeMock: UnknownMock = lifecycleMocks.getZaloRuntimeMock; function installLifecycleModuleMocks() { - vi.doMock(apiModuleId, async (importOriginal) => { - const actual = await importOriginal(); + vi.doMock(apiModuleId, async () => { + const actual = await vi.importActual(apiModuleId); return { ...actual, deleteWebhook: lifecycleMocks.deleteWebhookMock, diff --git a/src/agents/command/attempt-execution.ts b/src/agents/command/attempt-execution.ts index cc872694717..875110e1d91 100644 --- a/src/agents/command/attempt-execution.ts +++ b/src/agents/command/attempt-execution.ts @@ -396,6 +396,7 @@ export function runAgentAttempt(params: { sessionKey: params.sessionKey, storePath: params.storePath, entry: updatedEntry, + clearedFields: ["cliSessionBindings", "cliSessionIds", "claudeCliSessionId"], }); params.sessionEntry = updatedEntry; diff --git a/src/agents/subagent-registry.lifecycle-retry-grace.e2e.test.ts b/src/agents/subagent-registry.lifecycle-retry-grace.e2e.test.ts index 589bdbb7c88..bd431a8694e 100644 --- a/src/agents/subagent-registry.lifecycle-retry-grace.e2e.test.ts +++ b/src/agents/subagent-registry.lifecycle-retry-grace.e2e.test.ts @@ -1,4 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { __testing as subagentAnnounceDeliveryTesting } from "./subagent-announce-delivery.js"; +import { __testing as subagentAnnounceOutputTesting } from "./subagent-announce-output.js"; +import { __testing as subagentAnnounceTesting } from "./subagent-announce.js"; +import * as mod from "./subagent-registry.js"; const noop = () => {}; const MAIN_REQUESTER_SESSION_KEY = "agent:main:main"; @@ -83,25 +87,11 @@ const loadConfigMock = vi.fn(() => ({ agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } }, session: { mainKey: "main", scope: "per-sender" }, })); -const loadRegistryMock = vi.fn(() => new Map()); -const saveRegistryMock = vi.fn(() => {}); - -vi.mock("../gateway/call.js", () => ({ - callGateway: callGatewayMock, +const registryStoreMocks = vi.hoisted(() => ({ + loadRegistryMock: vi.fn(() => new Map()), + saveRegistryMock: vi.fn(() => {}), })); -vi.mock("../infra/agent-events.js", () => ({ - onAgentEvent: onAgentEventMock, -})); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: loadConfigMock, - }; -}); - vi.mock("../config/sessions.js", () => ({ loadSessionStore: vi.fn(() => sessionStore), resolveAgentIdFromSessionKey: (key: string) => key.match(/^agent:([^:]+)/)?.[1] ?? "main", @@ -114,76 +104,22 @@ vi.mock("../plugins/hook-runner-global.js", () => ({ getGlobalHookRunner: vi.fn(() => null), })); -vi.mock("./pi-embedded.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - isEmbeddedPiRunActive: () => false, - queueEmbeddedPiMessage: () => false, - waitForEmbeddedPiRunEnd: async () => true, - }; -}); - vi.mock("./subagent-depth.js", () => ({ getSubagentDepthFromSessionStore: () => 0, })); vi.mock("./subagent-registry.store.js", () => ({ - loadSubagentRegistryFromDisk: loadRegistryMock, - saveSubagentRegistryToDisk: saveRegistryMock, + loadSubagentRegistryFromDisk: registryStoreMocks.loadRegistryMock, + saveSubagentRegistryToDisk: registryStoreMocks.saveRegistryMock, })); describe("subagent registry lifecycle error grace", () => { - let mod: typeof import("./subagent-registry.js"); - - const installRegistryMocks = () => { - vi.doMock("../gateway/call.js", () => ({ - callGateway: callGatewayMock, - })); - vi.doMock("../infra/agent-events.js", () => ({ - onAgentEvent: onAgentEventMock, - })); - vi.doMock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: loadConfigMock, - }; - }); - vi.doMock("../config/sessions.js", () => ({ - loadSessionStore: vi.fn(() => sessionStore), - resolveAgentIdFromSessionKey: (key: string) => key.match(/^agent:([^:]+)/)?.[1] ?? "main", - resolveStorePath: () => "/tmp/test-store", - resolveMainSessionKey: () => "agent:main:main", - updateSessionStore: vi.fn(), - })); - vi.doMock("../plugins/hook-runner-global.js", () => ({ - getGlobalHookRunner: vi.fn(() => null), - })); - vi.doMock("./pi-embedded.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - isEmbeddedPiRunActive: () => false, - queueEmbeddedPiMessage: () => false, - waitForEmbeddedPiRunEnd: async () => true, - }; - }); - vi.doMock("./subagent-depth.js", () => ({ - getSubagentDepthFromSessionStore: () => 0, - })); - vi.doMock("./subagent-registry.store.js", () => ({ - loadSubagentRegistryFromDisk: loadRegistryMock, - saveSubagentRegistryToDisk: saveRegistryMock, - })); - }; - beforeEach(async () => { - vi.resetModules(); - installRegistryMocks(); vi.useFakeTimers(); callGatewayMock.mockClear(); onAgentEventMock.mockClear(); + registryStoreMocks.loadRegistryMock.mockClear().mockReturnValue(new Map()); + registryStoreMocks.saveRegistryMock.mockClear(); loadConfigMock.mockClear().mockReturnValue({ agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } }, session: { mainKey: "main", scope: "per-sender" }, @@ -213,11 +149,32 @@ describe("subagent registry lifecycle error grace", () => { }, }, ); - mod = await import("./subagent-registry.js"); + mod.__testing.setDepsForTest({ + callGateway: callGatewayMock as typeof import("../gateway/call.js").callGateway, + loadConfig: loadConfigMock as typeof import("../config/config.js").loadConfig, + onAgentEvent: + onAgentEventMock as unknown as typeof import("../infra/agent-events.js").onAgentEvent, + }); + subagentAnnounceTesting.setDepsForTest({ + callGateway: callGatewayMock as typeof import("../gateway/call.js").callGateway, + loadConfig: loadConfigMock as typeof import("../config/config.js").loadConfig, + }); + subagentAnnounceDeliveryTesting.setDepsForTest({ + callGateway: callGatewayMock as typeof import("../gateway/call.js").callGateway, + loadConfig: loadConfigMock as typeof import("../config/config.js").loadConfig, + }); + subagentAnnounceOutputTesting.setDepsForTest({ + callGateway: callGatewayMock as typeof import("../gateway/call.js").callGateway, + loadConfig: loadConfigMock as typeof import("../config/config.js").loadConfig, + }); }); afterEach(() => { lifecycleHandler = undefined; + subagentAnnounceDeliveryTesting.setDepsForTest(); + subagentAnnounceOutputTesting.setDepsForTest(); + subagentAnnounceTesting.setDepsForTest(); + mod.__testing.setDepsForTest(); mod.resetSubagentRegistryForTests({ persist: false }); vi.useRealTimers(); }); @@ -241,20 +198,6 @@ describe("subagent registry lifecycle error grace", () => { throw new Error(`run ${runId} did not reach cleanupHandled=false in time`); }; - const waitForCleanupCompleted = async (runId: string) => { - for (let attempt = 0; attempt < 40; attempt += 1) { - const run = mod - .listSubagentRunsForRequester(MAIN_REQUESTER_SESSION_KEY) - .find((candidate) => candidate.runId === runId); - if (typeof run?.cleanupCompletedAt === "number") { - return run; - } - await vi.advanceTimersByTimeAsync(1); - await flushAsync(); - } - throw new Error(`run ${runId} did not complete cleanup in time`); - }; - const waitForAgentCallCount = async (expectedCount: number) => { for (let attempt = 0; attempt < 80; attempt += 1) { if (getAgentCalls().length >= expectedCount) { @@ -368,7 +311,6 @@ describe("subagent registry lifecycle error grace", () => { await waitForAgentCallCount(1); expect(readFirstAnnounceOutcome()?.status).toBe("ok"); - await waitForCleanupCompleted("run-transient-error"); }); it("announces error when lifecycle error remains terminal after grace window", async () => { @@ -389,7 +331,6 @@ describe("subagent registry lifecycle error grace", () => { await waitForAgentCallCount(1); expect(readFirstAnnounceOutcome()?.status).toBe("error"); expect(readFirstAnnounceOutcome()?.error).toContain("fatal failure"); - await waitForCleanupCompleted("run-terminal-error"); }); it("freezes completion result at run termination across deferred announce retries", async () => { @@ -519,7 +460,13 @@ describe("subagent registry lifecycle error grace", () => { expect(cappedResults[0]).toContain("[truncated: frozen completion output exceeded 100KB"); expect(Buffer.byteLength(cappedResults[0] ?? "", "utf8")).toBeLessThanOrEqual(100 * 1024); - const run = await waitForCleanupCompleted("run-capped"); + const run = mod + .listSubagentRunsForRequester(MAIN_REQUESTER_SESSION_KEY) + .find((candidate) => candidate.runId === "run-capped"); + expect(run).toBeDefined(); + if (!run) { + throw new Error("expected capped run to exist"); + } expect(typeof run.frozenResultText).toBe("string"); expect(run.frozenResultText).toContain("[truncated: frozen completion output exceeded 100KB"); expect(run.frozenResultCapturedAt).toBeTypeOf("number"); diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index 02f4031a9ea..897216fe24f 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -65,27 +65,6 @@ const imageProviderHarness = vi.hoisted(() => { }; }); -vi.mock("../../media-understanding/runner.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - buildProviderRegistry: (overrides?: Record) => - imageProviderHarness.buildProviderRegistry(overrides), - }; -}); - -vi.mock("../../media-understanding/provider-registry.js", async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - getMediaUnderstandingProvider: ( - id: string, - registry: Map, - ) => imageProviderHarness.getMediaUnderstandingProvider(id, registry), - }; -}); - vi.mock("../bash-tools.js", () => ({ createExecTool: vi.fn(() => piToolsHarness.createStubTool("exec")), createProcessTool: vi.fn(() => piToolsHarness.createStubTool("process")), @@ -142,6 +121,15 @@ async function writeAuthProfiles(agentDir: string, profiles: unknown) { async function createOpenClawCodingToolsWithFreshModules(options?: CreateOpenClawCodingToolsArgs) { vi.resetModules(); + const freshImageTool = await import("./image-tool.js"); + freshImageTool.__testing.setProviderDepsForTest({ + buildProviderRegistry: (overrides?: Record) => + imageProviderHarness.buildProviderRegistry(overrides), + getMediaUnderstandingProvider: ( + id: string, + registry: Map, + ) => imageProviderHarness.getMediaUnderstandingProvider(id, registry), + }); const { createOpenClawCodingTools } = await import("../pi-tools.js"); return createOpenClawCodingTools(options); } @@ -883,20 +871,22 @@ describe("image tool implicit imageModel config", () => { }); }); - it("allows local image paths outside default media roots when workspaceOnly is off", async () => { + it("still rejects temp workspace paths outside allowed local roots when workspaceOnly is off", async () => { await withTempWorkspacePng(async ({ workspaceDir, imagePath }) => { const fetch = stubMinimaxOkFetch(); await withTempAgentDir(async (agentDir) => { const cfg = createMinimaxImageConfig(); const withoutWorkspace = createRequiredImageTool({ config: cfg, agentDir }); - await expectImageToolExecOk(withoutWorkspace, imagePath); + await expect( + withoutWorkspace.execute("t1", { prompt: "Describe.", image: imagePath }), + ).rejects.toThrow(/not under an allowed directory/i); const withWorkspace = createRequiredImageTool({ config: cfg, agentDir, workspaceDir }); await expectImageToolExecOk(withWorkspace, imagePath); - expect(fetch).toHaveBeenCalledTimes(2); + expect(fetch).toHaveBeenCalledTimes(1); }); }); }); @@ -933,7 +923,7 @@ describe("image tool implicit imageModel config", () => { }); }); - it("allows non-workspace local image paths when workspaceOnly is disabled", async () => { + it("still rejects non-workspace local image paths when workspaceOnly is disabled", async () => { const fetch = stubMinimaxOkFetch(); await withTempAgentDir(async (agentDir) => { const cfg = createMinimaxImageConfig(); @@ -947,8 +937,10 @@ describe("image tool implicit imageModel config", () => { fsPolicy: { workspaceOnly: false }, }); - await expectImageToolExecOk(tool, outsideImage); - expect(fetch).toHaveBeenCalledTimes(1); + await expect( + tool.execute("t1", { prompt: "Describe.", image: outsideImage }), + ).rejects.toThrow(/not under an allowed directory/i); + expect(fetch).not.toHaveBeenCalled(); } finally { await fs.rm(outsideDir, { recursive: true, force: true }); } diff --git a/src/auto-reply/reply/get-reply-inline-actions.skip-when-config-empty.test.ts b/src/auto-reply/reply/get-reply-inline-actions.skip-when-config-empty.test.ts index f1843f9a604..1d5b6b47ee9 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.skip-when-config-empty.test.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.skip-when-config-empty.test.ts @@ -26,8 +26,10 @@ async function loadFreshInlineActionsModuleForTest() { vi.doMock("../../agents/openclaw-tools.runtime.js", () => ({ createOpenClawTools: (...args: unknown[]) => createOpenClawToolsMock(...args), })); - vi.doMock("../../channels/plugins/index.js", async (importOriginal) => { - const actual = await importOriginal(); + vi.doMock("../../channels/plugins/index.js", async () => { + const actual = await vi.importActual( + "../../channels/plugins/index.js", + ); return { ...actual, getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), @@ -126,7 +128,11 @@ describe("handleInlineActions", () => { buildStatusReplyMock.mockResolvedValue({ text: "status" }); createOpenClawToolsMock.mockReturnValue([]); getChannelPluginMock.mockImplementation((channelId?: string) => - channelId === "whatsapp" ? { commands: { skipWhenConfigEmpty: true } } : undefined, + channelId === "whatsapp" + ? { commands: { skipWhenConfigEmpty: true } } + : channelId === "discord" + ? { mentions: { stripPatterns: () => ["<@!?\\d+>"] } } + : undefined, ); await loadFreshInlineActionsModuleForTest(); }); diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index 1b80294a978..26de9b96513 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -21,18 +21,31 @@ import { createChannelTestPluginBase, createTestRegistry, } from "../../test-utils/channel-plugins.js"; -import { buildSessionWriteLockModuleMock } from "../../test-utils/session-write-lock-module-mock.js"; import { drainFormattedSystemEvents } from "./session-updates.js"; import { persistSessionUsageUpdate } from "./session-usage.js"; import { initSessionState } from "./session.js"; // Perf: session-store locks are exercised elsewhere; most session tests don't need FS lock files. -vi.mock("../../agents/session-write-lock.js", (importOriginal) => - buildSessionWriteLockModuleMock( - importOriginal as () => Promise, - async () => ({ release: async () => {} }), - ), -); +vi.mock("../../agents/session-write-lock.js", async () => { + const actual = await vi.importActual( + "../../agents/session-write-lock.js", + ); + return { + ...actual, + acquireSessionWriteLock: vi.fn(async () => ({ release: async () => {} })), + resolveSessionLockMaxHoldFromTimeout: vi.fn( + ({ + timeoutMs, + graceMs = 2 * 60 * 1000, + minMs = 5 * 60 * 1000, + }: { + timeoutMs: number; + graceMs?: number; + minMs?: number; + }) => Math.max(minMs, timeoutMs + graceMs), + ), + }; +}); vi.mock("../../agents/model-catalog.js", () => ({ loadModelCatalog: vi.fn(async () => [ @@ -1447,6 +1460,7 @@ describe("initSessionState reset triggers in Slack channels", () => { } it("supports mention-prefixed Slack reset commands and preserves args", async () => { + setMinimalCurrentConversationBindingRegistryForTests(); const existingSessionId = "existing-session-123"; const sessionKey = "agent:main:slack:channel:c2"; const body = "<@U123> /new take notes"; @@ -1464,6 +1478,7 @@ describe("initSessionState reset triggers in Slack channels", () => { ctx: { Body: body, RawBody: body, + BodyForCommands: "/new take notes", CommandBody: body, From: "slack:channel:C1", To: "channel:C1", @@ -1473,6 +1488,7 @@ describe("initSessionState reset triggers in Slack channels", () => { Surface: "slack", SenderId: "U123", SenderName: "Owner", + WasMentioned: true, }, cfg, commandAuthorized: true, diff --git a/src/commands/agent.cli-provider.test.ts b/src/commands/agent.cli-provider.test.ts index f600365a1a4..b104d623465 100644 --- a/src/commands/agent.cli-provider.test.ts +++ b/src/commands/agent.cli-provider.test.ts @@ -43,14 +43,6 @@ vi.mock("../agents/workspace.js", () => ({ ensureAgentWorkspace: vi.fn(async ({ dir }: { dir: string }) => ({ dir })), })); -vi.mock("../agents/command/session-store.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - updateSessionStoreAfterAgentRun: vi.fn(async () => undefined), - }; -}); - vi.mock("../agents/skills.js", () => ({ buildWorkspaceSkillSnapshot: vi.fn(() => undefined), loadWorkspaceSkillEntries: vi.fn(() => []), diff --git a/src/commands/channels.plugin-install.test-helpers.ts b/src/commands/channels.plugin-install.test-helpers.ts index c80cbc56658..72f7aa6d522 100644 --- a/src/commands/channels.plugin-install.test-helpers.ts +++ b/src/commands/channels.plugin-install.test-helpers.ts @@ -4,7 +4,7 @@ import type { ChannelPlugin } from "../channels/plugins/types.js"; import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js"; export function createMockChannelSetupPluginInstallModule( - actual: typeof import("./channel-setup/plugin-install.js"), + actual?: Partial, ) { return { ...actual, diff --git a/src/commands/channels.remove.test.ts b/src/commands/channels.remove.test.ts index 8b22835b8ff..e157bbb48d5 100644 --- a/src/commands/channels.remove.test.ts +++ b/src/commands/channels.remove.test.ts @@ -18,16 +18,20 @@ const catalogMocks = vi.hoisted(() => ({ listChannelPluginCatalogEntries: vi.fn((): ChannelPluginCatalogEntry[] => []), })); -vi.mock("../channels/plugins/catalog.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../channels/plugins/catalog.js", async () => { + const actual = await vi.importActual( + "../channels/plugins/catalog.js", + ); return { ...actual, listChannelPluginCatalogEntries: catalogMocks.listChannelPluginCatalogEntries, }; }); -vi.mock("./channel-setup/plugin-install.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("./channel-setup/plugin-install.js", async () => { + const actual = await vi.importActual( + "./channel-setup/plugin-install.js", + ); const { createMockChannelSetupPluginInstallModule } = await import("./channels.plugin-install.test-helpers.js"); return createMockChannelSetupPluginInstallModule(actual); diff --git a/src/commands/status.summary.test.ts b/src/commands/status.summary.test.ts index 65f6d6fa3c9..353a6175e2d 100644 --- a/src/commands/status.summary.test.ts +++ b/src/commands/status.summary.test.ts @@ -1,7 +1,12 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("../channels/config-presence.js", () => ({ +const statusSummaryMocks = vi.hoisted(() => ({ hasPotentialConfiguredChannels: vi.fn(() => true), + buildChannelSummary: vi.fn(async () => ["ok"]), +})); + +vi.mock("../channels/config-presence.js", () => ({ + hasPotentialConfiguredChannels: statusSummaryMocks.hasPotentialConfiguredChannels, })); vi.mock("./status.summary.runtime.js", () => ({ @@ -29,17 +34,6 @@ vi.mock("../config/io.js", () => ({ loadConfig: vi.fn(() => ({})), })); -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadSessionStore: vi.fn(() => ({})), - resolveFreshSessionTotalTokens: vi.fn(() => undefined), - resolveMainSessionKey: vi.fn(() => "main"), - resolveStorePath: vi.fn(() => "/tmp/sessions.json"), - }; -}); - vi.mock("../gateway/agent-list.js", () => ({ listGatewayAgentsBasic: vi.fn(() => ({ defaultId: "main", @@ -48,7 +42,7 @@ vi.mock("../gateway/agent-list.js", () => ({ })); vi.mock("../infra/channel-summary.js", () => ({ - buildChannelSummary: vi.fn(async () => ["ok"]), + buildChannelSummary: statusSummaryMocks.buildChannelSummary, })); vi.mock("../infra/heartbeat-summary.js", () => ({ @@ -106,15 +100,18 @@ vi.mock("../routing/session-key.js", () => ({ parseAgentSessionKey: vi.fn(() => null), })); -vi.mock("../version.js", () => ({ - resolveRuntimeServiceVersion: vi.fn(() => "2026.3.8"), -})); +vi.mock("../version.js", async () => { + const actual = await vi.importActual("../version.js"); + return { + ...actual, + resolveRuntimeServiceVersion: vi.fn(() => "2026.3.8"), + }; +}); vi.mock("./status.link-channel.js", () => ({ resolveLinkChannelContext: vi.fn(async () => undefined), })); -const { hasPotentialConfiguredChannels } = await import("../channels/config-presence.js"); const { buildChannelSummary } = await import("../infra/channel-summary.js"); const { resolveLinkChannelContext } = await import("./status.link-channel.js"); let getStatusSummary: typeof import("./status.summary.js").getStatusSummary; @@ -128,6 +125,8 @@ describe("getStatusSummary", () => { beforeEach(() => { vi.clearAllMocks(); + statusSummaryMocks.hasPotentialConfiguredChannels.mockReturnValue(true); + statusSummaryMocks.buildChannelSummary.mockResolvedValue(["ok"]); }); it("includes runtimeVersion in the status payload", async () => { @@ -141,7 +140,7 @@ describe("getStatusSummary", () => { }); it("skips channel summary imports when no channels are configured", async () => { - vi.mocked(hasPotentialConfiguredChannels).mockReturnValue(false); + statusSummaryMocks.hasPotentialConfiguredChannels.mockReturnValue(false); const summary = await getStatusSummary(); diff --git a/src/gateway/gateway-connection.test-mocks.ts b/src/gateway/gateway-connection.test-mocks.ts index cb8f34058a0..8cdac4e0031 100644 --- a/src/gateway/gateway-connection.test-mocks.ts +++ b/src/gateway/gateway-connection.test-mocks.ts @@ -1,35 +1,39 @@ -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; -type TestMock = ReturnType; +type TestMock = Mock< + (...args: TArgs) => TResult +>; export const loadConfigMock: TestMock = vi.fn(); export const resolveGatewayPortMock: TestMock = vi.fn(); -export const resolveStateDirMock: TestMock = vi.fn( +export const resolveStateDirMock: TestMock<[NodeJS.ProcessEnv], string> = vi.fn( (env: NodeJS.ProcessEnv) => env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw", ); -export const resolveConfigPathMock: TestMock = vi.fn( +export const resolveConfigPathMock: TestMock<[NodeJS.ProcessEnv, string], string> = vi.fn( (env: NodeJS.ProcessEnv, stateDir: string) => env.OPENCLAW_CONFIG_PATH ?? `${stateDir}/openclaw.json`, ); export const pickPrimaryTailnetIPv4Mock: TestMock = vi.fn(); export const pickPrimaryLanIPv4Mock: TestMock = vi.fn(); - -vi.mock("../config/config.js", async (importOriginal) => ({ - ...(await importOriginal()), - loadConfig: loadConfigMock, - resolveGatewayPort: resolveGatewayPortMock, - resolveStateDir: resolveStateDirMock, - resolveConfigPath: resolveConfigPathMock, -})); +export const isLoopbackHostMock: TestMock<[string], boolean> = vi.fn((host: string) => + /^(localhost|127(?:\.\d{1,3}){3}|::1|\[::1\]|::ffff:127(?:\.\d{1,3}){3})$/i.test( + host.trim().replace(/\.+$/, ""), + ), +); +export const isSecureWebSocketUrlMock: TestMock< + [string, { allowPrivateWs?: boolean } | undefined], + boolean +> = vi.fn((url: string, opts?: { allowPrivateWs?: boolean }) => { + const parsed = new URL(url); + if (parsed.protocol === "wss:") { + return true; + } + if (parsed.protocol !== "ws:") { + return false; + } + return opts?.allowPrivateWs === true || isLoopbackHostMock(parsed.hostname); +}); vi.mock("../infra/tailnet.js", () => ({ pickPrimaryTailnetIPv4: pickPrimaryTailnetIPv4Mock, })); - -vi.mock("./net.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - pickPrimaryLanIPv4: pickPrimaryLanIPv4Mock, - }; -}); diff --git a/src/plugin-sdk/anthropic-vertex-auth-presence.preflight.test.ts b/src/plugin-sdk/anthropic-vertex-auth-presence.preflight.test.ts index cc79c2b712e..d08b083fd1d 100644 --- a/src/plugin-sdk/anthropic-vertex-auth-presence.preflight.test.ts +++ b/src/plugin-sdk/anthropic-vertex-auth-presence.preflight.test.ts @@ -5,8 +5,8 @@ const { existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({ readFileSyncMock: vi.fn(), })); -vi.mock("node:fs", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("node:fs", async () => { + const actual = await vi.importActual("node:fs"); existsSyncMock.mockImplementation((pathname) => actual.existsSync(pathname)); readFileSyncMock.mockImplementation((pathname, options) => String(pathname) === "/tmp/vertex-adc.json" diff --git a/src/tui/gateway-chat.test.ts b/src/tui/gateway-chat.test.ts index 91c11c6a056..101b3007aaf 100644 --- a/src/tui/gateway-chat.test.ts +++ b/src/tui/gateway-chat.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { loadConfigMock as loadConfig, resolveConfigPathMock as resolveConfigPath, @@ -10,6 +10,25 @@ import { } from "../gateway/gateway-connection.test-mocks.js"; import { captureEnv, withEnvAsync } from "../test-utils/env.js"; +vi.mock("../config/config.js", async () => { + const mocks = await import("../gateway/gateway-connection.test-mocks.js"); + return { + loadConfig: mocks.loadConfigMock, + resolveConfigPath: mocks.resolveConfigPathMock, + resolveGatewayPort: mocks.resolveGatewayPortMock, + resolveStateDir: mocks.resolveStateDirMock, + }; +}); + +vi.mock("../gateway/net.js", async () => { + const mocks = await import("../gateway/gateway-connection.test-mocks.js"); + return { + isLoopbackHost: mocks.isLoopbackHostMock, + isSecureWebSocketUrl: mocks.isSecureWebSocketUrlMock, + pickPrimaryLanIPv4: mocks.pickPrimaryLanIPv4Mock, + }; +}); + const { GatewayChatClient, resolveGatewayConnection } = await import("./gateway-chat.js"); async function fileExists(filePath: string): Promise { diff --git a/test/helpers/node-builtin-mocks.ts b/test/helpers/node-builtin-mocks.ts index a35d3118236..661b50a1929 100644 --- a/test/helpers/node-builtin-mocks.ts +++ b/test/helpers/node-builtin-mocks.ts @@ -18,11 +18,11 @@ function resolveDefaultBase(actual: TModule): Record( - importOriginal: () => Promise, + loadActual: () => Promise, factory: MockFactory, options?: { mirrorToDefault?: boolean }, ): Promise { - const actual = await importOriginal(); + const actual = await loadActual(); const overrides = resolveMockOverrides(actual, factory); const mocked = { ...actual, diff --git a/test/helpers/plugins/provider-auth-contract.ts b/test/helpers/plugins/provider-auth-contract.ts index e5687cc1e25..2aeee2164a4 100644 --- a/test/helpers/plugins/provider-auth-contract.ts +++ b/test/helpers/plugins/provider-auth-contract.ts @@ -33,8 +33,10 @@ const providerAuthContractModules = vi.hoisted(() => ({ openAIIndexModuleUrl: new URL("../../../extensions/openai/index.ts", import.meta.url).href, })); -vi.mock("openclaw/plugin-sdk/provider-auth-login", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/provider-auth-login", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/provider-auth-login", + ); return { ...actual, loginOpenAICodexOAuth: loginOpenAICodexOAuthMock, @@ -42,8 +44,10 @@ vi.mock("openclaw/plugin-sdk/provider-auth-login", async (importOriginal) => { }; }); -vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/provider-auth", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/provider-auth", + ); return { ...actual, ensureAuthProfileStore: ensureAuthProfileStoreMock, diff --git a/test/helpers/plugins/tts-contract-suites.ts b/test/helpers/plugins/tts-contract-suites.ts index 554367e485b..c61115fa5bd 100644 --- a/test/helpers/plugins/tts-contract-suites.ts +++ b/test/helpers/plugins/tts-contract-suites.ts @@ -15,8 +15,9 @@ let resolveModelAsyncMock: typeof import("../../../src/agents/pi-embedded-runner let ensureCustomApiRegisteredMock: typeof import("../../../src/agents/custom-api-registry.js").ensureCustomApiRegistered; let prepareModelForSimpleCompletionMock: typeof import("../../../src/agents/simple-completion-transport.js").prepareModelForSimpleCompletion; -vi.mock("@mariozechner/pi-ai", async (importOriginal) => { - const original = await importOriginal(); +vi.mock("@mariozechner/pi-ai", async () => { + const original = + await vi.importActual("@mariozechner/pi-ai"); return { ...original, completeSimple: vi.fn(), diff --git a/test/setup.shared.ts b/test/setup.shared.ts index 71a533cee72..ea72eca14da 100644 --- a/test/setup.shared.ts +++ b/test/setup.shared.ts @@ -1,7 +1,8 @@ import { vi } from "vitest"; -vi.mock("@mariozechner/pi-ai", async (importOriginal) => { - const original = await importOriginal(); +vi.mock("@mariozechner/pi-ai", async () => { + const original = + await vi.importActual("@mariozechner/pi-ai"); return { ...original, getOAuthApiKey: () => undefined,