diff --git a/src/discord/monitor/message-handler.preflight.acp-bindings.test.ts b/src/discord/monitor/message-handler.preflight.acp-bindings.test.ts index 1d7344ca15f..984c9e4cb20 100644 --- a/src/discord/monitor/message-handler.preflight.acp-bindings.test.ts +++ b/src/discord/monitor/message-handler.preflight.acp-bindings.test.ts @@ -1,4 +1,3 @@ -import { ChannelType } from "@buape/carbon"; import { beforeEach, describe, expect, it, vi } from "vitest"; const ensureConfiguredAcpBindingSessionMock = vi.hoisted(() => vi.fn()); @@ -13,7 +12,13 @@ vi.mock("../../acp/persistent-bindings.js", () => ({ import { __testing as sessionBindingTesting } from "../../infra/outbound/session-binding-service.js"; import { preflightDiscordMessage } from "./message-handler.preflight.js"; -import { createNoopThreadBindingManager } from "./thread-bindings.js"; +import { + createDiscordMessage, + createDiscordPreflightArgs, + createGuildEvent, + createGuildTextClient, + DEFAULT_PREFLIGHT_CFG, +} from "./message-handler.preflight.test-helpers.js"; const GUILD_ID = "guild-1"; const CHANNEL_ID = "channel-1"; @@ -48,70 +53,36 @@ function createConfiguredDiscordBinding() { } function createBasePreflightParams(overrides?: Record) { - const message = { + const message = createDiscordMessage({ id: "m-1", - content: "<@bot-1> hello", - timestamp: new Date().toISOString(), channelId: CHANNEL_ID, - attachments: [], + content: "<@bot-1> hello", mentionedUsers: [{ id: "bot-1" }], - mentionedRoles: [], - mentionedEveryone: false, author: { id: "user-1", bot: false, username: "alice", }, - } as unknown as import("@buape/carbon").Message; - - const client = { - fetchChannel: async (channelId: string) => { - if (channelId === CHANNEL_ID) { - return { - id: CHANNEL_ID, - type: ChannelType.GuildText, - name: "general", - }; - } - return null; - }, - } as unknown as import("@buape/carbon").Client; + }); return { - cfg: { - session: { - mainKey: "main", - scope: "per-sender", - }, - } as import("../../config/config.js").OpenClawConfig, + ...createDiscordPreflightArgs({ + cfg: DEFAULT_PREFLIGHT_CFG, + discordConfig: { + allowBots: true, + } as NonNullable["discord"], + data: createGuildEvent({ + channelId: CHANNEL_ID, + guildId: GUILD_ID, + author: message.author, + message, + }), + client: createGuildTextClient(CHANNEL_ID), + botUserId: "bot-1", + }), discordConfig: { allowBots: true, } as NonNullable["discord"], - accountId: "default", - token: "token", - runtime: {} as import("../../runtime.js").RuntimeEnv, - botUserId: "bot-1", - guildHistories: new Map(), - historyLimit: 0, - mediaMaxBytes: 1_000_000, - textLimit: 2_000, - replyToMode: "all", - dmEnabled: true, - groupDmEnabled: true, - ackReactionScope: "direct", - groupPolicy: "open", - threadBindings: createNoopThreadBindingManager("default"), - data: { - channel_id: CHANNEL_ID, - guild_id: GUILD_ID, - guild: { - id: GUILD_ID, - name: "Guild One", - }, - author: message.author, - message, - } as unknown as import("./listeners.js").DiscordMessageEvent, - client, ...overrides, } satisfies Parameters[0]; } diff --git a/src/discord/monitor/message-handler.preflight.test-helpers.ts b/src/discord/monitor/message-handler.preflight.test-helpers.ts new file mode 100644 index 00000000000..712aec7e187 --- /dev/null +++ b/src/discord/monitor/message-handler.preflight.test-helpers.ts @@ -0,0 +1,103 @@ +import { ChannelType } from "@buape/carbon"; +import type { OpenClawConfig } from "../../config/config.js"; +import type { preflightDiscordMessage } from "./message-handler.preflight.js"; +import { createNoopThreadBindingManager } from "./thread-bindings.js"; + +export type DiscordConfig = NonNullable["discord"]; +export type DiscordMessageEvent = import("./listeners.js").DiscordMessageEvent; +export type DiscordClient = import("@buape/carbon").Client; + +export const DEFAULT_PREFLIGHT_CFG = { + session: { + mainKey: "main", + scope: "per-sender", + }, +} as OpenClawConfig; + +export function createGuildTextClient(channelId: string): DiscordClient { + return { + fetchChannel: async (id: string) => { + if (id === channelId) { + return { + id: channelId, + type: ChannelType.GuildText, + name: "general", + }; + } + return null; + }, + } as unknown as DiscordClient; +} + +export function createGuildEvent(params: { + channelId: string; + guildId: string; + author: import("@buape/carbon").Message["author"]; + message: import("@buape/carbon").Message; +}): DiscordMessageEvent { + return { + channel_id: params.channelId, + guild_id: params.guildId, + guild: { + id: params.guildId, + name: "Guild One", + }, + author: params.author, + message: params.message, + } as unknown as DiscordMessageEvent; +} + +export function createDiscordMessage(params: { + id: string; + channelId: string; + content: string; + author: { + id: string; + bot: boolean; + username?: string; + }; + mentionedUsers?: Array<{ id: string }>; + mentionedEveryone?: boolean; + attachments?: Array>; +}): import("@buape/carbon").Message { + return { + id: params.id, + content: params.content, + timestamp: new Date().toISOString(), + channelId: params.channelId, + attachments: params.attachments ?? [], + mentionedUsers: params.mentionedUsers ?? [], + mentionedRoles: [], + mentionedEveryone: params.mentionedEveryone ?? false, + author: params.author, + } as unknown as import("@buape/carbon").Message; +} + +export function createDiscordPreflightArgs(params: { + cfg: OpenClawConfig; + discordConfig: DiscordConfig; + data: DiscordMessageEvent; + client: DiscordClient; + botUserId?: string; +}): Parameters[0] { + return { + cfg: params.cfg, + discordConfig: params.discordConfig, + accountId: "default", + token: "token", + runtime: {} as import("../../runtime.js").RuntimeEnv, + botUserId: params.botUserId ?? "openclaw-bot", + guildHistories: new Map(), + historyLimit: 0, + mediaMaxBytes: 1_000_000, + textLimit: 2_000, + replyToMode: "all", + dmEnabled: true, + groupDmEnabled: true, + ackReactionScope: "direct", + groupPolicy: "open", + threadBindings: createNoopThreadBindingManager("default"), + data: params.data, + client: params.client, + }; +} diff --git a/src/discord/monitor/message-handler.preflight.test.ts b/src/discord/monitor/message-handler.preflight.test.ts index 1e4d9c5dddb..c90c608e93b 100644 --- a/src/discord/monitor/message-handler.preflight.test.ts +++ b/src/discord/monitor/message-handler.preflight.test.ts @@ -15,25 +15,21 @@ import { resolvePreflightMentionRequirement, shouldIgnoreBoundThreadWebhookMessage, } from "./message-handler.preflight.js"; +import { + createDiscordMessage, + createDiscordPreflightArgs, + createGuildEvent, + createGuildTextClient, + DEFAULT_PREFLIGHT_CFG, + type DiscordClient, + type DiscordConfig, + type DiscordMessageEvent, +} from "./message-handler.preflight.test-helpers.js"; import { __testing as threadBindingTesting, - createNoopThreadBindingManager, createThreadBindingManager, } from "./thread-bindings.js"; -type DiscordConfig = NonNullable< - import("../../config/config.js").OpenClawConfig["channels"] ->["discord"]; -type DiscordMessageEvent = import("./listeners.js").DiscordMessageEvent; -type DiscordClient = import("@buape/carbon").Client; - -const DEFAULT_CFG = { - session: { - mainKey: "main", - scope: "per-sender", - }, -} as import("../../config/config.js").OpenClawConfig; - function createThreadBinding( overrides?: Partial< import("../../infra/outbound/session-binding-service.js").SessionBindingRecord @@ -67,41 +63,7 @@ function createPreflightArgs(params: { data: DiscordMessageEvent; client: DiscordClient; }): Parameters[0] { - return { - cfg: params.cfg, - discordConfig: params.discordConfig, - accountId: "default", - token: "token", - runtime: {} as import("../../runtime.js").RuntimeEnv, - botUserId: "openclaw-bot", - guildHistories: new Map(), - historyLimit: 0, - mediaMaxBytes: 1_000_000, - textLimit: 2_000, - replyToMode: "all", - dmEnabled: true, - groupDmEnabled: true, - ackReactionScope: "direct", - groupPolicy: "open", - threadBindings: createNoopThreadBindingManager("default"), - data: params.data, - client: params.client, - }; -} - -function createGuildTextClient(channelId: string): DiscordClient { - return { - fetchChannel: async (id: string) => { - if (id === channelId) { - return { - id: channelId, - type: ChannelType.GuildText, - name: "general", - }; - } - return null; - }, - } as unknown as DiscordClient; + return createDiscordPreflightArgs(params); } function createThreadClient(params: { threadId: string; parentId: string }): DiscordClient { @@ -128,50 +90,6 @@ function createThreadClient(params: { threadId: string; parentId: string }): Dis } as unknown as DiscordClient; } -function createGuildEvent(params: { - channelId: string; - guildId: string; - author: import("@buape/carbon").Message["author"]; - message: import("@buape/carbon").Message; -}): DiscordMessageEvent { - return { - channel_id: params.channelId, - guild_id: params.guildId, - guild: { - id: params.guildId, - name: "Guild One", - }, - author: params.author, - message: params.message, - } as unknown as DiscordMessageEvent; -} - -function createMessage(params: { - id: string; - channelId: string; - content: string; - author: { - id: string; - bot: boolean; - username?: string; - }; - mentionedUsers?: Array<{ id: string }>; - mentionedEveryone?: boolean; - attachments?: Array>; -}): import("@buape/carbon").Message { - return { - id: params.id, - content: params.content, - timestamp: new Date().toISOString(), - channelId: params.channelId, - attachments: params.attachments ?? [], - mentionedUsers: params.mentionedUsers ?? [], - mentionedRoles: [], - mentionedEveryone: params.mentionedEveryone ?? false, - author: params.author, - } as unknown as import("@buape/carbon").Message; -} - async function runThreadBoundPreflight(params: { threadId: string; parentId: string; @@ -197,7 +115,7 @@ async function runThreadBoundPreflight(params: { return preflightDiscordMessage({ ...createPreflightArgs({ - cfg: DEFAULT_CFG, + cfg: DEFAULT_PREFLIGHT_CFG, discordConfig: params.discordConfig, data: createGuildEvent({ channelId: params.threadId, @@ -223,7 +141,7 @@ async function runGuildPreflight(params: { }) { return preflightDiscordMessage({ ...createPreflightArgs({ - cfg: params.cfg ?? DEFAULT_CFG, + cfg: params.cfg ?? DEFAULT_PREFLIGHT_CFG, discordConfig: params.discordConfig, data: createGuildEvent({ channelId: params.channelId, @@ -237,6 +155,40 @@ async function runGuildPreflight(params: { }); } +async function runMentionOnlyBotPreflight(params: { + channelId: string; + guildId: string; + message: import("@buape/carbon").Message; +}) { + return runGuildPreflight({ + channelId: params.channelId, + guildId: params.guildId, + message: params.message, + discordConfig: { + allowBots: "mentions", + } as DiscordConfig, + }); +} + +async function runIgnoreOtherMentionsPreflight(params: { + channelId: string; + guildId: string; + message: import("@buape/carbon").Message; +}) { + return runGuildPreflight({ + channelId: params.channelId, + guildId: params.guildId, + message: params.message, + discordConfig: {} as DiscordConfig, + guildEntries: { + [params.guildId]: { + requireMention: false, + ignoreOtherMentions: true, + }, + }, + }); +} + describe("resolvePreflightMentionRequirement", () => { it("requires mention when config requires mention and thread is not bound", () => { expect( @@ -279,7 +231,7 @@ describe("preflightDiscordMessage", () => { }); const threadId = "thread-system-1"; const parentId = "channel-parent-1"; - const message = createMessage({ + const message = createDiscordMessage({ id: "m-system-1", channelId: threadId, content: @@ -311,7 +263,7 @@ describe("preflightDiscordMessage", () => { }); const threadId = "thread-bot-regular-1"; const parentId = "channel-parent-regular-1"; - const message = createMessage({ + const message = createDiscordMessage({ id: "m-bot-regular-1", channelId: threadId, content: "here is tool output chunk", @@ -342,7 +294,7 @@ describe("preflightDiscordMessage", () => { const threadId = "thread-bot-focus"; const parentId = "channel-parent-focus"; const client = createThreadClient({ threadId, parentId }); - const message = createMessage({ + const message = createDiscordMessage({ id: "m-bot-1", channelId: threadId, content: "relay message without mention", @@ -363,7 +315,7 @@ describe("preflightDiscordMessage", () => { const result = await preflightDiscordMessage( createPreflightArgs({ cfg: { - ...DEFAULT_CFG, + ...DEFAULT_PREFLIGHT_CFG, } as import("../../config/config.js").OpenClawConfig, discordConfig: { allowBots: true, @@ -386,7 +338,7 @@ describe("preflightDiscordMessage", () => { it("drops bot messages without mention when allowBots=mentions", async () => { const channelId = "channel-bot-mentions-off"; const guildId = "guild-bot-mentions-off"; - const message = createMessage({ + const message = createDiscordMessage({ id: "m-bot-mentions-off", channelId, content: "relay chatter", @@ -397,14 +349,7 @@ describe("preflightDiscordMessage", () => { }, }); - const result = await runGuildPreflight({ - channelId, - guildId, - message, - discordConfig: { - allowBots: "mentions", - } as DiscordConfig, - }); + const result = await runMentionOnlyBotPreflight({ channelId, guildId, message }); expect(result).toBeNull(); }); @@ -412,7 +357,7 @@ describe("preflightDiscordMessage", () => { it("allows bot messages with explicit mention when allowBots=mentions", async () => { const channelId = "channel-bot-mentions-on"; const guildId = "guild-bot-mentions-on"; - const message = createMessage({ + const message = createDiscordMessage({ id: "m-bot-mentions-on", channelId, content: "hi <@openclaw-bot>", @@ -424,14 +369,7 @@ describe("preflightDiscordMessage", () => { }, }); - const result = await runGuildPreflight({ - channelId, - guildId, - message, - discordConfig: { - allowBots: "mentions", - } as DiscordConfig, - }); + const result = await runMentionOnlyBotPreflight({ channelId, guildId, message }); expect(result).not.toBeNull(); }); @@ -439,7 +377,7 @@ describe("preflightDiscordMessage", () => { it("drops guild messages that mention another user when ignoreOtherMentions=true", async () => { const channelId = "channel-other-mention-1"; const guildId = "guild-other-mention-1"; - const message = createMessage({ + const message = createDiscordMessage({ id: "m-other-mention-1", channelId, content: "hello <@999>", @@ -451,18 +389,7 @@ describe("preflightDiscordMessage", () => { }, }); - const result = await runGuildPreflight({ - channelId, - guildId, - message, - discordConfig: {} as DiscordConfig, - guildEntries: { - [guildId]: { - requireMention: false, - ignoreOtherMentions: true, - }, - }, - }); + const result = await runIgnoreOtherMentionsPreflight({ channelId, guildId, message }); expect(result).toBeNull(); }); @@ -470,7 +397,7 @@ describe("preflightDiscordMessage", () => { it("does not drop @everyone messages when ignoreOtherMentions=true", async () => { const channelId = "channel-other-mention-everyone"; const guildId = "guild-other-mention-everyone"; - const message = createMessage({ + const message = createDiscordMessage({ id: "m-other-mention-everyone", channelId, content: "@everyone heads up", @@ -482,18 +409,7 @@ describe("preflightDiscordMessage", () => { }, }); - const result = await runGuildPreflight({ - channelId, - guildId, - message, - discordConfig: {} as DiscordConfig, - guildEntries: { - [guildId]: { - requireMention: false, - ignoreOtherMentions: true, - }, - }, - }); + const result = await runIgnoreOtherMentionsPreflight({ channelId, guildId, message }); expect(result).not.toBeNull(); expect(result?.hasAnyMention).toBe(true); @@ -503,7 +419,7 @@ describe("preflightDiscordMessage", () => { const channelId = "channel-everyone-1"; const guildId = "guild-everyone-1"; const client = createGuildTextClient(channelId); - const message = createMessage({ + const message = createDiscordMessage({ id: "m-everyone-1", channelId, content: "@everyone heads up", @@ -517,7 +433,7 @@ describe("preflightDiscordMessage", () => { const result = await preflightDiscordMessage({ ...createPreflightArgs({ - cfg: DEFAULT_CFG, + cfg: DEFAULT_PREFLIGHT_CFG, discordConfig: { allowBots: true, } as DiscordConfig, @@ -546,7 +462,7 @@ describe("preflightDiscordMessage", () => { const channelId = "channel-audio-1"; const client = createGuildTextClient(channelId); - const message = createMessage({ + const message = createDiscordMessage({ id: "m-audio-1", channelId, content: "", @@ -568,7 +484,7 @@ describe("preflightDiscordMessage", () => { const result = await preflightDiscordMessage( createPreflightArgs({ cfg: { - ...DEFAULT_CFG, + ...DEFAULT_PREFLIGHT_CFG, messages: { groupChat: { mentionPatterns: ["openclaw"],