test: dedupe discord preflight helpers

This commit is contained in:
Peter Steinberger 2026-03-13 22:37:57 +00:00
parent 6a44ca9f76
commit fd340a88d6
3 changed files with 190 additions and 200 deletions

View File

@ -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<string, unknown>) {
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<import("../../config/config.js").OpenClawConfig["channels"]>["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<import("../../config/config.js").OpenClawConfig["channels"]>["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<typeof preflightDiscordMessage>[0];
}

View File

@ -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<OpenClawConfig["channels"]>["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<Record<string, unknown>>;
}): 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<typeof preflightDiscordMessage>[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,
};
}

View File

@ -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<typeof preflightDiscordMessage>[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<Record<string, unknown>>;
}): 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"],