import type { OpenClawConfig } from "openclaw/plugin-sdk/discord"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import type { Mock } from "vitest"; import { expect, vi } from "vitest"; export type NativeCommandSpecMock = { name: string; description: string; acceptsArgs: boolean; }; export type PluginCommandSpecMock = { name: string; description: string; acceptsArgs: boolean; }; type ProviderMonitorTestMocks = { clientHandleDeployRequestMock: Mock<() => Promise>; clientFetchUserMock: Mock<(target: string) => Promise<{ id: string }>>; clientGetPluginMock: Mock<(name: string) => unknown>; clientConstructorOptionsMock: Mock<(options?: unknown) => void>; createDiscordAutoPresenceControllerMock: Mock<() => unknown>; createDiscordNativeCommandMock: Mock<(params?: { command?: { name?: string } }) => unknown>; createDiscordMessageHandlerMock: Mock<() => unknown>; createNoopThreadBindingManagerMock: Mock<() => { stop: ReturnType }>; createThreadBindingManagerMock: Mock<() => { stop: ReturnType }>; reconcileAcpThreadBindingsOnStartupMock: Mock<() => unknown>; createdBindingManagers: Array<{ stop: ReturnType }>; getAcpSessionStatusMock: Mock< (params: { cfg: OpenClawConfig; sessionKey: string; signal?: AbortSignal; }) => Promise<{ state: string }> >; getPluginCommandSpecsMock: Mock<() => PluginCommandSpecMock[]>; listNativeCommandSpecsForConfigMock: Mock<() => NativeCommandSpecMock[]>; listSkillCommandsForAgentsMock: Mock<() => unknown[]>; monitorLifecycleMock: Mock<(params: { threadBindings: { stop: () => void } }) => Promise>; resolveDiscordAccountMock: Mock<() => unknown>; resolveDiscordAllowlistConfigMock: Mock<() => Promise>; resolveNativeCommandsEnabledMock: Mock<() => boolean>; resolveNativeSkillsEnabledMock: Mock<() => boolean>; isVerboseMock: Mock<() => boolean>; shouldLogVerboseMock: Mock<() => boolean>; voiceRuntimeModuleLoadedMock: Mock<() => void>; }; export function baseDiscordAccountConfig() { return { commands: { native: true, nativeSkills: false }, voice: { enabled: false }, agentComponents: { enabled: false }, execApprovals: { enabled: false }, }; } const providerMonitorTestMocks: ProviderMonitorTestMocks = vi.hoisted(() => { const createdBindingManagers: Array<{ stop: ReturnType }> = []; const isVerboseMock = vi.fn(() => false); const shouldLogVerboseMock = vi.fn(() => false); return { clientHandleDeployRequestMock: vi.fn(async () => undefined), clientFetchUserMock: vi.fn(async (_target: string) => ({ id: "bot-1" })), clientGetPluginMock: vi.fn<(_name: string) => unknown>(() => undefined), clientConstructorOptionsMock: vi.fn(), createDiscordAutoPresenceControllerMock: vi.fn(() => ({ enabled: false, start: vi.fn(), stop: vi.fn(), refresh: vi.fn(), runNow: vi.fn(), })), createDiscordNativeCommandMock: vi.fn((params?: { command?: { name?: string } }) => ({ name: params?.command?.name ?? "mock-command", })), createDiscordMessageHandlerMock: vi.fn(() => Object.assign( vi.fn(async () => undefined), { deactivate: vi.fn(), }, ), ), createNoopThreadBindingManagerMock: vi.fn(() => { const manager = { stop: vi.fn() }; createdBindingManagers.push(manager); return manager; }), createThreadBindingManagerMock: vi.fn(() => { const manager = { stop: vi.fn() }; createdBindingManagers.push(manager); return manager; }), reconcileAcpThreadBindingsOnStartupMock: vi.fn(() => ({ checked: 0, removed: 0, staleSessionKeys: [], })), createdBindingManagers, getAcpSessionStatusMock: vi.fn( async (_params: { cfg: OpenClawConfig; sessionKey: string; signal?: AbortSignal }) => ({ state: "idle", }), ), getPluginCommandSpecsMock: vi.fn<() => PluginCommandSpecMock[]>(() => []), listNativeCommandSpecsForConfigMock: vi.fn<() => NativeCommandSpecMock[]>(() => [ { name: "cmd", description: "built-in", acceptsArgs: false }, ]), listSkillCommandsForAgentsMock: vi.fn(() => []), monitorLifecycleMock: vi.fn(async (params: { threadBindings: { stop: () => void } }) => { params.threadBindings.stop(); }), resolveDiscordAccountMock: vi.fn(() => ({ accountId: "default", token: "cfg-token", config: baseDiscordAccountConfig(), })), resolveDiscordAllowlistConfigMock: vi.fn(async () => ({ guildEntries: undefined, allowFrom: undefined, })), resolveNativeCommandsEnabledMock: vi.fn(() => true), resolveNativeSkillsEnabledMock: vi.fn(() => false), isVerboseMock, shouldLogVerboseMock, voiceRuntimeModuleLoadedMock: vi.fn(), }; }); const { clientHandleDeployRequestMock, clientFetchUserMock, clientGetPluginMock, clientConstructorOptionsMock, createDiscordAutoPresenceControllerMock, createDiscordNativeCommandMock, createDiscordMessageHandlerMock, createNoopThreadBindingManagerMock, createThreadBindingManagerMock, reconcileAcpThreadBindingsOnStartupMock, createdBindingManagers, getAcpSessionStatusMock, getPluginCommandSpecsMock, listNativeCommandSpecsForConfigMock, listSkillCommandsForAgentsMock, monitorLifecycleMock, resolveDiscordAccountMock, resolveDiscordAllowlistConfigMock, resolveNativeCommandsEnabledMock, resolveNativeSkillsEnabledMock, isVerboseMock, shouldLogVerboseMock, voiceRuntimeModuleLoadedMock, } = providerMonitorTestMocks; export function getProviderMonitorTestMocks(): typeof providerMonitorTestMocks { return providerMonitorTestMocks; } export function mockResolvedDiscordAccountConfig(overrides: Record) { resolveDiscordAccountMock.mockImplementation(() => ({ accountId: "default", token: "cfg-token", config: { ...baseDiscordAccountConfig(), ...overrides, }, })); } export function getFirstDiscordMessageHandlerParams() { expect(createDiscordMessageHandlerMock).toHaveBeenCalledTimes(1); const firstCall = createDiscordMessageHandlerMock.mock.calls.at(0) as [T] | undefined; return firstCall?.[0]; } export function resetDiscordProviderMonitorMocks(params?: { nativeCommands?: NativeCommandSpecMock[]; }) { clientHandleDeployRequestMock.mockClear().mockResolvedValue(undefined); clientFetchUserMock.mockClear().mockResolvedValue({ id: "bot-1" }); clientGetPluginMock.mockClear().mockReturnValue(undefined); clientConstructorOptionsMock.mockClear(); createDiscordAutoPresenceControllerMock.mockClear().mockImplementation(() => ({ enabled: false, start: vi.fn(), stop: vi.fn(), refresh: vi.fn(), runNow: vi.fn(), })); createDiscordNativeCommandMock.mockClear().mockImplementation((input) => ({ name: input?.command?.name ?? "mock-command", })); createDiscordMessageHandlerMock.mockClear().mockImplementation(() => Object.assign( vi.fn(async () => undefined), { deactivate: vi.fn(), }, ), ); createNoopThreadBindingManagerMock.mockClear(); createThreadBindingManagerMock.mockClear(); reconcileAcpThreadBindingsOnStartupMock.mockClear().mockReturnValue({ checked: 0, removed: 0, staleSessionKeys: [], }); createdBindingManagers.length = 0; getAcpSessionStatusMock.mockClear().mockResolvedValue({ state: "idle" }); getPluginCommandSpecsMock.mockClear().mockReturnValue([]); listNativeCommandSpecsForConfigMock .mockClear() .mockReturnValue( params?.nativeCommands ?? [{ name: "cmd", description: "built-in", acceptsArgs: false }], ); listSkillCommandsForAgentsMock.mockClear().mockReturnValue([]); monitorLifecycleMock.mockClear().mockImplementation(async (monitorParams) => { monitorParams.threadBindings.stop(); }); resolveDiscordAccountMock.mockClear().mockReturnValue({ accountId: "default", token: "cfg-token", config: baseDiscordAccountConfig(), }); resolveDiscordAllowlistConfigMock.mockClear().mockResolvedValue({ guildEntries: undefined, allowFrom: undefined, }); resolveNativeCommandsEnabledMock.mockClear().mockReturnValue(true); resolveNativeSkillsEnabledMock.mockClear().mockReturnValue(false); isVerboseMock.mockClear().mockReturnValue(false); shouldLogVerboseMock.mockClear().mockReturnValue(false); voiceRuntimeModuleLoadedMock.mockClear(); } export const baseRuntime = (): RuntimeEnv => ({ log: vi.fn(), error: vi.fn(), exit: vi.fn(), }); export const baseConfig = (): OpenClawConfig => ({ channels: { discord: { accounts: { default: {}, }, }, }, }) as OpenClawConfig; vi.mock("@buape/carbon", () => { class Command {} class ReadyListener {} class RateLimitError extends Error { status = 429; discordCode?: number; retryAfter: number; scope: string | null; bucket: string | null; constructor( response: Response, body: { message: string; retry_after: number; global: boolean }, ) { super(body.message); this.retryAfter = body.retry_after; this.scope = body.global ? "global" : response.headers.get("X-RateLimit-Scope"); this.bucket = response.headers.get("X-RateLimit-Bucket"); } } class Client { listeners: unknown[]; rest: { put: ReturnType }; options: unknown; constructor(options: unknown, handlers: { listeners?: unknown[] }) { this.options = options; this.listeners = handlers.listeners ?? []; this.rest = { put: vi.fn(async () => undefined) }; clientConstructorOptionsMock(options); } async handleDeployRequest() { return await clientHandleDeployRequestMock(); } async fetchUser(target: string) { return await clientFetchUserMock(target); } getPlugin(name: string) { return clientGetPluginMock(name); } } return { Client, Command, RateLimitError, ReadyListener }; }); vi.mock("@buape/carbon/gateway", () => ({ GatewayCloseCodes: { DisallowedIntents: 4014 }, })); vi.mock("@buape/carbon/voice", () => ({ VoicePlugin: class VoicePlugin {}, })); vi.mock("openclaw/plugin-sdk/acp-runtime", async () => { const actual = await vi.importActual( "openclaw/plugin-sdk/acp-runtime", ); return { ...actual, getAcpSessionManager: () => ({ getSessionStatus: getAcpSessionStatusMock, }), isAcpRuntimeError: (error: unknown): error is { code: string } => error instanceof Error && "code" in error, }; }); vi.mock("openclaw/plugin-sdk/reply-runtime", async () => { const actual = await vi.importActual( "openclaw/plugin-sdk/reply-runtime", ); return { ...actual, resolveTextChunkLimit: () => 2000, listNativeCommandSpecsForConfig: listNativeCommandSpecsForConfigMock, listSkillCommandsForAgents: listSkillCommandsForAgentsMock, }; }); vi.mock("openclaw/plugin-sdk/config-runtime", async () => { const actual = await vi.importActual( "openclaw/plugin-sdk/config-runtime", ); return { ...actual, isNativeCommandsExplicitlyDisabled: () => false, loadConfig: () => ({}), resolveNativeCommandsEnabled: resolveNativeCommandsEnabledMock, resolveNativeSkillsEnabled: resolveNativeSkillsEnabledMock, }; }); vi.mock("openclaw/plugin-sdk/runtime-env", async () => { const actual = await vi.importActual( "openclaw/plugin-sdk/runtime-env", ); return { ...actual, danger: (value: string) => value, isVerbose: isVerboseMock, logVerbose: vi.fn(), shouldLogVerbose: shouldLogVerboseMock, warn: (value: string) => value, createSubsystemLogger: () => { const logger = { child: vi.fn(() => logger), info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), }; return logger; }, createNonExitingRuntime: () => ({ log: vi.fn(), error: vi.fn(), exit: vi.fn() }), }; }); vi.mock("openclaw/plugin-sdk/infra-runtime", async () => { const actual = await vi.importActual( "openclaw/plugin-sdk/infra-runtime", ); return { ...actual, formatErrorMessage: (error: unknown) => String(error), }; }); vi.mock("../../../extensions/discord/src/accounts.js", () => ({ resolveDiscordAccount: resolveDiscordAccountMock, })); vi.mock("../../../extensions/discord/src/probe.js", () => ({ fetchDiscordApplicationId: async () => "app-1", })); vi.mock("../../../extensions/discord/src/token.js", () => ({ normalizeDiscordToken: (value?: string) => value, })); vi.mock("../../../extensions/discord/src/voice/command.js", () => ({ createDiscordVoiceCommand: () => ({ name: "voice-command" }), })); vi.mock("../../../extensions/discord/src/monitor/agent-components.js", () => ({ createAgentComponentButton: () => ({ id: "btn" }), createAgentSelectMenu: () => ({ id: "menu" }), createDiscordComponentButton: () => ({ id: "btn2" }), createDiscordComponentChannelSelect: () => ({ id: "channel" }), createDiscordComponentMentionableSelect: () => ({ id: "mentionable" }), createDiscordComponentModal: () => ({ id: "modal" }), createDiscordComponentRoleSelect: () => ({ id: "role" }), createDiscordComponentStringSelect: () => ({ id: "string" }), createDiscordComponentUserSelect: () => ({ id: "user" }), })); vi.mock("../../../extensions/discord/src/monitor/auto-presence.js", () => ({ createDiscordAutoPresenceController: createDiscordAutoPresenceControllerMock, })); vi.mock("../../../extensions/discord/src/monitor/commands.js", () => ({ resolveDiscordSlashCommandConfig: () => ({ ephemeral: false }), })); vi.mock("../../../extensions/discord/src/monitor/exec-approvals.js", () => ({ createExecApprovalButton: () => ({ id: "exec-approval" }), DiscordExecApprovalHandler: class DiscordExecApprovalHandler { async start() { return undefined; } async stop() { return undefined; } }, })); vi.mock("../../../extensions/discord/src/monitor/gateway-plugin.js", () => ({ createDiscordGatewayPlugin: () => ({ id: "gateway-plugin" }), })); vi.mock("../../../extensions/discord/src/monitor/listeners.js", () => ({ DiscordMessageListener: class DiscordMessageListener {}, DiscordPresenceListener: class DiscordPresenceListener {}, DiscordReactionListener: class DiscordReactionListener {}, DiscordReactionRemoveListener: class DiscordReactionRemoveListener {}, DiscordThreadUpdateListener: class DiscordThreadUpdateListener {}, registerDiscordListener: vi.fn(), })); vi.mock("../../../extensions/discord/src/monitor/message-handler.js", () => ({ createDiscordMessageHandler: createDiscordMessageHandlerMock, })); vi.mock("../../../extensions/discord/src/monitor/native-command.js", () => ({ createDiscordCommandArgFallbackButton: () => ({ id: "arg-fallback" }), createDiscordModelPickerFallbackButton: () => ({ id: "model-fallback-btn" }), createDiscordModelPickerFallbackSelect: () => ({ id: "model-fallback-select" }), createDiscordNativeCommand: createDiscordNativeCommandMock, })); vi.mock("../../../extensions/discord/src/monitor/presence.js", () => ({ resolveDiscordPresenceUpdate: () => undefined, })); vi.mock("../../../extensions/discord/src/monitor/provider.allowlist.js", () => ({ resolveDiscordAllowlistConfig: resolveDiscordAllowlistConfigMock, })); vi.mock("../../../extensions/discord/src/monitor/provider.lifecycle.js", () => ({ runDiscordGatewayLifecycle: monitorLifecycleMock, })); vi.mock("../../../extensions/discord/src/monitor/rest-fetch.js", () => ({ resolveDiscordRestFetch: () => async () => undefined, })); vi.mock("../../../extensions/discord/src/monitor/thread-bindings.js", () => ({ createNoopThreadBindingManager: createNoopThreadBindingManagerMock, createThreadBindingManager: createThreadBindingManagerMock, reconcileAcpThreadBindingsOnStartup: reconcileAcpThreadBindingsOnStartupMock, }));