diff --git a/extensions/discord/src/accounts.ts b/extensions/discord/src/accounts.ts index ea28be7fb0d..49193f5fabf 100644 --- a/extensions/discord/src/accounts.ts +++ b/extensions/discord/src/accounts.ts @@ -1,8 +1,10 @@ import { createAccountActionGate, createAccountListHelpers, - normalizeAccountId, - resolveAccountEntry, +} from "openclaw/plugin-sdk/account-helpers"; +import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +import { resolveAccountEntry } from "openclaw/plugin-sdk/routing"; +import { type OpenClawConfig, type DiscordAccountConfig, type DiscordActionConfig, diff --git a/extensions/discord/src/audit.test.ts b/extensions/discord/src/audit.test.ts index ffa7b370c5a..36995eabc4f 100644 --- a/extensions/discord/src/audit.test.ts +++ b/extensions/discord/src/audit.test.ts @@ -1,9 +1,13 @@ 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, + addRoleDiscord: vi.fn(), + fetchChannelPermissionsDiscord: vi.fn(), + }; +}); describe("discord audit", () => { it("collects numeric channel ids and counts unresolved keys", async () => { diff --git a/extensions/discord/src/monitor/message-handler.process.test.ts b/extensions/discord/src/monitor/message-handler.process.test.ts index e419706b30b..fb0f0311a04 100644 --- a/extensions/discord/src/monitor/message-handler.process.test.ts +++ b/extensions/discord/src/monitor/message-handler.process.test.ts @@ -67,11 +67,15 @@ 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, + addRoleDiscord: vi.fn(), + reactMessageDiscord: sendMocks.reactMessageDiscord, + removeReactionDiscord: sendMocks.removeReactionDiscord, + }; +}); vi.mock("../send.messages.js", () => ({ editMessageDiscord: deliveryMocks.editMessageDiscord, diff --git a/extensions/discord/src/monitor/provider.registry.test.ts b/extensions/discord/src/monitor/provider.registry.test.ts deleted file mode 100644 index 5e092445065..00000000000 --- a/extensions/discord/src/monitor/provider.registry.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { clearPluginCommands, registerPluginCommand } from "../../../../src/plugins/commands.js"; -import { - baseConfig, - baseRuntime, - getProviderMonitorTestMocks, - resetDiscordProviderMonitorMocks, -} from "../../../../test/helpers/extensions/discord-provider.test-support.js"; - -const { createDiscordNativeCommandMock, clientHandleDeployRequestMock, monitorLifecycleMock } = - getProviderMonitorTestMocks(); - -describe("monitorDiscordProvider real plugin registry", () => { - beforeEach(() => { - clearPluginCommands(); - resetDiscordProviderMonitorMocks({ - nativeCommands: [{ name: "status", description: "Status", acceptsArgs: false }], - }); - }); - - it("registers plugin commands from the real registry as native Discord commands", async () => { - expect( - registerPluginCommand("demo-plugin", { - name: "pair", - description: "Pair device", - acceptsArgs: true, - requireAuth: false, - handler: async ({ args }) => ({ text: `paired:${args ?? ""}` }), - }), - ).toEqual({ ok: true }); - - const { monitorDiscordProvider } = await import("./provider.js"); - - await monitorDiscordProvider({ - config: baseConfig(), - runtime: baseRuntime(), - }); - - const commandNames = (createDiscordNativeCommandMock.mock.calls as Array) - .map((call) => (call[0] as { command?: { name?: string } } | undefined)?.command?.name) - .filter((value): value is string => typeof value === "string"); - - expect(commandNames).toContain("status"); - expect(commandNames).toContain("pair"); - expect(clientHandleDeployRequestMock).toHaveBeenCalledTimes(1); - expect(monitorLifecycleMock).toHaveBeenCalledTimes(1); - }); -}); diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index 0e7780374b5..23c4b394379 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -468,6 +468,43 @@ describe("monitorDiscordProvider", () => { expect(commandNames).toContain("cron_jobs"); }); + it("registers plugin commands from the real registry as native Discord commands", async () => { + const { clearPluginCommands, getPluginCommandSpecs, registerPluginCommand } = + await import("../../../../src/plugins/commands.js"); + clearPluginCommands(); + const { monitorDiscordProvider } = await import("./provider.js"); + listNativeCommandSpecsForConfigMock.mockReturnValue([ + { name: "status", description: "Status", acceptsArgs: false }, + ]); + getPluginCommandSpecsMock.mockImplementation((provider?: string) => + getPluginCommandSpecs(provider), + ); + + expect( + registerPluginCommand("demo-plugin", { + name: "pair", + description: "Pair device", + acceptsArgs: true, + requireAuth: false, + handler: async ({ args }) => ({ text: `paired:${args ?? ""}` }), + }), + ).toEqual({ ok: true }); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + const commandNames = (createDiscordNativeCommandMock.mock.calls as Array) + .map((call) => (call[0] as { command?: { name?: string } } | undefined)?.command?.name) + .filter((value): value is string => typeof value === "string"); + + expect(commandNames).toContain("status"); + expect(commandNames).toContain("pair"); + expect(clientHandleDeployRequestMock).toHaveBeenCalledTimes(1); + expect(monitorLifecycleMock).toHaveBeenCalledTimes(1); + }); + it("continues startup when Discord daily slash-command create quota is exhausted", async () => { const { RateLimitError } = await import("@buape/carbon"); const { monitorDiscordProvider } = await import("./provider.js"); 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..51ae59de906 100644 --- a/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts @@ -24,11 +24,15 @@ vi.mock("../client.js", () => ({ 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, + addRoleDiscord: vi.fn(), + 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..82249d3fe7b 100644 --- a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts @@ -41,11 +41,15 @@ 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, + addRoleDiscord: vi.fn(), + sendMessageDiscord: hoisted.sendMessageDiscord, + sendWebhookMessageDiscord: hoisted.sendWebhookMessageDiscord, + }; +}); vi.mock("../send.messages.js", () => ({ createThreadDiscord: hoisted.createThreadDiscord, diff --git a/extensions/discord/src/runtime-api.ts b/extensions/discord/src/runtime-api.ts index 637aebb2cb1..2357a477e76 100644 --- a/extensions/discord/src/runtime-api.ts +++ b/extensions/discord/src/runtime-api.ts @@ -1,12 +1,10 @@ export { buildComputedAccountStatusSnapshot, buildTokenChannelStatusSummary, - listDiscordDirectoryGroupsFromConfig, - listDiscordDirectoryPeersFromConfig, PAIRING_APPROVED_MESSAGE, projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses, -} from "openclaw/plugin-sdk/discord"; +} from "openclaw/plugin-sdk/channel-runtime"; export { buildChannelConfigSchema, getChatChannelMeta, @@ -37,10 +35,9 @@ export { export { createAccountActionGate, createAccountListHelpers, - DEFAULT_ACCOUNT_ID, - normalizeAccountId, - resolveAccountEntry, -} from "openclaw/plugin-sdk/account-resolution"; +} 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, diff --git a/extensions/discord/src/send.creates-thread.test.ts b/extensions/discord/src/send.creates-thread.test.ts index c1012816d22..6c0818db2ab 100644 --- a/extensions/discord/src/send.creates-thread.test.ts +++ b/extensions/discord/src/send.creates-thread.test.ts @@ -1,5 +1,6 @@ import { RateLimitError } from "@buape/carbon"; import { ChannelType, Routes } from "discord-api-types/v10"; +import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { addRoleDiscord, @@ -18,7 +19,7 @@ import { } from "./send.js"; import { makeDiscordRest } from "./send.test-harness.js"; -vi.mock("../../whatsapp/src/media.js", async () => { +vi.mock("openclaw/plugin-sdk/web-media", async () => { const { discordWebMediaMockFactory } = await import("./send.test-harness.js"); return discordWebMediaMockFactory(); }); @@ -288,6 +289,7 @@ describe("uploadEmojiDiscord", () => { }, }), ); + expect(loadWebMediaRaw).toHaveBeenCalledWith("file:///tmp/party.png", 256 * 1024); }); }); @@ -325,6 +327,7 @@ describe("uploadStickerDiscord", () => { }, }), ); + expect(loadWebMediaRaw).toHaveBeenCalledWith("file:///tmp/wave.png", 512 * 1024); }); }); diff --git a/extensions/discord/src/send.sends-basic-channel-messages.test.ts b/extensions/discord/src/send.sends-basic-channel-messages.test.ts index 7d0f359f90a..54c45c6f483 100644 --- a/extensions/discord/src/send.sends-basic-channel-messages.test.ts +++ b/extensions/discord/src/send.sends-basic-channel-messages.test.ts @@ -1,6 +1,6 @@ import { ChannelType, PermissionFlagsBits, Routes } from "discord-api-types/v10"; +import { loadWebMedia } from "openclaw/plugin-sdk/web-media"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { loadWebMedia } from "../../whatsapp/src/media.js"; import { __resetDiscordDirectoryCacheForTest, rememberDiscordDirectoryUser, @@ -21,7 +21,7 @@ import { } from "./send.js"; import { makeDiscordRest } from "./send.test-harness.js"; -vi.mock("../../whatsapp/src/media.js", async () => { +vi.mock("openclaw/plugin-sdk/web-media", async () => { const { discordWebMediaMockFactory } = await import("./send.test-harness.js"); return discordWebMediaMockFactory(); }); diff --git a/src/plugin-sdk/account-helpers.ts b/src/plugin-sdk/account-helpers.ts index 5055e80571a..0ad90ae9ad3 100644 --- a/src/plugin-sdk/account-helpers.ts +++ b/src/plugin-sdk/account-helpers.ts @@ -1 +1,2 @@ export { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; +export { createAccountActionGate } from "../channels/plugins/account-action-gate.js"; diff --git a/src/plugin-sdk/channel-runtime.ts b/src/plugin-sdk/channel-runtime.ts index dfbbad1e854..b45315a6757 100644 --- a/src/plugin-sdk/channel-runtime.ts +++ b/src/plugin-sdk/channel-runtime.ts @@ -34,6 +34,7 @@ export type * from "../channels/plugins/types.js"; export * from "../channels/plugins/config-writes.js"; export * from "../channels/plugins/directory-adapters.js"; export * from "../channels/plugins/media-payload.js"; +export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; export * from "./message-tool-schema.js"; export * from "../channels/plugins/normalize/signal.js"; export * from "../channels/plugins/normalize/whatsapp.js"; @@ -45,6 +46,14 @@ export * from "../channels/plugins/target-resolvers.js"; export * from "../channels/plugins/threading-helpers.js"; export * from "../channels/plugins/status-issues/shared.js"; export * from "../channels/plugins/whatsapp-heartbeat.js"; +export { + buildComputedAccountStatusSnapshot, + buildTokenChannelStatusSummary, +} from "./status-helpers.js"; +export { + projectCredentialSnapshotFields, + resolveConfiguredFromCredentialStatuses, +} from "../channels/account-snapshot-fields.js"; export * from "../infra/outbound/send-deps.js"; export * from "../polls.js"; export * from "../utils/message-channel.js";