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);
|
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||||
}, 20_000);
|
}, 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 () => {
|
it("accepts guild reply-to-bot messages as implicit mentions", async () => {
|
||||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|||||||
@@ -400,6 +400,51 @@ describe("monitorSlackProvider tool results", () => {
|
|||||||
expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true);
|
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 () => {
|
it("treats replies to bot threads as implicit mentions", async () => {
|
||||||
slackTestState.config = {
|
slackTestState.config = {
|
||||||
channels: {
|
channels: {
|
||||||
|
|||||||
@@ -210,6 +210,47 @@ describe("createTelegramBot", () => {
|
|||||||
new RegExp(`^\\[Telegram Test Group id:7 (\\+\\d+[smhd] )?${timestampPattern}\\]`),
|
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 () => {
|
it("keeps group envelope headers stable (sender identity is separate)", async () => {
|
||||||
onSpy.mockReset();
|
onSpy.mockReset();
|
||||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
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