test: cover explicit mention gating across channels
This commit is contained in:
45
src/auto-reply/reply/mentions.test.ts
Normal file
45
src/auto-reply/reply/mentions.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { matchesMentionWithExplicit } from "./mentions.js";
|
||||
|
||||
describe("matchesMentionWithExplicit", () => {
|
||||
const mentionRegexes = [/\bclawd\b/i];
|
||||
|
||||
it("prefers explicit mentions when other mentions are present", () => {
|
||||
const result = matchesMentionWithExplicit({
|
||||
text: "@clawd hello",
|
||||
mentionRegexes,
|
||||
explicit: {
|
||||
hasAnyMention: true,
|
||||
isExplicitlyMentioned: false,
|
||||
canResolveExplicit: true,
|
||||
},
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when explicitly mentioned even if regexes do not match", () => {
|
||||
const result = matchesMentionWithExplicit({
|
||||
text: "<@123456>",
|
||||
mentionRegexes: [],
|
||||
explicit: {
|
||||
hasAnyMention: true,
|
||||
isExplicitlyMentioned: true,
|
||||
canResolveExplicit: true,
|
||||
},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to regex matching when explicit mention cannot be resolved", () => {
|
||||
const result = matchesMentionWithExplicit({
|
||||
text: "clawd please",
|
||||
mentionRegexes,
|
||||
explicit: {
|
||||
hasAnyMention: true,
|
||||
isExplicitlyMentioned: false,
|
||||
canResolveExplicit: false,
|
||||
},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -135,6 +135,86 @@ describe("discord tool result dispatch", () => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
}, 20_000);
|
||||
|
||||
it("skips guild messages when another user is explicitly mentioned", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
channels: {
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open" },
|
||||
groupPolicy: "open",
|
||||
guilds: { "*": { requireMention: true } },
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
responsePrefix: "PFX",
|
||||
groupChat: { mentionPatterns: ["\\bclawd\\b"] },
|
||||
},
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
cfg,
|
||||
discordConfig: cfg.channels.discord,
|
||||
accountId: "default",
|
||||
token: "token",
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: (code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
},
|
||||
botUserId: "bot-id",
|
||||
guildHistories: new Map(),
|
||||
historyLimit: 0,
|
||||
mediaMaxBytes: 10_000,
|
||||
textLimit: 2000,
|
||||
replyToMode: "off",
|
||||
dmEnabled: true,
|
||||
groupDmEnabled: false,
|
||||
guildEntries: { "*": { requireMention: true } },
|
||||
});
|
||||
|
||||
const client = {
|
||||
fetchChannel: vi.fn().mockResolvedValue({
|
||||
type: ChannelType.GuildText,
|
||||
name: "general",
|
||||
}),
|
||||
} as unknown as Client;
|
||||
|
||||
await handler(
|
||||
{
|
||||
message: {
|
||||
id: "m2",
|
||||
content: "clawd: hello",
|
||||
channelId: "c1",
|
||||
timestamp: new Date().toISOString(),
|
||||
type: MessageType.Default,
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
mentionedEveryone: false,
|
||||
mentionedUsers: [{ id: "u2", bot: false, username: "Bea" }],
|
||||
mentionedRoles: [],
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
},
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
member: { nickname: "Ada" },
|
||||
guild: { id: "g1", name: "Guild" },
|
||||
guild_id: "g1",
|
||||
},
|
||||
client,
|
||||
);
|
||||
|
||||
expect(dispatchMock).not.toHaveBeenCalled();
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
}, 20_000);
|
||||
|
||||
it("accepts guild reply-to-bot messages as implicit mentions", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
const cfg = {
|
||||
|
||||
@@ -400,6 +400,51 @@ describe("monitorSlackProvider tool results", () => {
|
||||
expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true);
|
||||
});
|
||||
|
||||
it("skips channel messages when another user is explicitly mentioned", async () => {
|
||||
slackTestState.config = {
|
||||
messages: {
|
||||
responsePrefix: "PFX",
|
||||
groupChat: { mentionPatterns: ["\\bclawd\\b"] },
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { C1: { allow: true, requireMention: true } },
|
||||
},
|
||||
},
|
||||
};
|
||||
replyMock.mockResolvedValue({ text: "hi" });
|
||||
|
||||
const controller = new AbortController();
|
||||
const run = monitorSlackProvider({
|
||||
botToken: "bot-token",
|
||||
appToken: "app-token",
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
|
||||
await waitForSlackEvent("message");
|
||||
const handler = getSlackHandlers()?.get("message");
|
||||
if (!handler) throw new Error("Slack message handler not registered");
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
user: "U1",
|
||||
text: "clawd: hello <@U2>",
|
||||
ts: "123",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await flush();
|
||||
controller.abort();
|
||||
await run;
|
||||
|
||||
expect(replyMock).not.toHaveBeenCalled();
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats replies to bot threads as implicit mentions", async () => {
|
||||
slackTestState.config = {
|
||||
channels: {
|
||||
|
||||
@@ -210,6 +210,47 @@ describe("createTelegramBot", () => {
|
||||
new RegExp(`^\\[Telegram Test Group id:7 (\\+\\d+[smhd] )?${timestampPattern}\\]`),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips group messages when another user is explicitly mentioned", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
agents: {
|
||||
defaults: {
|
||||
envelopeTimezone: "utc",
|
||||
},
|
||||
},
|
||||
identity: { name: "Bert" },
|
||||
messages: { groupChat: { mentionPatterns: ["\\bbert\\b"] } },
|
||||
channels: {
|
||||
telegram: {
|
||||
groupPolicy: "open",
|
||||
groups: { "*": { requireMention: true } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 7, type: "group", title: "Test Group" },
|
||||
text: "bert: hello @alice",
|
||||
entities: [{ type: "mention", offset: 12, length: 6 }],
|
||||
date: 1736380800,
|
||||
message_id: 3,
|
||||
from: { id: 9, first_name: "Ada" },
|
||||
},
|
||||
me: { username: "clawdbot_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps group envelope headers stable (sender identity is separate)", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
55
src/web/auto-reply/mentions.test.ts
Normal file
55
src/web/auto-reply/mentions.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { WebInboundMsg } from "./types.js";
|
||||
import { isBotMentionedFromTargets, resolveMentionTargets } from "./mentions.js";
|
||||
|
||||
const makeMsg = (overrides: Partial<WebInboundMsg>): WebInboundMsg =>
|
||||
({
|
||||
id: "m1",
|
||||
from: "120363401234567890@g.us",
|
||||
conversationId: "120363401234567890@g.us",
|
||||
to: "15551234567@s.whatsapp.net",
|
||||
accountId: "default",
|
||||
body: "",
|
||||
chatType: "group",
|
||||
chatId: "120363401234567890@g.us",
|
||||
sendComposing: async () => {},
|
||||
reply: async () => {},
|
||||
sendMedia: async () => {},
|
||||
...overrides,
|
||||
}) as WebInboundMsg;
|
||||
|
||||
describe("isBotMentionedFromTargets", () => {
|
||||
const mentionCfg = { mentionRegexes: [/\bclawd\b/i] };
|
||||
|
||||
it("ignores regex matches when other mentions are present", () => {
|
||||
const msg = makeMsg({
|
||||
body: "@Clawd please help",
|
||||
mentionedJids: ["19998887777@s.whatsapp.net"],
|
||||
selfE164: "+15551234567",
|
||||
selfJid: "15551234567@s.whatsapp.net",
|
||||
});
|
||||
const targets = resolveMentionTargets(msg);
|
||||
expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(false);
|
||||
});
|
||||
|
||||
it("matches explicit self mentions", () => {
|
||||
const msg = makeMsg({
|
||||
body: "hey",
|
||||
mentionedJids: ["15551234567@s.whatsapp.net"],
|
||||
selfE164: "+15551234567",
|
||||
selfJid: "15551234567@s.whatsapp.net",
|
||||
});
|
||||
const targets = resolveMentionTargets(msg);
|
||||
expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to regex when no mentions are present", () => {
|
||||
const msg = makeMsg({
|
||||
body: "clawd can you help?",
|
||||
selfE164: "+15551234567",
|
||||
selfJid: "15551234567@s.whatsapp.net",
|
||||
});
|
||||
const targets = resolveMentionTargets(msg);
|
||||
expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user