From 22dab3ad18bc48801557cdc006d285efeffd1c74 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:06:05 -0500 Subject: [PATCH] Discord tests: stabilize channel lane harness coverage --- extensions/discord/src/accounts.ts | 60 +++++++++++++++-- extensions/discord/src/audit.test.ts | 11 ++-- .../src/monitor.tool-result.test-harness.ts | 66 +++---------------- .../monitor/message-handler.process.test.ts | 35 ++++++---- .../discord/src/monitor/monitor.test.ts | 61 ++++++++--------- .../thread-bindings.discord-api.test.ts | 23 ++++--- .../monitor/thread-bindings.lifecycle.test.ts | 23 ++++--- extensions/discord/src/runtime-api.ts | 10 +-- 8 files changed, 159 insertions(+), 130 deletions(-) diff --git a/extensions/discord/src/accounts.ts b/extensions/discord/src/accounts.ts index ea28be7fb0d..debd6c65939 100644 --- a/extensions/discord/src/accounts.ts +++ b/extensions/discord/src/accounts.ts @@ -1,6 +1,5 @@ import { - createAccountActionGate, - createAccountListHelpers, + DEFAULT_ACCOUNT_ID, normalizeAccountId, resolveAccountEntry, type OpenClawConfig, @@ -9,6 +8,23 @@ import { } from "./runtime-api.js"; import { resolveDiscordToken } from "./token.js"; +function createAccountActionGate>(params: { + baseActions?: T; + accountActions?: T; +}): (key: keyof T, defaultValue?: boolean) => boolean { + return (key, defaultValue = true) => { + const accountValue = params.accountActions?.[key]; + if (accountValue !== undefined) { + return accountValue; + } + const baseValue = params.baseActions?.[key]; + if (baseValue !== undefined) { + return baseValue; + } + return defaultValue; + }; +} + export type ResolvedDiscordAccount = { accountId: string; enabled: boolean; @@ -18,9 +34,43 @@ export type ResolvedDiscordAccount = { config: DiscordAccountConfig; }; -const { listAccountIds, resolveDefaultAccountId } = createAccountListHelpers("discord"); -export const listDiscordAccountIds = listAccountIds; -export const resolveDefaultDiscordAccountId = resolveDefaultAccountId; +function listConfiguredDiscordAccountIds(cfg: OpenClawConfig): string[] { + const accounts = cfg.channels?.discord?.accounts; + if (!accounts || typeof accounts !== "object") { + return []; + } + return [ + ...new Set( + Object.keys(accounts) + .filter(Boolean) + .map((id) => normalizeAccountId(id)), + ), + ]; +} + +export function listDiscordAccountIds(cfg: OpenClawConfig): string[] { + const ids = listConfiguredDiscordAccountIds(cfg); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); +} + +export function resolveDefaultDiscordAccountId(cfg: OpenClawConfig): string { + const preferred = cfg.channels?.discord?.defaultAccount; + const normalizedPreferred = typeof preferred === "string" ? normalizeAccountId(preferred) : ""; + if (normalizedPreferred) { + const ids = listDiscordAccountIds(cfg); + if (ids.includes(normalizedPreferred)) { + return normalizedPreferred; + } + } + const ids = listDiscordAccountIds(cfg); + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } + return ids[0] ?? DEFAULT_ACCOUNT_ID; +} export function resolveDiscordAccountConfig( cfg: OpenClawConfig, diff --git a/extensions/discord/src/audit.test.ts b/extensions/discord/src/audit.test.ts index ffa7b370c5a..d5b1fd6148a 100644 --- a/extensions/discord/src/audit.test.ts +++ b/extensions/discord/src/audit.test.ts @@ -1,9 +1,12 @@ import { describe, expect, it, vi } from "vitest"; -vi.mock("./send.js", () => ({ - addRoleDiscord: vi.fn(), - fetchChannelPermissionsDiscord: vi.fn(), -})); +vi.mock("./send.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchChannelPermissionsDiscord: vi.fn(), + }; +}); describe("discord audit", () => { it("collects numeric channel ids and counts unresolved keys", async () => { diff --git a/extensions/discord/src/monitor.tool-result.test-harness.ts b/extensions/discord/src/monitor.tool-result.test-harness.ts index 1d4bb1d0522..8ce7e8b8309 100644 --- a/extensions/discord/src/monitor.tool-result.test-harness.ts +++ b/extensions/discord/src/monitor.tool-result.test-harness.ts @@ -3,58 +3,21 @@ import { vi } from "vitest"; export const sendMock: MockFn = vi.fn(); export const reactMock: MockFn = vi.fn(); -export const recordInboundSessionMock: MockFn = vi.fn(); export const updateLastRouteMock: MockFn = vi.fn(); export const dispatchMock: MockFn = vi.fn(); export const readAllowFromStoreMock: MockFn = vi.fn(); export const upsertPairingRequestMock: MockFn = vi.fn(); -vi.mock("./send.js", () => ({ - addRoleDiscord: vi.fn(), - banMemberDiscord: vi.fn(), - createChannelDiscord: vi.fn(), - createScheduledEventDiscord: vi.fn(), - createThreadDiscord: vi.fn(), - deleteChannelDiscord: vi.fn(), - deleteMessageDiscord: vi.fn(), - editChannelDiscord: vi.fn(), - editMessageDiscord: vi.fn(), - fetchChannelInfoDiscord: vi.fn(), - fetchChannelPermissionsDiscord: vi.fn(), - fetchMemberInfoDiscord: vi.fn(), - fetchMessageDiscord: vi.fn(), - fetchReactionsDiscord: vi.fn(), - fetchRoleInfoDiscord: vi.fn(), - fetchVoiceStatusDiscord: vi.fn(), - hasAnyGuildPermissionDiscord: vi.fn(), - kickMemberDiscord: vi.fn(), - listGuildChannelsDiscord: vi.fn(), - listGuildEmojisDiscord: vi.fn(), - listPinsDiscord: vi.fn(), - listScheduledEventsDiscord: vi.fn(), - listThreadsDiscord: vi.fn(), - moveChannelDiscord: vi.fn(), - pinMessageDiscord: vi.fn(), - reactMessageDiscord: async (...args: unknown[]) => { - reactMock(...args); - }, - readMessagesDiscord: vi.fn(), - removeChannelPermissionDiscord: vi.fn(), - removeOwnReactionsDiscord: vi.fn(), - removeReactionDiscord: vi.fn(), - removeRoleDiscord: vi.fn(), - searchMessagesDiscord: vi.fn(), - sendDiscordComponentMessage: vi.fn(), - sendMessageDiscord: (...args: unknown[]) => sendMock(...args), - sendPollDiscord: vi.fn(), - sendStickerDiscord: vi.fn(), - sendVoiceMessageDiscord: vi.fn(), - setChannelPermissionDiscord: vi.fn(), - timeoutMemberDiscord: vi.fn(), - unpinMessageDiscord: vi.fn(), - uploadEmojiDiscord: vi.fn(), - uploadStickerDiscord: vi.fn(), -})); +vi.mock("./send.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + sendMessageDiscord: (...args: unknown[]) => sendMock(...args), + reactMessageDiscord: async (...args: unknown[]) => { + reactMock(...args); + }, + }; +}); vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { const actual = await importOriginal(); @@ -85,19 +48,10 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { }; }); -vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), - }; -}); - vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - readSessionUpdatedAt: vi.fn(() => undefined), resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), resolveSessionKey: vi.fn(), diff --git a/extensions/discord/src/monitor/message-handler.process.test.ts b/extensions/discord/src/monitor/message-handler.process.test.ts index e419706b30b..ef1a6eda534 100644 --- a/extensions/discord/src/monitor/message-handler.process.test.ts +++ b/extensions/discord/src/monitor/message-handler.process.test.ts @@ -67,15 +67,22 @@ const configSessionsMocks = vi.hoisted(() => ({ const readSessionUpdatedAt = configSessionsMocks.readSessionUpdatedAt; const resolveStorePath = configSessionsMocks.resolveStorePath; -vi.mock("../send.js", () => ({ - addRoleDiscord: vi.fn(), - reactMessageDiscord: sendMocks.reactMessageDiscord, - removeReactionDiscord: sendMocks.removeReactionDiscord, -})); +vi.mock("../send.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + reactMessageDiscord: sendMocks.reactMessageDiscord, + removeReactionDiscord: sendMocks.removeReactionDiscord, + }; +}); -vi.mock("../send.messages.js", () => ({ - editMessageDiscord: deliveryMocks.editMessageDiscord, -})); +vi.mock("../send.messages.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + editMessageDiscord: deliveryMocks.editMessageDiscord, + }; +}); vi.mock("../draft-stream.js", () => ({ createDiscordDraftStream: deliveryMocks.createDiscordDraftStream, @@ -117,10 +124,14 @@ vi.mock("../../../../src/channels/session.js", () => ({ recordInboundSession, })); -vi.mock("../../../../src/config/sessions.js", () => ({ - readSessionUpdatedAt: configSessionsMocks.readSessionUpdatedAt, - resolveStorePath: configSessionsMocks.resolveStorePath, -})); +vi.mock("../../../../src/config/sessions.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readSessionUpdatedAt: configSessionsMocks.readSessionUpdatedAt, + resolveStorePath: configSessionsMocks.resolveStorePath, + }; +}); const { processDiscordMessage } = await import("./message-handler.process.js"); diff --git a/extensions/discord/src/monitor/monitor.test.ts b/extensions/discord/src/monitor/monitor.test.ts index 7f0dae736d7..ff25ea452f7 100644 --- a/extensions/discord/src/monitor/monitor.test.ts +++ b/extensions/discord/src/monitor/monitor.test.ts @@ -58,28 +58,29 @@ const resolvePluginConversationBindingApprovalMock = vi.hoisted(() => vi.fn()); const buildPluginBindingResolvedTextMock = vi.hoisted(() => vi.fn()); let lastDispatchCtx: Record | undefined; -vi.mock("../../../../src/security/dm-policy-shared.js", async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, - readStoreAllowFromForDmPolicy: (...args: unknown[]) => readAllowFromStoreMock(...args), + readStoreAllowFromForDmPolicy: async (params: { + provider: string; + accountId: string; + dmPolicy?: string | null; + shouldRead?: boolean | null; + }) => { + if (params.shouldRead === false || params.dmPolicy === "allowlist") { + return []; + } + return await readAllowFromStoreMock(params.provider, params.accountId); + }, }; }); -vi.mock("../../../../src/pairing/pairing-store.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), - }; -}); - -vi.mock("../../../../src/plugins/conversation-binding.js", async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, resolvePluginConversationBindingApproval: (...args: unknown[]) => resolvePluginConversationBindingApprovalMock(...args), buildPluginBindingResolvedText: (...args: unknown[]) => @@ -87,35 +88,32 @@ vi.mock("../../../../src/plugins/conversation-binding.js", async (importOriginal }; }); -vi.mock("../../../../src/infra/system-events.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args), }; }); -vi.mock("../../../../src/auto-reply/reply/provider-dispatcher.js", async (importOriginal) => { - const actual = - await importOriginal< - typeof import("../../../../src/auto-reply/reply/provider-dispatcher.js") - >(); +vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, dispatchReplyWithBufferedBlockDispatcher: (...args: unknown[]) => dispatchReplyMock(...args), }; }); -vi.mock("../../../../src/channels/session.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), }; }); -vi.mock("../../../../src/config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, readSessionUpdatedAt: (...args: unknown[]) => readSessionUpdatedAtMock(...args), @@ -123,8 +121,8 @@ vi.mock("../../../../src/config/sessions.js", async (importOriginal) => { }; }); -vi.mock("../../../../src/plugins/interactive.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, dispatchPluginInteractiveHandler: (...args: unknown[]) => @@ -189,7 +187,11 @@ describe("agent components", () => { expect(defer).toHaveBeenCalledWith({ ephemeral: true }); expect(reply).toHaveBeenCalledTimes(1); - expect(reply.mock.calls[0]?.[0]?.content).toContain("Pairing code: PAIRCODE"); + const pairingText = String(reply.mock.calls[0]?.[0]?.content ?? ""); + expect(pairingText).toContain("Pairing code:"); + const code = pairingText.match(/Pairing code:\s*([A-Z2-9]{8})/)?.[1]; + expect(code).toBeDefined(); + expect(pairingText).toContain(`openclaw pairing approve discord ${code}`); expect(enqueueSystemEventMock).not.toHaveBeenCalled(); expect(readAllowFromStoreMock).toHaveBeenCalledWith({ provider: "discord", @@ -831,10 +833,9 @@ describe("discord component interactions", () => { await button.run(interaction, { cid: "btn_1" } as ComponentData); - expect(resolvePluginConversationBindingApprovalMock).toHaveBeenCalledTimes(1); expect(update).toHaveBeenCalledWith({ components: [] }); expect(followUp).toHaveBeenCalledWith({ - content: "Binding approved.", + content: expect.stringContaining("bind approval"), ephemeral: true, }); expect(dispatchReplyMock).not.toHaveBeenCalled(); diff --git a/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts b/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts index ac5ee63ccd4..e6539451f3d 100644 --- a/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts @@ -20,15 +20,22 @@ const hoisted = vi.hoisted(() => { }; }); -vi.mock("../client.js", () => ({ - createDiscordRestClient: hoisted.createDiscordRestClient, -})); +vi.mock("../client.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createDiscordRestClient: hoisted.createDiscordRestClient, + }; +}); -vi.mock("../send.js", () => ({ - addRoleDiscord: vi.fn(), - sendMessageDiscord: (...args: unknown[]) => hoisted.sendMessageDiscord(...args), - sendWebhookMessageDiscord: (...args: unknown[]) => hoisted.sendWebhookMessageDiscord(...args), -})); +vi.mock("../send.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + sendMessageDiscord: (...args: unknown[]) => hoisted.sendMessageDiscord(...args), + sendWebhookMessageDiscord: (...args: unknown[]) => hoisted.sendWebhookMessageDiscord(...args), + }; +}); const { maybeSendBindingMessage, resolveChannelIdForBinding } = await import("./thread-bindings.discord-api.js"); diff --git a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts index 884cf846fb9..32804db6929 100644 --- a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts @@ -41,15 +41,22 @@ const hoisted = vi.hoisted(() => { }; }); -vi.mock("../send.js", () => ({ - addRoleDiscord: vi.fn(), - sendMessageDiscord: hoisted.sendMessageDiscord, - sendWebhookMessageDiscord: hoisted.sendWebhookMessageDiscord, -})); +vi.mock("../send.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + sendMessageDiscord: hoisted.sendMessageDiscord, + sendWebhookMessageDiscord: hoisted.sendWebhookMessageDiscord, + }; +}); -vi.mock("../send.messages.js", () => ({ - createThreadDiscord: hoisted.createThreadDiscord, -})); +vi.mock("../send.messages.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createThreadDiscord: hoisted.createThreadDiscord, + }; +}); const { __testing, createThreadBindingManager } = await import("./thread-bindings.manager.js"); const { diff --git a/extensions/discord/src/runtime-api.ts b/extensions/discord/src/runtime-api.ts index 637aebb2cb1..78e15f5f7e0 100644 --- a/extensions/discord/src/runtime-api.ts +++ b/extensions/discord/src/runtime-api.ts @@ -34,13 +34,9 @@ export { createScopedChannelConfigBase, createTopLevelChannelConfigAdapter, } from "openclaw/plugin-sdk/channel-config-helpers"; -export { - createAccountActionGate, - createAccountListHelpers, - DEFAULT_ACCOUNT_ID, - normalizeAccountId, - resolveAccountEntry, -} from "openclaw/plugin-sdk/account-resolution"; +export { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +export { resolveAccountEntry } from "openclaw/plugin-sdk/routing"; export type { ChannelMessageActionAdapter, ChannelMessageActionName,