diff --git a/extensions/bluebubbles/src/monitor.webhook.test-helpers.ts b/extensions/bluebubbles/src/monitor.webhook.test-helpers.ts index 3fd7dae3bc1..6f56fdbfff2 100644 --- a/extensions/bluebubbles/src/monitor.webhook.test-helpers.ts +++ b/extensions/bluebubbles/src/monitor.webhook.test-helpers.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { IncomingMessage, ServerResponse } from "node:http"; -import { expect, vi } from "vitest"; +import { expect, vi, type Mock } from "vitest"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; import { handleBlueBubblesWebhookRequest } from "./monitor.js"; import { registerBlueBubblesWebhookTarget } from "./monitor.js"; @@ -16,6 +16,11 @@ export type WebhookRequestParams = { }; export const LOOPBACK_REMOTE_ADDRESSES_FOR_TEST = ["127.0.0.1", "::1", "::ffff:127.0.0.1"] as const; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type HangingWebhookRequestForTest = { + req: IncomingMessage; + destroyMock: UnknownMock; +}; export function createMockAccount( overrides: Partial = {}, @@ -182,7 +187,7 @@ export function createLoopbackWebhookRequestParamsForTest( export function createHangingWebhookRequestForTest( url = "/bluebubbles-webhook?password=test-password", remoteAddress = "127.0.0.1", -) { +): HangingWebhookRequestForTest { const req = new EventEmitter() as IncomingMessage; const destroyMock = vi.fn(); req.method = "POST"; diff --git a/extensions/browser/src/cli/browser-cli-manage.test-helpers.ts b/extensions/browser/src/cli/browser-cli-manage.test-helpers.ts index 86de7010e7d..5cad4190cb1 100644 --- a/extensions/browser/src/cli/browser-cli-manage.test-helpers.ts +++ b/extensions/browser/src/cli/browser-cli-manage.test-helpers.ts @@ -1,3 +1,4 @@ +import type { Command } from "commander"; import { vi } from "vitest"; import * as parentCoreApiModule from "../core-api.js"; import * as browserCliSharedModule from "./browser-cli-shared.js"; @@ -56,7 +57,7 @@ vi.spyOn(cliCoreApiModule.defaultRuntime, "exit").mockImplementation(browserCliR const { registerBrowserManageCommands } = await import("./browser-cli-manage.js"); -export function createBrowserManageProgram(params?: { withParentTimeout?: boolean }) { +export function createBrowserManageProgram(params?: { withParentTimeout?: boolean }): Command { const { program, browser, parentOpts } = createBrowserProgram(); if (params?.withParentTimeout) { browser.option("--timeout ", "Timeout in ms", "30000"); diff --git a/extensions/discord/src/outbound-adapter.test-harness.ts b/extensions/discord/src/outbound-adapter.test-harness.ts index fcc04f5040c..623dae0058c 100644 --- a/extensions/discord/src/outbound-adapter.test-harness.ts +++ b/extensions/discord/src/outbound-adapter.test-harness.ts @@ -1,6 +1,28 @@ -import { expect, vi } from "vitest"; +import { expect, vi, type Mock } from "vitest"; -export function createDiscordOutboundHoisted() { +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; + +type DiscordOutboundHoisted = { + sendMessageDiscordMock: AsyncUnknownMock; + sendDiscordComponentMessageMock: AsyncUnknownMock; + sendPollDiscordMock: AsyncUnknownMock; + sendWebhookMessageDiscordMock: AsyncUnknownMock; + getThreadBindingManagerMock: UnknownMock; +}; + +type DiscordSendModule = typeof import("./send.js"); +type DiscordSendComponentsModule = typeof import("./send.components.js"); +type DiscordThreadBindingsModule = typeof import("./monitor/thread-bindings.js"); + +function invokeMock( + mock: (...args: unknown[]) => unknown, + ...args: TArgs +): TResult { + return mock(...args) as TResult; +} + +export function createDiscordOutboundHoisted(): DiscordOutboundHoisted { const sendMessageDiscordMock = vi.fn(); const sendDiscordComponentMessageMock = vi.fn(); const sendPollDiscordMock = vi.fn(); @@ -21,28 +43,94 @@ export const DEFAULT_DISCORD_SEND_RESULT = { channelId: "ch-1", } as const; -type DiscordOutboundHoisted = ReturnType; +export async function createDiscordSendModuleMock( + hoisted: DiscordOutboundHoisted, + importOriginal: () => Promise, +): Promise { + const actual = await importOriginal(); + return { + ...actual, + sendMessageDiscord: (...args: Parameters) => + invokeMock< + Parameters, + ReturnType + >(hoisted.sendMessageDiscordMock, ...args), + sendPollDiscord: (...args: Parameters) => + invokeMock< + Parameters, + ReturnType + >(hoisted.sendPollDiscordMock, ...args), + sendWebhookMessageDiscord: ( + ...args: Parameters + ) => + invokeMock< + Parameters, + ReturnType + >(hoisted.sendWebhookMessageDiscordMock, ...args), + }; +} + +export async function createDiscordSendComponentsModuleMock( + hoisted: DiscordOutboundHoisted, + importOriginal: () => Promise, +): Promise { + const actual = await importOriginal(); + return { + ...actual, + sendDiscordComponentMessage: ( + ...args: Parameters + ) => + invokeMock< + Parameters, + ReturnType + >(hoisted.sendDiscordComponentMessageMock, ...args), + }; +} + +export async function createDiscordThreadBindingsModuleMock( + hoisted: DiscordOutboundHoisted, + importOriginal: () => Promise, +): Promise { + const actual = await importOriginal(); + return { + ...actual, + getThreadBindingManager: ( + ...args: Parameters + ) => + invokeMock< + Parameters, + ReturnType + >(hoisted.getThreadBindingManagerMock, ...args), + }; +} export async function installDiscordOutboundModuleSpies(hoisted: DiscordOutboundHoisted) { const sendModule = await import("./send.js"); - vi.spyOn(sendModule, "sendMessageDiscord").mockImplementation((...args: unknown[]) => - hoisted.sendMessageDiscordMock(...args), + const mockedSendModule = await createDiscordSendModuleMock(hoisted, async () => sendModule); + vi.spyOn(sendModule, "sendMessageDiscord").mockImplementation( + mockedSendModule.sendMessageDiscord, ); - vi.spyOn(sendModule, "sendPollDiscord").mockImplementation((...args: unknown[]) => - hoisted.sendPollDiscordMock(...args), - ); - vi.spyOn(sendModule, "sendWebhookMessageDiscord").mockImplementation((...args: unknown[]) => - hoisted.sendWebhookMessageDiscordMock(...args), + vi.spyOn(sendModule, "sendPollDiscord").mockImplementation(mockedSendModule.sendPollDiscord); + vi.spyOn(sendModule, "sendWebhookMessageDiscord").mockImplementation( + mockedSendModule.sendWebhookMessageDiscord, ); const sendComponentsModule = await import("./send.components.js"); + const mockedSendComponentsModule = await createDiscordSendComponentsModuleMock( + hoisted, + async () => sendComponentsModule, + ); vi.spyOn(sendComponentsModule, "sendDiscordComponentMessage").mockImplementation( - (...args: unknown[]) => hoisted.sendDiscordComponentMessageMock(...args), + mockedSendComponentsModule.sendDiscordComponentMessage, ); const threadBindingsModule = await import("./monitor/thread-bindings.js"); + const mockedThreadBindingsModule = await createDiscordThreadBindingsModuleMock( + hoisted, + async () => threadBindingsModule, + ); vi.spyOn(threadBindingsModule, "getThreadBindingManager").mockImplementation( - (...args: unknown[]) => hoisted.getThreadBindingManagerMock(...args), + mockedThreadBindingsModule.getThreadBindingManager, ); } diff --git a/extensions/discord/src/test-support/component-runtime.ts b/extensions/discord/src/test-support/component-runtime.ts index e6fce6da4d6..fb45b1ea061 100644 --- a/extensions/discord/src/test-support/component-runtime.ts +++ b/extensions/discord/src/test-support/component-runtime.ts @@ -1,32 +1,51 @@ -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; import { parsePluginBindingApprovalCustomId } from "../../../../src/plugins/conversation-binding.js"; import { resolvePinnedMainDmOwnerFromAllowlist } from "../../../../src/security/dm-policy-shared.js"; -const runtimeMocks = vi.hoisted(() => ({ - buildPluginBindingResolvedTextMock: vi.fn(), - dispatchPluginInteractiveHandlerMock: vi.fn(), - dispatchReplyMock: vi.fn(), - enqueueSystemEventMock: vi.fn(), - readAllowFromStoreMock: vi.fn(), - readSessionUpdatedAtMock: vi.fn(), - recordInboundSessionMock: vi.fn(), - resolveStorePathMock: vi.fn(), - resolvePluginConversationBindingApprovalMock: vi.fn(), - upsertPairingRequestMock: vi.fn(), -})); +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; -export const readAllowFromStoreMock = runtimeMocks.readAllowFromStoreMock; -export const dispatchPluginInteractiveHandlerMock = +type DiscordComponentRuntimeMocks = { + buildPluginBindingResolvedTextMock: UnknownMock; + dispatchPluginInteractiveHandlerMock: AsyncUnknownMock; + dispatchReplyMock: UnknownMock; + enqueueSystemEventMock: UnknownMock; + readAllowFromStoreMock: AsyncUnknownMock; + readSessionUpdatedAtMock: UnknownMock; + recordInboundSessionMock: AsyncUnknownMock; + resolveStorePathMock: UnknownMock; + resolvePluginConversationBindingApprovalMock: AsyncUnknownMock; + upsertPairingRequestMock: AsyncUnknownMock; +}; + +const runtimeMocks = vi.hoisted( + (): DiscordComponentRuntimeMocks => ({ + buildPluginBindingResolvedTextMock: vi.fn(), + dispatchPluginInteractiveHandlerMock: vi.fn(), + dispatchReplyMock: vi.fn(), + enqueueSystemEventMock: vi.fn(), + readAllowFromStoreMock: vi.fn(), + readSessionUpdatedAtMock: vi.fn(), + recordInboundSessionMock: vi.fn(), + resolveStorePathMock: vi.fn(), + resolvePluginConversationBindingApprovalMock: vi.fn(), + upsertPairingRequestMock: vi.fn(), + }), +); + +export const readAllowFromStoreMock: AsyncUnknownMock = runtimeMocks.readAllowFromStoreMock; +export const dispatchPluginInteractiveHandlerMock: AsyncUnknownMock = runtimeMocks.dispatchPluginInteractiveHandlerMock; -export const dispatchReplyMock = runtimeMocks.dispatchReplyMock; -export const enqueueSystemEventMock = runtimeMocks.enqueueSystemEventMock; -export const upsertPairingRequestMock = runtimeMocks.upsertPairingRequestMock; -export const recordInboundSessionMock = runtimeMocks.recordInboundSessionMock; -export const readSessionUpdatedAtMock = runtimeMocks.readSessionUpdatedAtMock; -export const resolveStorePathMock = runtimeMocks.resolveStorePathMock; -export const resolvePluginConversationBindingApprovalMock = +export const dispatchReplyMock: UnknownMock = runtimeMocks.dispatchReplyMock; +export const enqueueSystemEventMock: UnknownMock = runtimeMocks.enqueueSystemEventMock; +export const upsertPairingRequestMock: AsyncUnknownMock = runtimeMocks.upsertPairingRequestMock; +export const recordInboundSessionMock: AsyncUnknownMock = runtimeMocks.recordInboundSessionMock; +export const readSessionUpdatedAtMock: UnknownMock = runtimeMocks.readSessionUpdatedAtMock; +export const resolveStorePathMock: UnknownMock = runtimeMocks.resolveStorePathMock; +export const resolvePluginConversationBindingApprovalMock: AsyncUnknownMock = runtimeMocks.resolvePluginConversationBindingApprovalMock; -export const buildPluginBindingResolvedTextMock = runtimeMocks.buildPluginBindingResolvedTextMock; +export const buildPluginBindingResolvedTextMock: UnknownMock = + runtimeMocks.buildPluginBindingResolvedTextMock; async function readStoreAllowFromForDmPolicy(params: { provider: string; diff --git a/extensions/feishu/src/lifecycle.test-support.ts b/extensions/feishu/src/lifecycle.test-support.ts index f2433d653df..52fd6fac459 100644 --- a/extensions/feishu/src/lifecycle.test-support.ts +++ b/extensions/feishu/src/lifecycle.test-support.ts @@ -1,31 +1,77 @@ -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; type BoundConversation = { bindingId: string; targetSessionKey: string; }; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; +type FinalizeInboundContextMock = Mock< + (ctx: Record, opts?: unknown) => Record +>; +type DispatchReplyCounts = { + final: number; + block?: number; + tool?: number; +}; +type DispatchReplyContext = Record & { + SessionKey?: string; +}; +type DispatchReplyDispatcher = { + sendFinalReply: (payload: { text: string }) => unknown | Promise; +}; +type DispatchReplyFromConfigMock = Mock< + (params: { + ctx: DispatchReplyContext; + dispatcher: DispatchReplyDispatcher; + }) => Promise<{ queuedFinal: boolean; counts: DispatchReplyCounts }> +>; +type WithReplyDispatcherMock = Mock< + (params: { run: () => unknown | Promise }) => Promise +>; +type FeishuLifecycleTestMocks = { + createEventDispatcherMock: UnknownMock; + monitorWebSocketMock: AsyncUnknownMock; + monitorWebhookMock: AsyncUnknownMock; + createFeishuThreadBindingManagerMock: UnknownMock; + createFeishuReplyDispatcherMock: UnknownMock; + resolveBoundConversationMock: Mock<() => BoundConversation | null>; + touchBindingMock: UnknownMock; + resolveAgentRouteMock: UnknownMock; + resolveConfiguredBindingRouteMock: UnknownMock; + ensureConfiguredBindingRouteReadyMock: UnknownMock; + dispatchReplyFromConfigMock: DispatchReplyFromConfigMock; + withReplyDispatcherMock: WithReplyDispatcherMock; + finalizeInboundContextMock: FinalizeInboundContextMock; + getMessageFeishuMock: AsyncUnknownMock; + listFeishuThreadMessagesMock: AsyncUnknownMock; + sendMessageFeishuMock: AsyncUnknownMock; + sendCardFeishuMock: AsyncUnknownMock; +}; -const feishuLifecycleTestMocks = vi.hoisted(() => ({ - createEventDispatcherMock: vi.fn(), - monitorWebSocketMock: vi.fn(async () => {}), - monitorWebhookMock: vi.fn(async () => {}), - createFeishuThreadBindingManagerMock: vi.fn(() => ({ stop: vi.fn() })), - createFeishuReplyDispatcherMock: vi.fn(), - resolveBoundConversationMock: vi.fn<() => BoundConversation | null>(() => null), - touchBindingMock: vi.fn(), - resolveAgentRouteMock: vi.fn(), - resolveConfiguredBindingRouteMock: vi.fn(), - ensureConfiguredBindingRouteReadyMock: vi.fn(), - dispatchReplyFromConfigMock: vi.fn(), - withReplyDispatcherMock: vi.fn(), - finalizeInboundContextMock: vi.fn((ctx) => ctx), - getMessageFeishuMock: vi.fn(async () => null), - listFeishuThreadMessagesMock: vi.fn(async () => []), - sendMessageFeishuMock: vi.fn(async () => ({ messageId: "om_sent", chatId: "chat_default" })), - sendCardFeishuMock: vi.fn(async () => ({ messageId: "om_card", chatId: "chat_default" })), -})); +const feishuLifecycleTestMocks = vi.hoisted( + (): FeishuLifecycleTestMocks => ({ + createEventDispatcherMock: vi.fn(), + monitorWebSocketMock: vi.fn(async () => {}), + monitorWebhookMock: vi.fn(async () => {}), + createFeishuThreadBindingManagerMock: vi.fn(() => ({ stop: vi.fn() })), + createFeishuReplyDispatcherMock: vi.fn(), + resolveBoundConversationMock: vi.fn<() => BoundConversation | null>(() => null), + touchBindingMock: vi.fn(), + resolveAgentRouteMock: vi.fn(), + resolveConfiguredBindingRouteMock: vi.fn(), + ensureConfiguredBindingRouteReadyMock: vi.fn(), + dispatchReplyFromConfigMock: vi.fn(), + withReplyDispatcherMock: vi.fn(), + finalizeInboundContextMock: vi.fn((ctx) => ctx), + getMessageFeishuMock: vi.fn(async () => null), + listFeishuThreadMessagesMock: vi.fn(async () => []), + sendMessageFeishuMock: vi.fn(async () => ({ messageId: "om_sent", chatId: "chat_default" })), + sendCardFeishuMock: vi.fn(async () => ({ messageId: "om_card", chatId: "chat_default" })), + }), +); -export function getFeishuLifecycleTestMocks() { +export function getFeishuLifecycleTestMocks(): FeishuLifecycleTestMocks { return feishuLifecycleTestMocks; } diff --git a/extensions/feishu/src/test-support/lifecycle-test-support.ts b/extensions/feishu/src/test-support/lifecycle-test-support.ts index cdfbf0a5271..2693df30cbc 100644 --- a/extensions/feishu/src/test-support/lifecycle-test-support.ts +++ b/extensions/feishu/src/test-support/lifecycle-test-support.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { expect, vi } from "vitest"; +import { expect, vi, type Mock } from "vitest"; import { createPluginRuntimeMock } from "../../../../test/helpers/plugins/plugin-runtime-mock.js"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../../runtime-api.js"; import { setFeishuRuntime } from "../runtime.js"; @@ -9,6 +9,37 @@ type InboundDebouncerParams = { onFlush?: (items: T[]) => Promise; onError?: (err: unknown, items: T[]) => void; }; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; +type FeishuDispatchReplyCounts = { + final: number; + block?: number; + tool?: number; +}; +type FeishuDispatchReplyContext = Record & { + SessionKey?: string; +}; +type FeishuDispatchReplyDispatcher = { + sendFinalReply: (payload: { text: string }) => unknown | Promise; +}; +type FeishuDispatchReplyMock = Mock< + (args: { + ctx: FeishuDispatchReplyContext; + dispatcher: FeishuDispatchReplyDispatcher; + }) => Promise<{ queuedFinal: boolean; counts: FeishuDispatchReplyCounts }> +>; +type FeishuLifecycleReplyDispatcher = { + dispatcher: { + sendToolResult: UnknownMock; + sendBlockReply: UnknownMock; + sendFinalReply: AsyncUnknownMock; + waitForIdle: AsyncUnknownMock; + getQueuedCounts: UnknownMock; + markComplete: UnknownMock; + }; + replyOptions: Record; + markDispatchIdle: UnknownMock; +}; export function setFeishuLifecycleStateDir(prefix: string) { process.env.OPENCLAW_STATE_DIR = `/tmp/${prefix}-${randomUUID()}`; @@ -28,7 +59,7 @@ export const FEISHU_PREFETCHED_BOT_OPEN_ID_SOURCE = { botName: "Bot", } as const; -export function createFeishuLifecycleReplyDispatcher() { +export function createFeishuLifecycleReplyDispatcher(): FeishuLifecycleReplyDispatcher { return { dispatcher: { sendToolResult: vi.fn(() => false), @@ -134,16 +165,7 @@ export function installFeishuLifecycleReplyRuntime(params: { } export function mockFeishuReplyOnceDispatch(params: { - dispatchReplyFromConfigMock: { - mockImplementation: ( - fn: (args: { - ctx?: unknown; - dispatcher?: { - sendFinalReply?: (payload: { text: string }) => Promise; - }; - }) => Promise, - ) => void; - }; + dispatchReplyFromConfigMock: FeishuDispatchReplyMock; replyText: string; shouldSendFinalReply?: (ctx: unknown) => boolean; }) { 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 1172a605368..360088b78b6 100644 --- a/extensions/telegram/src/bot-message-context.route-test-support.ts +++ b/extensions/telegram/src/bot-message-context.route-test-support.ts @@ -1,10 +1,12 @@ -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; -const hoisted = vi.hoisted(() => ({ +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; + +const hoisted = vi.hoisted((): { recordInboundSessionMock: AsyncUnknownMock } => ({ recordInboundSessionMock: vi.fn().mockResolvedValue(undefined), })); -export const recordInboundSessionMock = hoisted.recordInboundSessionMock; +export const recordInboundSessionMock: AsyncUnknownMock = hoisted.recordInboundSessionMock; vi.mock("./bot-message-context.session.runtime.js", async (importOriginal) => { const actual = await importOriginal(); diff --git a/extensions/telegram/src/bot-native-commands.menu-test-support.ts b/extensions/telegram/src/bot-native-commands.menu-test-support.ts index c60ed16379a..83e7cbebd5e 100644 --- a/extensions/telegram/src/bot-native-commands.menu-test-support.ts +++ b/extensions/telegram/src/bot-native-commands.menu-test-support.ts @@ -1,5 +1,5 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; -import { expect, vi } from "vitest"; +import { expect, vi, type Mock } from "vitest"; import type { OpenClawConfig } from "../runtime-api.js"; import type { TelegramNativeCommandDeps } from "./bot-native-command-deps.runtime.js"; import { @@ -12,6 +12,7 @@ type RegisteredCommand = { command: string; description: string; }; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; type CreateCommandBotResult = { bot: RegisterTelegramNativeCommandsParams["bot"]; @@ -36,7 +37,7 @@ const deliveryMocks = vi.hoisted(() => ({ export const listSkillCommandsForAgents = skillCommandMocks.listSkillCommandsForAgents; export const deliverReplies = deliveryMocks.deliverReplies; export const editMessageTelegram = deliveryMocks.editMessageTelegram; -export const emitTelegramMessageSentHooks = deliveryMocks.emitTelegramMessageSentHooks; +export const emitTelegramMessageSentHooks: UnknownMock = deliveryMocks.emitTelegramMessageSentHooks; vi.mock("./bot/delivery.js", () => ({ deliverReplies, diff --git a/extensions/telegram/src/bot.ts b/extensions/telegram/src/bot.ts index 3f0dedf0cdb..dff656c19c6 100644 --- a/extensions/telegram/src/bot.ts +++ b/extensions/telegram/src/bot.ts @@ -73,6 +73,7 @@ type TelegramBotRuntime = { sequentialize: typeof sequentialize; apiThrottler: typeof apiThrottler; }; +type TelegramBotInstance = InstanceType; const DEFAULT_TELEGRAM_BOT_RUNTIME: TelegramBotRuntime = { Bot, @@ -134,7 +135,7 @@ function extractTelegramApiMethod(input: TelegramFetchInput): string | null { } } -export function createTelegramBot(opts: TelegramBotOptions) { +export function createTelegramBot(opts: TelegramBotOptions): TelegramBotInstance { const botRuntime = telegramBotRuntimeForTest ?? DEFAULT_TELEGRAM_BOT_RUNTIME; const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime(); const telegramDeps = opts.telegramDeps ?? defaultTelegramBotDeps; diff --git a/extensions/telegram/src/target-writeback.test-shared.ts b/extensions/telegram/src/target-writeback.test-shared.ts index ceb91dadacb..ba21a7a77e6 100644 --- a/extensions/telegram/src/target-writeback.test-shared.ts +++ b/extensions/telegram/src/target-writeback.test-shared.ts @@ -1,11 +1,14 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/testing"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi, type Mock } from "vitest"; -export const readConfigFileSnapshotForWrite = vi.fn(); -export const writeConfigFile = vi.fn(); -export const loadCronStore = vi.fn(); -export const resolveCronStorePath = vi.fn(); -export const saveCronStore = vi.fn(); +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; + +export const readConfigFileSnapshotForWrite: AsyncUnknownMock = vi.fn(); +export const writeConfigFile: AsyncUnknownMock = vi.fn(); +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(); diff --git a/extensions/whatsapp/src/auto-reply.test-harness.ts b/extensions/whatsapp/src/auto-reply.test-harness.ts index 4f864b1f0cb..b338b4180ce 100644 --- a/extensions/whatsapp/src/auto-reply.test-harness.ts +++ b/extensions/whatsapp/src/auto-reply.test-harness.ts @@ -4,7 +4,7 @@ import os from "node:os"; import path from "node:path"; import { resetInboundDedupe } from "openclaw/plugin-sdk/reply-runtime"; import { resetLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env"; -import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, vi, type Mock } from "vitest"; import type { WebInboundMessage, WebListenerCloseReason } from "./inbound.js"; import { resetBaileysMocks as _resetBaileysMocks, @@ -25,6 +25,18 @@ type MockWebListener = { sendReaction: () => Promise; sendComposingTo: () => Promise; }; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; +type WebAutoReplyRuntime = { + log: UnknownMock; + error: UnknownMock; + exit: UnknownMock; +}; +type WebAutoReplyMonitorHarness = { + runtime: WebAutoReplyRuntime; + controller: AbortController; + run: Promise; +}; export const TEST_NET_IP = "203.0.113.10"; @@ -228,7 +240,7 @@ export function createWebInboundDeliverySpies(): AnyExport { }; } -export function createWebAutoReplyRuntime() { +export function createWebAutoReplyRuntime(): WebAutoReplyRuntime { return { log: vi.fn(), error: vi.fn(), @@ -239,13 +251,13 @@ export function createWebAutoReplyRuntime() { export function startWebAutoReplyMonitor(params: { monitorWebChannelFn: (...args: unknown[]) => Promise; listenerFactory: unknown; - sleep: ReturnType; + sleep: UnknownMock | AsyncUnknownMock; signal?: AbortSignal; heartbeatSeconds?: number; messageTimeoutMs?: number; watchdogCheckMs?: number; reconnect?: { initialMs: number; maxMs: number; maxAttempts: number; factor: number }; -}) { +}): WebAutoReplyMonitorHarness { const runtime = createWebAutoReplyRuntime(); const controller = new AbortController(); const run = params.monitorWebChannelFn( diff --git a/extensions/whatsapp/src/monitor-inbox.test-harness.ts b/extensions/whatsapp/src/monitor-inbox.test-harness.ts index b54a05a5763..267abe50ec4 100644 --- a/extensions/whatsapp/src/monitor-inbox.test-harness.ts +++ b/extensions/whatsapp/src/monitor-inbox.test-harness.ts @@ -29,7 +29,7 @@ export const DEFAULT_WEB_INBOX_CONFIG = { responsePrefix: undefined, }, } as const; -export const mockLoadConfig = loadConfigMock; +export const mockLoadConfig: typeof loadConfigMock = loadConfigMock; export const readAllowFromStoreMock = pairingReadAllowFromStoreMock; export const upsertPairingRequestMock = pairingUpsertPairingRequestMock; diff --git a/extensions/whatsapp/src/pairing-security.test-harness.ts b/extensions/whatsapp/src/pairing-security.test-harness.ts index a45ad858237..fb9af991cc7 100644 --- a/extensions/whatsapp/src/pairing-security.test-harness.ts +++ b/extensions/whatsapp/src/pairing-security.test-harness.ts @@ -3,7 +3,7 @@ import { resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce, } from "openclaw/plugin-sdk/runtime-group-policy"; -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; export type AsyncMock = { (...args: TArgs): Promise; @@ -11,8 +11,9 @@ export type AsyncMock = mockResolvedValue: (value: TResult) => AsyncMock; mockResolvedValueOnce: (value: TResult) => AsyncMock; }; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; -export const loadConfigMock = vi.fn(); +export const loadConfigMock: UnknownMock = vi.fn(); export const readAllowFromStoreMock = vi.fn() as AsyncMock; export const upsertPairingRequestMock = vi.fn() as AsyncMock; diff --git a/extensions/zalo/test-support/monitor-mocks-test-support.ts b/extensions/zalo/test-support/monitor-mocks-test-support.ts index fbb7c8aa3c4..57556f280fd 100644 --- a/extensions/zalo/test-support/monitor-mocks-test-support.ts +++ b/extensions/zalo/test-support/monitor-mocks-test-support.ts @@ -1,5 +1,5 @@ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/zalo"; -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; import { createEmptyPluginRegistry, setActivePluginRegistry, @@ -16,19 +16,34 @@ const secretInputModuleUrl = new URL("../src/secret-input.ts", import.meta.url). const apiModuleId = new URL("../src/api.js", import.meta.url).pathname; const runtimeModuleId = new URL("../src/runtime.js", import.meta.url).pathname; -const lifecycleMocks = vi.hoisted(() => ({ - setWebhookMock: vi.fn(async () => ({ ok: true, result: { url: "" } })), - deleteWebhookMock: vi.fn(async () => ({ ok: true, result: { url: "" } })), - getWebhookInfoMock: vi.fn(async () => ({ ok: true, result: { url: "" } })), - getUpdatesMock: vi.fn(() => new Promise(() => {})), - sendChatActionMock: vi.fn(async () => ({ ok: true })), - sendMessageMock: vi.fn(async () => ({ - ok: true, - result: { message_id: "zalo-test-reply-1" }, - })), - sendPhotoMock: vi.fn(async () => ({ ok: true })), - getZaloRuntimeMock: vi.fn(), -})); +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; +type ZaloLifecycleMocks = { + setWebhookMock: AsyncUnknownMock; + deleteWebhookMock: AsyncUnknownMock; + getWebhookInfoMock: AsyncUnknownMock; + getUpdatesMock: UnknownMock; + sendChatActionMock: AsyncUnknownMock; + sendMessageMock: AsyncUnknownMock; + sendPhotoMock: AsyncUnknownMock; + getZaloRuntimeMock: UnknownMock; +}; + +const lifecycleMocks = vi.hoisted( + (): ZaloLifecycleMocks => ({ + setWebhookMock: vi.fn(async () => ({ ok: true, result: { url: "" } })), + deleteWebhookMock: vi.fn(async () => ({ ok: true, result: { url: "" } })), + getWebhookInfoMock: vi.fn(async () => ({ ok: true, result: { url: "" } })), + getUpdatesMock: vi.fn(() => new Promise(() => {})), + sendChatActionMock: vi.fn(async () => ({ ok: true })), + sendMessageMock: vi.fn(async () => ({ + ok: true, + result: { message_id: "zalo-test-reply-1" }, + })), + sendPhotoMock: vi.fn(async () => ({ ok: true })), + getZaloRuntimeMock: vi.fn(), + }), +); export const setWebhookMock = lifecycleMocks.setWebhookMock; export const deleteWebhookMock = lifecycleMocks.deleteWebhookMock; @@ -37,7 +52,7 @@ export const getUpdatesMock = lifecycleMocks.getUpdatesMock; export const sendChatActionMock = lifecycleMocks.sendChatActionMock; export const sendMessageMock = lifecycleMocks.sendMessageMock; export const sendPhotoMock = lifecycleMocks.sendPhotoMock; -export const getZaloRuntimeMock = lifecycleMocks.getZaloRuntimeMock; +export const getZaloRuntimeMock: UnknownMock = lifecycleMocks.getZaloRuntimeMock; function installLifecycleModuleMocks() { vi.doMock(apiModuleId, async (importOriginal) => { diff --git a/extensions/zalouser/src/zalo-js.test-mocks.ts b/extensions/zalouser/src/zalo-js.test-mocks.ts index 2b9853a26d7..5899cc706be 100644 --- a/extensions/zalouser/src/zalo-js.test-mocks.ts +++ b/extensions/zalouser/src/zalo-js.test-mocks.ts @@ -1,31 +1,59 @@ -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; -const zaloJsMocks = vi.hoisted(() => ({ - checkZaloAuthenticatedMock: vi.fn(async () => false), - getZaloUserInfoMock: vi.fn(async () => null), - listZaloFriendsMock: vi.fn(async () => []), - listZaloFriendsMatchingMock: vi.fn(async () => []), - listZaloGroupMembersMock: vi.fn(async () => []), - listZaloGroupsMock: vi.fn(async () => []), - listZaloGroupsMatchingMock: vi.fn(async () => []), - logoutZaloProfileMock: vi.fn(async () => {}), - resolveZaloAllowFromEntriesMock: vi.fn(async ({ entries }: { entries: string[] }) => - entries.map((entry) => ({ input: entry, resolved: true, id: entry, note: undefined })), - ), - resolveZaloGroupContextMock: vi.fn(async () => null), - resolveZaloGroupsByEntriesMock: vi.fn(async ({ entries }: { entries: string[] }) => - entries.map((entry) => ({ input: entry, resolved: true, id: entry, note: undefined })), - ), - startZaloListenerMock: vi.fn(async () => ({ stop: vi.fn() })), - startZaloQrLoginMock: vi.fn(async () => ({ - message: "qr pending", - qrDataUrl: undefined, - })), - waitForZaloQrLoginMock: vi.fn(async () => ({ - connected: false, - message: "login pending", - })), -})); +type ZaloJsModule = typeof import("./zalo-js.js"); +type ZaloJsMocks = { + checkZaloAuthenticatedMock: Mock; + getZaloUserInfoMock: Mock; + listZaloFriendsMock: Mock; + listZaloFriendsMatchingMock: Mock; + listZaloGroupMembersMock: Mock; + listZaloGroupsMock: Mock; + listZaloGroupsMatchingMock: Mock; + logoutZaloProfileMock: Mock; + resolveZaloAllowFromEntriesMock: Mock; + resolveZaloGroupContextMock: Mock; + resolveZaloGroupsByEntriesMock: Mock; + startZaloListenerMock: Mock; + startZaloQrLoginMock: Mock; + waitForZaloQrLoginMock: Mock; +}; + +const zaloJsMocks = vi.hoisted( + (): ZaloJsMocks => ({ + checkZaloAuthenticatedMock: vi.fn(async () => false), + getZaloUserInfoMock: vi.fn(async () => null), + listZaloFriendsMock: vi.fn(async () => []), + listZaloFriendsMatchingMock: vi.fn(async () => []), + listZaloGroupMembersMock: vi.fn(async () => []), + listZaloGroupsMock: vi.fn(async () => []), + listZaloGroupsMatchingMock: vi.fn(async () => []), + logoutZaloProfileMock: vi.fn(async () => ({ + cleared: true, + loggedOut: true, + message: "Logged out and cleared local session.", + })), + resolveZaloAllowFromEntriesMock: vi.fn(async ({ entries }: { entries: string[] }) => + entries.map((entry) => ({ input: entry, resolved: true, id: entry, note: undefined })), + ), + resolveZaloGroupContextMock: vi.fn(async (_profile, groupId) => ({ + groupId, + name: undefined, + members: [], + })), + resolveZaloGroupsByEntriesMock: vi.fn(async ({ entries }: { entries: string[] }) => + entries.map((entry) => ({ input: entry, resolved: true, id: entry, note: undefined })), + ), + startZaloListenerMock: vi.fn(async () => ({ stop: vi.fn() })), + startZaloQrLoginMock: vi.fn(async () => ({ + message: "qr pending", + qrDataUrl: undefined, + })), + waitForZaloQrLoginMock: vi.fn(async () => ({ + connected: false, + message: "login pending", + })), + }), +); export const checkZaloAuthenticatedMock = zaloJsMocks.checkZaloAuthenticatedMock; export const getZaloUserInfoMock = zaloJsMocks.getZaloUserInfoMock; @@ -38,7 +66,8 @@ export const logoutZaloProfileMock = zaloJsMocks.logoutZaloProfileMock; export const resolveZaloAllowFromEntriesMock = zaloJsMocks.resolveZaloAllowFromEntriesMock; export const resolveZaloGroupContextMock = zaloJsMocks.resolveZaloGroupContextMock; export const resolveZaloGroupsByEntriesMock = zaloJsMocks.resolveZaloGroupsByEntriesMock; -export const startZaloListenerMock = zaloJsMocks.startZaloListenerMock; +export const startZaloListenerMock: Mock = + zaloJsMocks.startZaloListenerMock; export const startZaloQrLoginMock = zaloJsMocks.startZaloQrLoginMock; export const waitForZaloQrLoginMock = zaloJsMocks.waitForZaloQrLoginMock; diff --git a/src/agents/cli-runner.test-support.ts b/src/agents/cli-runner.test-support.ts index 9d936575319..411fcde9d12 100644 --- a/src/agents/cli-runner.test-support.ts +++ b/src/agents/cli-runner.test-support.ts @@ -1,46 +1,65 @@ import fs from "node:fs/promises"; +import type { Mock } from "vitest"; import { beforeEach, vi } from "vitest"; import { buildAnthropicCliBackend } from "../../extensions/anthropic/test-api.js"; import { buildGoogleGeminiCliBackend } from "../../extensions/google/test-api.js"; import { buildOpenAICodexCliBackend } from "../../extensions/openai/test-api.js"; import type { OpenClawConfig } from "../config/config.js"; +import type { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; +import type { enqueueSystemEvent } from "../infra/system-events.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; +import type { getProcessSupervisor } from "../process/supervisor/index.js"; import { setCliRunnerExecuteTestDeps } from "./cli-runner/execute.js"; import { setCliRunnerPrepareTestDeps } from "./cli-runner/prepare.js"; import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; import type { WorkspaceBootstrapFile } from "./workspace.js"; -export const supervisorSpawnMock = vi.fn(); -export const enqueueSystemEventMock = vi.fn(); -export const requestHeartbeatNowMock = vi.fn(); +type ProcessSupervisor = ReturnType; +type SupervisorSpawnFn = ProcessSupervisor["spawn"]; +type EnqueueSystemEventFn = typeof enqueueSystemEvent; +type RequestHeartbeatNowFn = typeof requestHeartbeatNow; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type BootstrapContext = { + bootstrapFiles: WorkspaceBootstrapFile[]; + contextFiles: EmbeddedContextFile[]; +}; +type ResolveBootstrapContextForRunMock = Mock<() => Promise>; + +export const supervisorSpawnMock: UnknownMock = vi.fn(); +export const enqueueSystemEventMock: UnknownMock = vi.fn(); +export const requestHeartbeatNowMock: UnknownMock = vi.fn(); export const SMALL_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII="; -const hoisted = vi.hoisted(() => { - type BootstrapContext = { - bootstrapFiles: WorkspaceBootstrapFile[]; - contextFiles: EmbeddedContextFile[]; - }; - - return { - resolveBootstrapContextForRunMock: vi.fn<() => Promise>(async () => ({ - bootstrapFiles: [], - contextFiles: [], - })), - }; -}); +const hoisted = vi.hoisted( + (): { + resolveBootstrapContextForRunMock: ResolveBootstrapContextForRunMock; + } => { + return { + resolveBootstrapContextForRunMock: vi.fn<() => Promise>(async () => ({ + bootstrapFiles: [], + contextFiles: [], + })), + }; + }, +); setCliRunnerExecuteTestDeps({ getProcessSupervisor: () => ({ - spawn: (...args: unknown[]) => supervisorSpawnMock(...args), + spawn: (params: Parameters[0]) => + supervisorSpawnMock(params) as ReturnType, cancel: vi.fn(), cancelScope: vi.fn(), reconcileOrphans: vi.fn(), getRecord: vi.fn(), }), - enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args), - requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args), + enqueueSystemEvent: ( + text: Parameters[0], + options: Parameters[1], + ) => enqueueSystemEventMock(text, options) as ReturnType, + requestHeartbeatNow: (options?: Parameters[0]) => + requestHeartbeatNowMock(options) as ReturnType, }); setCliRunnerPrepareTestDeps({ @@ -71,7 +90,19 @@ type TestCliBackendConfig = { clearEnv?: string[]; }; -export function createManagedRun(exit: MockRunExit, pid = 1234) { +type ManagedRunMock = { + runId: string; + pid: number; + startedAtMs: number; + stdin: undefined; + wait: Mock<() => Promise>; + cancel: Mock<() => void>; +}; + +export function createManagedRun( + exit: MockRunExit, + pid = 1234, +): ManagedRunMock & Awaited> { return { runId: "run-supervisor", pid, diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts index b84e603daa1..3c05e0944a1 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts @@ -3,7 +3,7 @@ import os from "node:os"; import path from "node:path"; import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { Api, Model } from "@mariozechner/pi-ai"; -import { expect, vi } from "vitest"; +import { expect, vi, type Mock } from "vitest"; import type { AssembleResult, BootstrapResult, @@ -22,12 +22,37 @@ type AcquireSessionWriteLockFn = typeof import("../../session-write-lock.js").acquireSessionWriteLock; type SubscriptionMock = ReturnType; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; +type BootstrapContext = { + bootstrapFiles: WorkspaceBootstrapFile[]; + contextFiles: EmbeddedContextFile[]; +}; +type SessionManagerMocks = { + getLeafEntry: UnknownMock; + branch: UnknownMock; + resetLeaf: UnknownMock; + buildSessionContext: Mock<() => { messages: AgentMessage[] }>; + appendCustomEntry: UnknownMock; +}; +type AttemptSpawnWorkspaceHoisted = { + spawnSubagentDirectMock: UnknownMock; + createAgentSessionMock: UnknownMock; + sessionManagerOpenMock: UnknownMock; + resolveSandboxContextMock: UnknownMock; + subscribeEmbeddedPiSessionMock: Mock; + acquireSessionWriteLockMock: Mock; + installToolResultContextGuardMock: UnknownMock; + flushPendingToolResultsAfterIdleMock: AsyncUnknownMock; + releaseWsSessionMock: UnknownMock; + resolveBootstrapContextForRunMock: Mock<() => Promise>; + getGlobalHookRunnerMock: Mock<() => unknown>; + initializeGlobalHookRunnerMock: UnknownMock; + runContextEngineMaintenanceMock: AsyncUnknownMock; + sessionManager: SessionManagerMocks; +}; -const hoisted = vi.hoisted(() => { - type BootstrapContext = { - bootstrapFiles: WorkspaceBootstrapFile[]; - contextFiles: EmbeddedContextFile[]; - }; +const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { const spawnSubagentDirectMock = vi.fn(); const createAgentSessionMock = vi.fn(); const sessionManagerOpenMock = vi.fn(); @@ -90,7 +115,7 @@ const hoisted = vi.hoisted(() => { }; }); -export function getHoisted() { +export function getHoisted(): AttemptSpawnWorkspaceHoisted { return hoisted; } diff --git a/src/agents/pi-embedded-runner/stream-payload-utils.ts b/src/agents/pi-embedded-runner/stream-payload-utils.ts index 580bf5b1391..9fe634b7932 100644 --- a/src/agents/pi-embedded-runner/stream-payload-utils.ts +++ b/src/agents/pi-embedded-runner/stream-payload-utils.ts @@ -6,7 +6,7 @@ export function streamWithPayloadPatch( context: Parameters[1], options: Parameters[2], patchPayload: (payload: Record) => void, -) { +): ReturnType { const originalOnPayload = options?.onPayload; return underlying(model, context, { ...options, diff --git a/src/agents/sandbox/fs-bridge.test-helpers.ts b/src/agents/sandbox/fs-bridge.test-helpers.ts index 5cd987b06a7..60609a56864 100644 --- a/src/agents/sandbox/fs-bridge.test-helpers.ts +++ b/src/agents/sandbox/fs-bridge.test-helpers.ts @@ -1,21 +1,30 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { beforeEach, expect, vi } from "vitest"; +import { beforeEach, expect, vi, type Mock } from "vitest"; -let actualOpenBoundaryFile: - | (( - ...args: Parameters - ) => ReturnType) - | undefined; +type ExecDockerRawFn = typeof import("./docker.js").execDockerRaw; +type OpenBoundaryFileFn = typeof import("../../infra/boundary-file-read.js").openBoundaryFile; +type ExecDockerArgs = Parameters[0]; +type ExecDockerRawMock = Mock; +type OpenBoundaryFileMock = Mock; +type FsBridgeHoisted = { + execDockerRaw: ExecDockerRawMock; + openBoundaryFile: OpenBoundaryFileMock; +}; -const hoisted = vi.hoisted(() => ({ - execDockerRaw: vi.fn(), - openBoundaryFile: vi.fn(), -})); +let actualOpenBoundaryFile: OpenBoundaryFileFn | undefined; + +const hoisted = vi.hoisted( + (): FsBridgeHoisted => ({ + execDockerRaw: vi.fn(), + openBoundaryFile: vi.fn(), + }), +); vi.mock("./docker.js", () => ({ - execDockerRaw: (...args: unknown[]) => hoisted.execDockerRaw(...args), + execDockerRaw: (args: ExecDockerArgs, opts?: Parameters[1]) => + hoisted.execDockerRaw(args, opts), })); vi.mock("../../infra/boundary-file-read.js", async (importOriginal) => { @@ -23,7 +32,8 @@ vi.mock("../../infra/boundary-file-read.js", async (importOriginal) => { actualOpenBoundaryFile = actual.openBoundaryFile; return { ...actual, - openBoundaryFile: (...args: unknown[]) => hoisted.openBoundaryFile(...args), + openBoundaryFile: (params: Parameters[0]) => + hoisted.openBoundaryFile(params), }; }); @@ -35,14 +45,16 @@ let createSandboxFsBridgeImpl: typeof import("./fs-bridge.js").createSandboxFsBr async function loadFreshFsBridgeModuleForTest() { vi.resetModules(); vi.doMock("./docker.js", () => ({ - execDockerRaw: (...args: unknown[]) => hoisted.execDockerRaw(...args), + execDockerRaw: (args: ExecDockerArgs, opts?: Parameters[1]) => + hoisted.execDockerRaw(args, opts), })); vi.doMock("../../infra/boundary-file-read.js", async (importOriginal) => { const actual = await importOriginal(); actualOpenBoundaryFile = actual.openBoundaryFile; return { ...actual, - openBoundaryFile: (...args: unknown[]) => hoisted.openBoundaryFile(...args), + openBoundaryFile: (params: Parameters[0]) => + hoisted.openBoundaryFile(params), }; }); ({ createSandboxFsBridge: createSandboxFsBridgeImpl } = await import("./fs-bridge.js")); @@ -57,8 +69,8 @@ export function createSandboxFsBridge( return createSandboxFsBridgeImpl(...args); } -export const mockedExecDockerRaw = hoisted.execDockerRaw; -export const mockedOpenBoundaryFile = hoisted.openBoundaryFile; +export const mockedExecDockerRaw: ExecDockerRawMock = hoisted.execDockerRaw; +export const mockedOpenBoundaryFile: OpenBoundaryFileMock = hoisted.openBoundaryFile; const DOCKER_SCRIPT_INDEX = 5; const DOCKER_FIRST_SCRIPT_ARG_INDEX = 7; diff --git a/src/cli/plugins-cli-test-helpers.ts b/src/cli/plugins-cli-test-helpers.ts index b6436254429..5616591fe38 100644 --- a/src/cli/plugins-cli-test-helpers.ts +++ b/src/cli/plugins-cli-test-helpers.ts @@ -1,38 +1,52 @@ import { Command } from "commander"; +import type { Mock } from "vitest"; import { vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { createCliRuntimeCapture } from "./test-runtime-capture.js"; -export const loadConfig = vi.fn<() => OpenClawConfig>(() => ({}) as OpenClawConfig); -export const readConfigFileSnapshot = vi.fn(); -export const writeConfigFile = vi.fn<(config: OpenClawConfig) => Promise>( - async () => undefined, -); -export const replaceConfigFile = vi.fn( +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; +type LoadConfigFn = (typeof import("../config/config.js"))["loadConfig"]; +type ParseClawHubPluginSpecFn = (typeof import("../infra/clawhub.js"))["parseClawHubPluginSpec"]; +type InstallPluginFromMarketplaceFn = + (typeof import("../plugins/marketplace.js"))["installPluginFromMarketplace"]; +type ListMarketplacePluginsFn = + (typeof import("../plugins/marketplace.js"))["listMarketplacePlugins"]; +type ResolveMarketplaceInstallShortcutFn = + (typeof import("../plugins/marketplace.js"))["resolveMarketplaceInstallShortcut"]; + +function invokeMock(mock: unknown, ...args: TArgs): TResult { + return (mock as (...args: TArgs) => TResult)(...args); +} + +export const loadConfig: Mock = vi.fn(() => ({}) as OpenClawConfig); +export const readConfigFileSnapshot: AsyncUnknownMock = vi.fn(); +export const writeConfigFile: AsyncUnknownMock = vi.fn(async () => undefined); +export const replaceConfigFile: AsyncUnknownMock = vi.fn( async (params: { nextConfig: OpenClawConfig }) => await writeConfigFile(params.nextConfig), -); -export const resolveStateDir = vi.fn(() => "/tmp/openclaw-state"); -export const installPluginFromMarketplace = vi.fn(); -export const listMarketplacePlugins = vi.fn(); -export const resolveMarketplaceInstallShortcut = vi.fn(); -export const enablePluginInConfig = vi.fn(); -export const recordPluginInstall = vi.fn(); -export const clearPluginManifestRegistryCache = vi.fn(); -export const buildPluginSnapshotReport = vi.fn(); -export const buildPluginDiagnosticsReport = vi.fn(); -export const buildPluginCompatibilityNotices = vi.fn(); -export const applyExclusiveSlotSelection = vi.fn(); -export const uninstallPlugin = vi.fn(); -export const updateNpmInstalledPlugins = vi.fn(); -export const updateNpmInstalledHookPacks = vi.fn(); -export const promptYesNo = vi.fn(); -export const installPluginFromNpmSpec = vi.fn(); -export const installPluginFromPath = vi.fn(); -export const installPluginFromClawHub = vi.fn(); -export const parseClawHubPluginSpec = vi.fn(); -export const installHooksFromNpmSpec = vi.fn(); -export const installHooksFromPath = vi.fn(); -export const recordHookInstall = vi.fn(); +) as AsyncUnknownMock; +export const resolveStateDir: Mock<() => string> = vi.fn(() => "/tmp/openclaw-state"); +export const installPluginFromMarketplace: Mock = vi.fn(); +export const listMarketplacePlugins: Mock = vi.fn(); +export const resolveMarketplaceInstallShortcut: Mock = vi.fn(); +export const enablePluginInConfig: UnknownMock = vi.fn(); +export const recordPluginInstall: UnknownMock = vi.fn(); +export const clearPluginManifestRegistryCache: UnknownMock = vi.fn(); +export const buildPluginSnapshotReport: UnknownMock = vi.fn(); +export const buildPluginDiagnosticsReport: UnknownMock = vi.fn(); +export const buildPluginCompatibilityNotices: UnknownMock = vi.fn(); +export const applyExclusiveSlotSelection: UnknownMock = vi.fn(); +export const uninstallPlugin: AsyncUnknownMock = vi.fn(); +export const updateNpmInstalledPlugins: AsyncUnknownMock = vi.fn(); +export const updateNpmInstalledHookPacks: AsyncUnknownMock = vi.fn(); +export const promptYesNo: AsyncUnknownMock = vi.fn(); +export const installPluginFromNpmSpec: AsyncUnknownMock = vi.fn(); +export const installPluginFromPath: AsyncUnknownMock = vi.fn(); +export const installPluginFromClawHub: AsyncUnknownMock = vi.fn(); +export const parseClawHubPluginSpec: Mock = vi.fn(); +export const installHooksFromNpmSpec: AsyncUnknownMock = vi.fn(); +export const installHooksFromPath: AsyncUnknownMock = vi.fn(); +export const recordHookInstall: UnknownMock = vi.fn(); const { defaultRuntime, runtimeLogs, runtimeErrors, resetRuntimeCapture } = createCliRuntimeCapture(); @@ -45,9 +59,28 @@ vi.mock("../runtime.js", () => ({ vi.mock("../config/config.js", () => ({ loadConfig: () => loadConfig(), - readConfigFileSnapshot: (...args: unknown[]) => readConfigFileSnapshot(...args), - writeConfigFile: (config: OpenClawConfig) => writeConfigFile(config), - replaceConfigFile: (params: { nextConfig: OpenClawConfig }) => replaceConfigFile(params), + readConfigFileSnapshot: (( + ...args: Parameters<(typeof import("../config/config.js"))["readConfigFileSnapshot"]> + ) => + invokeMock< + Parameters<(typeof import("../config/config.js"))["readConfigFileSnapshot"]>, + ReturnType<(typeof import("../config/config.js"))["readConfigFileSnapshot"]> + >( + readConfigFileSnapshot, + ...args, + )) as (typeof import("../config/config.js"))["readConfigFileSnapshot"], + writeConfigFile: ((config: OpenClawConfig) => + invokeMock< + [OpenClawConfig], + ReturnType<(typeof import("../config/config.js"))["writeConfigFile"]> + >(writeConfigFile, config)) as (typeof import("../config/config.js"))["writeConfigFile"], + replaceConfigFile: (( + params: Parameters<(typeof import("../config/config.js"))["replaceConfigFile"]>[0], + ) => + invokeMock< + [Parameters<(typeof import("../config/config.js"))["replaceConfigFile"]>[0]], + ReturnType<(typeof import("../config/config.js"))["replaceConfigFile"]> + >(replaceConfigFile, params)) as (typeof import("../config/config.js"))["replaceConfigFile"], })); vi.mock("../config/paths.js", () => ({ @@ -55,18 +88,34 @@ vi.mock("../config/paths.js", () => ({ })); vi.mock("../plugins/marketplace.js", () => ({ - installPluginFromMarketplace: (...args: unknown[]) => installPluginFromMarketplace(...args), - listMarketplacePlugins: (...args: unknown[]) => listMarketplacePlugins(...args), - resolveMarketplaceInstallShortcut: (...args: unknown[]) => - resolveMarketplaceInstallShortcut(...args), + installPluginFromMarketplace: ((...args: Parameters) => + installPluginFromMarketplace(...args)) as InstallPluginFromMarketplaceFn, + listMarketplacePlugins: ((...args: Parameters) => + listMarketplacePlugins(...args)) as ListMarketplacePluginsFn, + resolveMarketplaceInstallShortcut: ((...args: Parameters) => + resolveMarketplaceInstallShortcut(...args)) as ResolveMarketplaceInstallShortcutFn, })); vi.mock("../plugins/enable.js", () => ({ - enablePluginInConfig: (...args: unknown[]) => enablePluginInConfig(...args), + enablePluginInConfig: ((cfg: OpenClawConfig, pluginId: string) => + invokeMock<[OpenClawConfig, string], unknown>( + enablePluginInConfig, + cfg, + pluginId, + )) as (typeof import("../plugins/enable.js"))["enablePluginInConfig"], })); vi.mock("../plugins/installs.js", () => ({ - recordPluginInstall: (...args: unknown[]) => recordPluginInstall(...args), + recordPluginInstall: (( + ...args: Parameters<(typeof import("../plugins/installs.js"))["recordPluginInstall"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/installs.js"))["recordPluginInstall"]>, + ReturnType<(typeof import("../plugins/installs.js"))["recordPluginInstall"]> + >( + recordPluginInstall, + ...args, + )) as (typeof import("../plugins/installs.js"))["recordPluginInstall"], })); vi.mock("../plugins/manifest-registry.js", () => ({ @@ -74,17 +123,59 @@ vi.mock("../plugins/manifest-registry.js", () => ({ })); vi.mock("../plugins/status.js", () => ({ - buildPluginSnapshotReport: (...args: unknown[]) => buildPluginSnapshotReport(...args), - buildPluginDiagnosticsReport: (...args: unknown[]) => buildPluginDiagnosticsReport(...args), - buildPluginCompatibilityNotices: (...args: unknown[]) => buildPluginCompatibilityNotices(...args), + buildPluginSnapshotReport: (( + ...args: Parameters<(typeof import("../plugins/status.js"))["buildPluginSnapshotReport"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/status.js"))["buildPluginSnapshotReport"]>, + ReturnType<(typeof import("../plugins/status.js"))["buildPluginSnapshotReport"]> + >( + buildPluginSnapshotReport, + ...args, + )) as (typeof import("../plugins/status.js"))["buildPluginSnapshotReport"], + buildPluginDiagnosticsReport: (( + ...args: Parameters<(typeof import("../plugins/status.js"))["buildPluginDiagnosticsReport"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/status.js"))["buildPluginDiagnosticsReport"]>, + ReturnType<(typeof import("../plugins/status.js"))["buildPluginDiagnosticsReport"]> + >( + buildPluginDiagnosticsReport, + ...args, + )) as (typeof import("../plugins/status.js"))["buildPluginDiagnosticsReport"], + buildPluginCompatibilityNotices: (( + ...args: Parameters<(typeof import("../plugins/status.js"))["buildPluginCompatibilityNotices"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/status.js"))["buildPluginCompatibilityNotices"]>, + ReturnType<(typeof import("../plugins/status.js"))["buildPluginCompatibilityNotices"]> + >( + buildPluginCompatibilityNotices, + ...args, + )) as (typeof import("../plugins/status.js"))["buildPluginCompatibilityNotices"], })); vi.mock("../plugins/slots.js", () => ({ - applyExclusiveSlotSelection: (...args: unknown[]) => applyExclusiveSlotSelection(...args), + applyExclusiveSlotSelection: (( + params: Parameters<(typeof import("../plugins/slots.js"))["applyExclusiveSlotSelection"]>[0], + ) => + invokeMock< + [Parameters<(typeof import("../plugins/slots.js"))["applyExclusiveSlotSelection"]>[0]], + ReturnType<(typeof import("../plugins/slots.js"))["applyExclusiveSlotSelection"]> + >( + applyExclusiveSlotSelection, + params, + )) as (typeof import("../plugins/slots.js"))["applyExclusiveSlotSelection"], })); vi.mock("../plugins/uninstall.js", () => ({ - uninstallPlugin: (...args: unknown[]) => uninstallPlugin(...args), + uninstallPlugin: (( + ...args: Parameters<(typeof import("../plugins/uninstall.js"))["uninstallPlugin"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/uninstall.js"))["uninstallPlugin"]>, + ReturnType<(typeof import("../plugins/uninstall.js"))["uninstallPlugin"]> + >(uninstallPlugin, ...args)) as (typeof import("../plugins/uninstall.js"))["uninstallPlugin"], resolveUninstallDirectoryTarget: ({ installRecord, }: { @@ -93,33 +184,97 @@ vi.mock("../plugins/uninstall.js", () => ({ })); vi.mock("../plugins/update.js", () => ({ - updateNpmInstalledPlugins: (...args: unknown[]) => updateNpmInstalledPlugins(...args), + updateNpmInstalledPlugins: (( + ...args: Parameters<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]>, + ReturnType<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]> + >( + updateNpmInstalledPlugins, + ...args, + )) as (typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"], })); vi.mock("../hooks/update.js", () => ({ - updateNpmInstalledHookPacks: (...args: unknown[]) => updateNpmInstalledHookPacks(...args), + updateNpmInstalledHookPacks: (( + ...args: Parameters<(typeof import("../hooks/update.js"))["updateNpmInstalledHookPacks"]> + ) => + invokeMock< + Parameters<(typeof import("../hooks/update.js"))["updateNpmInstalledHookPacks"]>, + ReturnType<(typeof import("../hooks/update.js"))["updateNpmInstalledHookPacks"]> + >( + updateNpmInstalledHookPacks, + ...args, + )) as (typeof import("../hooks/update.js"))["updateNpmInstalledHookPacks"], })); vi.mock("./prompt.js", () => ({ - promptYesNo: (...args: unknown[]) => promptYesNo(...args), + promptYesNo: ((...args: Parameters<(typeof import("./prompt.js"))["promptYesNo"]>) => + invokeMock< + Parameters<(typeof import("./prompt.js"))["promptYesNo"]>, + ReturnType<(typeof import("./prompt.js"))["promptYesNo"]> + >(promptYesNo, ...args)) as (typeof import("./prompt.js"))["promptYesNo"], })); vi.mock("../plugins/install.js", () => ({ PLUGIN_INSTALL_ERROR_CODE: { NPM_PACKAGE_NOT_FOUND: "npm_package_not_found", }, - installPluginFromNpmSpec: (...args: unknown[]) => installPluginFromNpmSpec(...args), - installPluginFromPath: (...args: unknown[]) => installPluginFromPath(...args), + installPluginFromNpmSpec: (( + ...args: Parameters<(typeof import("../plugins/install.js"))["installPluginFromNpmSpec"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/install.js"))["installPluginFromNpmSpec"]>, + ReturnType<(typeof import("../plugins/install.js"))["installPluginFromNpmSpec"]> + >( + installPluginFromNpmSpec, + ...args, + )) as (typeof import("../plugins/install.js"))["installPluginFromNpmSpec"], + installPluginFromPath: (( + ...args: Parameters<(typeof import("../plugins/install.js"))["installPluginFromPath"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/install.js"))["installPluginFromPath"]>, + ReturnType<(typeof import("../plugins/install.js"))["installPluginFromPath"]> + >( + installPluginFromPath, + ...args, + )) as (typeof import("../plugins/install.js"))["installPluginFromPath"], })); vi.mock("../hooks/install.js", () => ({ - installHooksFromNpmSpec: (...args: unknown[]) => installHooksFromNpmSpec(...args), - installHooksFromPath: (...args: unknown[]) => installHooksFromPath(...args), + installHooksFromNpmSpec: (( + ...args: Parameters<(typeof import("../hooks/install.js"))["installHooksFromNpmSpec"]> + ) => + invokeMock< + Parameters<(typeof import("../hooks/install.js"))["installHooksFromNpmSpec"]>, + ReturnType<(typeof import("../hooks/install.js"))["installHooksFromNpmSpec"]> + >( + installHooksFromNpmSpec, + ...args, + )) as (typeof import("../hooks/install.js"))["installHooksFromNpmSpec"], + installHooksFromPath: (( + ...args: Parameters<(typeof import("../hooks/install.js"))["installHooksFromPath"]> + ) => + invokeMock< + Parameters<(typeof import("../hooks/install.js"))["installHooksFromPath"]>, + ReturnType<(typeof import("../hooks/install.js"))["installHooksFromPath"]> + >( + installHooksFromPath, + ...args, + )) as (typeof import("../hooks/install.js"))["installHooksFromPath"], resolveHookInstallDir: (hookId: string) => `/tmp/hooks/${hookId}`, })); vi.mock("../hooks/installs.js", () => ({ - recordHookInstall: (...args: unknown[]) => recordHookInstall(...args), + recordHookInstall: (( + ...args: Parameters<(typeof import("../hooks/installs.js"))["recordHookInstall"]> + ) => + invokeMock< + Parameters<(typeof import("../hooks/installs.js"))["recordHookInstall"]>, + ReturnType<(typeof import("../hooks/installs.js"))["recordHookInstall"]> + >(recordHookInstall, ...args)) as (typeof import("../hooks/installs.js"))["recordHookInstall"], })); vi.mock("../plugins/clawhub.js", () => ({ @@ -127,13 +282,31 @@ vi.mock("../plugins/clawhub.js", () => ({ PACKAGE_NOT_FOUND: "package_not_found", VERSION_NOT_FOUND: "version_not_found", }, - installPluginFromClawHub: (...args: unknown[]) => installPluginFromClawHub(...args), + installPluginFromClawHub: (( + ...args: Parameters<(typeof import("../plugins/clawhub.js"))["installPluginFromClawHub"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/clawhub.js"))["installPluginFromClawHub"]>, + ReturnType<(typeof import("../plugins/clawhub.js"))["installPluginFromClawHub"]> + >( + installPluginFromClawHub, + ...args, + )) as (typeof import("../plugins/clawhub.js"))["installPluginFromClawHub"], formatClawHubSpecifier: ({ name, version }: { name: string; version?: string }) => `clawhub:${name}${version ? `@${version}` : ""}`, })); vi.mock("../infra/clawhub.js", () => ({ - parseClawHubPluginSpec: (...args: unknown[]) => parseClawHubPluginSpec(...args), + parseClawHubPluginSpec: (( + ...args: Parameters<(typeof import("../infra/clawhub.js"))["parseClawHubPluginSpec"]> + ) => + invokeMock< + Parameters<(typeof import("../infra/clawhub.js"))["parseClawHubPluginSpec"]>, + ReturnType<(typeof import("../infra/clawhub.js"))["parseClawHubPluginSpec"]> + >( + parseClawHubPluginSpec, + ...args, + )) as (typeof import("../infra/clawhub.js"))["parseClawHubPluginSpec"], })); const { registerPluginsCli } = await import("./plugins-cli.js"); @@ -197,7 +370,8 @@ export function resetPluginsCliTestState() { }); writeConfigFile.mockResolvedValue(undefined); replaceConfigFile.mockImplementation( - async (params: { nextConfig: OpenClawConfig }) => await writeConfigFile(params.nextConfig), + (async (params: { nextConfig: OpenClawConfig }) => + await writeConfigFile(params.nextConfig)) as (...args: unknown[]) => Promise, ); resolveStateDir.mockReturnValue("/tmp/openclaw-state"); resolveMarketplaceInstallShortcut.mockResolvedValue(null); @@ -205,8 +379,12 @@ export function resetPluginsCliTestState() { ok: false, error: "marketplace install failed", }); - enablePluginInConfig.mockImplementation((cfg: OpenClawConfig) => ({ config: cfg })); - recordPluginInstall.mockImplementation((cfg: OpenClawConfig) => cfg); + enablePluginInConfig.mockImplementation(((cfg: OpenClawConfig) => ({ config: cfg })) as ( + ...args: unknown[] + ) => unknown); + recordPluginInstall.mockImplementation( + ((cfg: OpenClawConfig) => cfg) as (...args: unknown[]) => unknown, + ); const defaultPluginReport = { plugins: [], diagnostics: [], @@ -214,10 +392,10 @@ export function resetPluginsCliTestState() { buildPluginSnapshotReport.mockReturnValue(defaultPluginReport); buildPluginDiagnosticsReport.mockReturnValue(defaultPluginReport); buildPluginCompatibilityNotices.mockReturnValue([]); - applyExclusiveSlotSelection.mockImplementation(({ config }: { config: OpenClawConfig }) => ({ + applyExclusiveSlotSelection.mockImplementation((({ config }: { config: OpenClawConfig }) => ({ config, warnings: [], - })); + })) as (...args: unknown[]) => unknown); uninstallPlugin.mockResolvedValue({ ok: true, config: {} as OpenClawConfig, @@ -260,5 +438,7 @@ export function resetPluginsCliTestState() { ok: false, error: "hook npm install disabled in test", }); - recordHookInstall.mockImplementation((cfg: OpenClawConfig) => cfg); + recordHookInstall.mockImplementation( + ((cfg: OpenClawConfig) => cfg) as (...args: unknown[]) => unknown, + ); } diff --git a/src/cli/plugins-cli.install.test.ts b/src/cli/plugins-cli.install.test.ts index 75a8386a8e4..8de96a91d3a 100644 --- a/src/cli/plugins-cli.install.test.ts +++ b/src/cli/plugins-cli.install.test.ts @@ -172,6 +172,7 @@ describe("plugins cli install", () => { ok: true, pluginId: "alpha", targetDir: cliInstallPath("alpha"), + extensions: ["index.js"], version: "1.2.3", marketplaceName: "Claude", marketplaceSource: "local/repo", diff --git a/src/commands/agents.bind.test-support.ts b/src/commands/agents.bind.test-support.ts index 20b65abfa85..eff5478cd7d 100644 --- a/src/commands/agents.bind.test-support.ts +++ b/src/commands/agents.bind.test-support.ts @@ -1,3 +1,4 @@ +import type { Mock } from "vitest"; import { vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js"; @@ -7,9 +8,11 @@ type ReplaceConfigFileResult = Awaited< ReturnType<(typeof import("../config/config.js"))["replaceConfigFile"]> >; -export const readConfigFileSnapshotMock = vi.fn(); -export const writeConfigFileMock = vi.fn().mockResolvedValue(undefined); -export const replaceConfigFileMock = vi.fn( +export const readConfigFileSnapshotMock: Mock<(...args: unknown[]) => Promise> = vi.fn(); +export const writeConfigFileMock: Mock<(...args: unknown[]) => Promise> = vi + .fn() + .mockResolvedValue(undefined); +export const replaceConfigFileMock: Mock<(...args: unknown[]) => Promise> = vi.fn( async (params: { nextConfig: OpenClawConfig }): Promise => { await writeConfigFileMock(params.nextConfig); return { @@ -19,17 +22,18 @@ export const replaceConfigFileMock = vi.fn( nextConfig: params.nextConfig, }; }, -); +) as Mock<(...args: unknown[]) => Promise>; vi.mock("../config/config.js", async (importOriginal) => { - return await mergeMockedModule( - await importOriginal(), - () => ({ - readConfigFileSnapshot: readConfigFileSnapshotMock, - writeConfigFile: writeConfigFileMock, - replaceConfigFile: replaceConfigFileMock, - }), - ); + const actual = await importOriginal(); + return await mergeMockedModule(actual, () => ({ + readConfigFileSnapshot: (...args: Parameters) => + readConfigFileSnapshotMock(...args) as ReturnType, + writeConfigFile: (...args: Parameters) => + writeConfigFileMock(...args) as ReturnType, + replaceConfigFile: (...args: Parameters) => + replaceConfigFileMock(...args) as ReturnType, + })); }); export const runtime = createTestRuntime(); diff --git a/src/commands/doctor.note-test-helpers.ts b/src/commands/doctor.note-test-helpers.ts index 1c5830921ea..fbc4e00a436 100644 --- a/src/commands/doctor.note-test-helpers.ts +++ b/src/commands/doctor.note-test-helpers.ts @@ -1,6 +1,7 @@ +import type { Mock } from "vitest"; import { vi } from "vitest"; -export const terminalNoteMock = vi.fn(); +export const terminalNoteMock: Mock<(...args: unknown[]) => unknown> = vi.fn(); vi.mock("../terminal/note.js", () => ({ note: (...args: unknown[]) => terminalNoteMock(...args), diff --git a/src/commands/status.scan.test-helpers.ts b/src/commands/status.scan.test-helpers.ts index 914872233c5..4e8795c0998 100644 --- a/src/commands/status.scan.test-helpers.ts +++ b/src/commands/status.scan.test-helpers.ts @@ -1,7 +1,27 @@ +import type { Mock } from "vitest"; import { vi } from "vitest"; import type { OpenClawConfig } from "../config/types.js"; -export function createStatusScanSharedMocks(configPathLabel: string) { +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type ResolveConfigPathMock = Mock<() => string>; + +export type StatusScanSharedMocks = { + resolveConfigPath: ResolveConfigPathMock; + hasPotentialConfiguredChannels: UnknownMock; + readBestEffortConfig: UnknownMock; + resolveCommandSecretRefsViaGateway: UnknownMock; + getUpdateCheckResult: UnknownMock; + getAgentLocalStatuses: UnknownMock; + getStatusSummary: UnknownMock; + getMemorySearchManager: UnknownMock; + buildGatewayConnectionDetails: UnknownMock; + probeGateway: UnknownMock; + resolveGatewayProbeAuthResolution: UnknownMock; + ensurePluginRegistryLoaded: UnknownMock; + buildPluginCompatibilityNotices: Mock<() => unknown[]>; +}; + +export function createStatusScanSharedMocks(configPathLabel: string): StatusScanSharedMocks { return { resolveConfigPath: vi.fn(() => `/tmp/openclaw-${configPathLabel}-missing-${process.pid}.json`), hasPotentialConfiguredChannels: vi.fn(), @@ -19,37 +39,54 @@ export function createStatusScanSharedMocks(configPathLabel: string) { }; } -export type StatusScanSharedMocks = ReturnType; +type StatusOsSummaryModuleMock = { + resolveOsSummary: Mock<() => { label: string }>; +}; -export function createStatusOsSummaryModuleMock() { +export function createStatusOsSummaryModuleMock(): StatusOsSummaryModuleMock { return { resolveOsSummary: vi.fn(() => ({ label: "test-os" })), }; } +type StatusScanDepsRuntimeModuleMock = { + getTailnetHostname: UnknownMock; + getMemorySearchManager: StatusScanSharedMocks["getMemorySearchManager"]; +}; + export function createStatusScanDepsRuntimeModuleMock( mocks: Pick, -) { +): StatusScanDepsRuntimeModuleMock { return { getTailnetHostname: vi.fn(), getMemorySearchManager: mocks.getMemorySearchManager, }; } +type StatusGatewayProbeModuleMock = { + pickGatewaySelfPresence: Mock<() => null>; + resolveGatewayProbeAuthResolution: StatusScanSharedMocks["resolveGatewayProbeAuthResolution"]; +}; + export function createStatusGatewayProbeModuleMock( mocks: Pick, -) { +): StatusGatewayProbeModuleMock { return { pickGatewaySelfPresence: vi.fn(() => null), resolveGatewayProbeAuthResolution: mocks.resolveGatewayProbeAuthResolution, }; } +type StatusGatewayCallModuleMock = { + buildGatewayConnectionDetails: StatusScanSharedMocks["buildGatewayConnectionDetails"]; + callGateway?: unknown; +}; + export function createStatusGatewayCallModuleMock( mocks: Pick & { callGateway?: unknown; }, -) { +): StatusGatewayCallModuleMock { return { buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails, ...(mocks.callGateway ? { callGateway: mocks.callGateway } : {}), @@ -58,7 +95,7 @@ export function createStatusGatewayCallModuleMock( export function createStatusPluginRegistryModuleMock( mocks: Pick, -) { +): { ensurePluginRegistryLoaded: StatusScanSharedMocks["ensurePluginRegistryLoaded"] } { return { ensurePluginRegistryLoaded: mocks.ensurePluginRegistryLoaded, }; @@ -66,7 +103,7 @@ export function createStatusPluginRegistryModuleMock( export function createStatusPluginStatusModuleMock( mocks: Pick, -) { +): { buildPluginCompatibilityNotices: StatusScanSharedMocks["buildPluginCompatibilityNotices"] } { return { buildPluginCompatibilityNotices: mocks.buildPluginCompatibilityNotices, }; @@ -74,7 +111,7 @@ export function createStatusPluginStatusModuleMock( export function createStatusUpdateModuleMock( mocks: Pick, -) { +): { getUpdateCheckResult: StatusScanSharedMocks["getUpdateCheckResult"] } { return { getUpdateCheckResult: mocks.getUpdateCheckResult, }; @@ -82,7 +119,7 @@ export function createStatusUpdateModuleMock( export function createStatusAgentLocalModuleMock( mocks: Pick, -) { +): { getAgentLocalStatuses: StatusScanSharedMocks["getAgentLocalStatuses"] } { return { getAgentLocalStatuses: mocks.getAgentLocalStatuses, }; @@ -90,23 +127,23 @@ export function createStatusAgentLocalModuleMock( export function createStatusSummaryModuleMock( mocks: Pick, -) { +): { getStatusSummary: StatusScanSharedMocks["getStatusSummary"] } { return { getStatusSummary: mocks.getStatusSummary, }; } -export function createStatusExecModuleMock() { +export function createStatusExecModuleMock(): { runExec: UnknownMock } { return { runExec: vi.fn(), }; } type StatusScanModuleTestMocks = StatusScanSharedMocks & { - buildChannelsTable?: ReturnType; - callGateway?: ReturnType; - getStatusCommandSecretTargetIds?: ReturnType; - resolveMemorySearchConfig?: ReturnType; + buildChannelsTable?: UnknownMock; + callGateway?: UnknownMock; + getStatusCommandSecretTargetIds?: UnknownMock; + resolveMemorySearchConfig?: UnknownMock; }; export async function loadStatusScanModuleForTest( diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index 5aa73ac4882..7bb3c184855 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -33,6 +33,9 @@ type GetReplyFromConfigFn = ( opts?: GetReplyOptions, configOverride?: OpenClawConfig, ) => Promise; +type CronIsolatedRunFn = (...args: unknown[]) => Promise<{ status: string; summary: string }>; +type AgentCommandFn = (...args: unknown[]) => Promise; +type SendWhatsAppFn = (...args: unknown[]) => Promise<{ messageId: string; toJid: string }>; const createStubOutboundAdapter = (channelId: ChannelPlugin["id"]): ChannelOutboundAdapter => ({ deliveryMode: "direct", @@ -233,8 +236,8 @@ const hoisted = vi.hoisted(() => { reasoning?: boolean; }>; }; - cronIsolatedRun: ReturnType; - agentCommand: ReturnType; + cronIsolatedRun: Mock; + agentCommand: Mock; testIsNixMode: { value: boolean }; sessionStoreSaveDelayMs: { value: number }; embeddedRunMock: { @@ -244,8 +247,8 @@ const hoisted = vi.hoisted(() => { waitResults: Map; }; testTailscaleWhois: { value: TailscaleWhoisIdentity | null }; - getReplyFromConfig: ReturnType>; - sendWhatsAppMock: ReturnType; + getReplyFromConfig: Mock; + sendWhatsAppMock: Mock; testState: { agentConfig: Record | undefined; agentsConfig: Record | undefined; @@ -346,13 +349,13 @@ export const setTestConfigRoot = (root: string) => { export const testTailnetIPv4 = hoisted.testTailnetIPv4; export const testTailscaleWhois = hoisted.testTailscaleWhois; export const piSdkMock = hoisted.piSdkMock; -export const cronIsolatedRun = hoisted.cronIsolatedRun; -export const agentCommand = hoisted.agentCommand; +export const cronIsolatedRun: Mock = hoisted.cronIsolatedRun; +export const agentCommand: Mock = hoisted.agentCommand; export const getReplyFromConfig: Mock = hoisted.getReplyFromConfig; export const mockGetReplyFromConfigOnce = (impl: GetReplyFromConfigFn) => { getReplyFromConfig.mockImplementationOnce(impl); }; -export const sendWhatsAppMock = hoisted.sendWhatsAppMock; +export const sendWhatsAppMock: Mock = hoisted.sendWhatsAppMock; export const testState = hoisted.testState; diff --git a/src/globals.ts b/src/globals.ts index 9cbb534219f..2060716a260 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -29,7 +29,9 @@ export function logVerboseConsole(message: string) { console.log(theme.muted(message)); } -export const success = theme.success; -export const warn = theme.warn; -export const info = theme.info; -export const danger = theme.error; +type ThemeFormatter = (value: string) => string; + +export const success: ThemeFormatter = theme.success; +export const warn: ThemeFormatter = theme.warn; +export const info: ThemeFormatter = theme.info; +export const danger: ThemeFormatter = theme.error; diff --git a/src/plugin-sdk/provider-auth-login.ts b/src/plugin-sdk/provider-auth-login.ts index f4848ef6207..4846227ce51 100644 --- a/src/plugin-sdk/provider-auth-login.ts +++ b/src/plugin-sdk/provider-auth-login.ts @@ -2,15 +2,17 @@ import { createLazyRuntimeMethodBinder, createLazyRuntimeModule } from "../shared/lazy-runtime.js"; +type ProviderAuthLoginRuntime = typeof import("./provider-auth-login.runtime.js"); + const loadProviderAuthLoginRuntime = createLazyRuntimeModule( () => import("./provider-auth-login.runtime.js"), ); const bindProviderAuthLoginRuntime = createLazyRuntimeMethodBinder(loadProviderAuthLoginRuntime); -export const githubCopilotLoginCommand = bindProviderAuthLoginRuntime( - (runtime) => runtime.githubCopilotLoginCommand, -); -export const loginChutes = bindProviderAuthLoginRuntime((runtime) => runtime.loginChutes); -export const loginOpenAICodexOAuth = bindProviderAuthLoginRuntime( - (runtime) => runtime.loginOpenAICodexOAuth, +export const githubCopilotLoginCommand: ProviderAuthLoginRuntime["githubCopilotLoginCommand"] = + bindProviderAuthLoginRuntime((runtime) => runtime.githubCopilotLoginCommand); +export const loginChutes: ProviderAuthLoginRuntime["loginChutes"] = bindProviderAuthLoginRuntime( + (runtime) => runtime.loginChutes, ); +export const loginOpenAICodexOAuth: ProviderAuthLoginRuntime["loginOpenAICodexOAuth"] = + bindProviderAuthLoginRuntime((runtime) => runtime.loginOpenAICodexOAuth); diff --git a/test/helpers/plugins/setup-wizard.ts b/test/helpers/plugins/setup-wizard.ts index aa2e5d9dee0..0141fb33c64 100644 --- a/test/helpers/plugins/setup-wizard.ts +++ b/test/helpers/plugins/setup-wizard.ts @@ -1,10 +1,23 @@ -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; import type { ChannelPlugin } from "../../../src/channels/plugins/types.js"; import type { WizardPrompter } from "../../../src/wizard/prompts.js"; import { createRuntimeEnv } from "./runtime-env.js"; export type { WizardPrompter } from "../../../src/wizard/prompts.js"; +type UnknownMock = Mock<(...args: unknown[]) => unknown>; +type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; +type QueuedWizardPrompter = { + intro: AsyncUnknownMock; + outro: AsyncUnknownMock; + note: AsyncUnknownMock; + select: AsyncUnknownMock; + multiselect: AsyncUnknownMock; + text: AsyncUnknownMock; + confirm: AsyncUnknownMock; + progress: Mock<() => { update: UnknownMock; stop: UnknownMock }>; + prompter: WizardPrompter; +}; export async function selectFirstWizardOption(params: { options: Array<{ value: T }>; @@ -34,7 +47,7 @@ export function createQueuedWizardPrompter(params?: { selectValues?: string[]; textValues?: string[]; confirmValues?: boolean[]; -}) { +}): QueuedWizardPrompter { const selectValues = [...(params?.selectValues ?? [])]; const textValues = [...(params?.textValues ?? [])]; const confirmValues = [...(params?.confirmValues ?? [])];