From fe841487248e6eafbf8947e96cb3ce7ed2aeea96 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 25 Mar 2026 04:51:13 +0000 Subject: [PATCH] test: collapse messaging target test suites --- extensions/discord/src/group-policy.test.ts | 79 -------- extensions/discord/src/targets.test.ts | 78 ++++++++ .../signal/src/channel.outbound.test.ts | 63 ------- extensions/signal/src/channel.test.ts | 54 ------ extensions/signal/src/core.test.ts | 149 +++++++++++++++ extensions/signal/src/format.links.test.ts | 35 ---- extensions/signal/src/format.test.ts | 75 ++++++++ extensions/signal/src/format.visual.test.ts | 57 ------ extensions/signal/src/identity.test.ts | 56 ------ .../signal/src/outbound-adapter.test.ts | 67 ------- extensions/signal/src/outbound.test.ts | 175 ++++++++++++++++++ extensions/signal/src/probe.test.ts | 62 ------- .../signal/src/setup-allow-from.test.ts | 39 ---- extensions/telegram/src/group-policy.test.ts | 40 ---- extensions/telegram/src/targets.test.ts | 39 ++++ .../whatsapp/src/channel.directory.test.ts | 62 ------- .../whatsapp/src/channel.outbound.test.ts | 49 ----- extensions/whatsapp/src/channel.test.ts | 142 +++++++++++++- extensions/whatsapp/src/group-policy.test.ts | 36 ---- 19 files changed, 657 insertions(+), 700 deletions(-) delete mode 100644 extensions/discord/src/group-policy.test.ts delete mode 100644 extensions/signal/src/channel.outbound.test.ts delete mode 100644 extensions/signal/src/channel.test.ts create mode 100644 extensions/signal/src/core.test.ts delete mode 100644 extensions/signal/src/format.links.test.ts delete mode 100644 extensions/signal/src/format.visual.test.ts delete mode 100644 extensions/signal/src/identity.test.ts delete mode 100644 extensions/signal/src/outbound-adapter.test.ts create mode 100644 extensions/signal/src/outbound.test.ts delete mode 100644 extensions/signal/src/probe.test.ts delete mode 100644 extensions/signal/src/setup-allow-from.test.ts delete mode 100644 extensions/telegram/src/group-policy.test.ts delete mode 100644 extensions/whatsapp/src/channel.directory.test.ts delete mode 100644 extensions/whatsapp/src/channel.outbound.test.ts delete mode 100644 extensions/whatsapp/src/group-policy.test.ts diff --git a/extensions/discord/src/group-policy.test.ts b/extensions/discord/src/group-policy.test.ts deleted file mode 100644 index 249df3fa8a7..00000000000 --- a/extensions/discord/src/group-policy.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - resolveDiscordGroupRequireMention, - resolveDiscordGroupToolPolicy, -} from "./group-policy.js"; - -describe("discord group policy", () => { - it("prefers channel policy, then guild policy, with sender-specific overrides", () => { - const discordCfg = { - channels: { - discord: { - token: "discord-test", - guilds: { - guild1: { - requireMention: false, - tools: { allow: ["message.guild"] }, - toolsBySender: { - "id:user:guild-admin": { allow: ["sessions.list"] }, - }, - channels: { - "123": { - requireMention: true, - tools: { allow: ["message.channel"] }, - toolsBySender: { - "id:user:channel-admin": { deny: ["exec"] }, - }, - }, - }, - }, - }, - }, - }, - // oxlint-disable-next-line typescript/no-explicit-any - } as any; - - expect( - resolveDiscordGroupRequireMention({ cfg: discordCfg, groupSpace: "guild1", groupId: "123" }), - ).toBe(true); - expect( - resolveDiscordGroupRequireMention({ - cfg: discordCfg, - groupSpace: "guild1", - groupId: "missing", - }), - ).toBe(false); - expect( - resolveDiscordGroupToolPolicy({ - cfg: discordCfg, - groupSpace: "guild1", - groupId: "123", - senderId: "user:channel-admin", - }), - ).toEqual({ deny: ["exec"] }); - expect( - resolveDiscordGroupToolPolicy({ - cfg: discordCfg, - groupSpace: "guild1", - groupId: "123", - senderId: "user:someone", - }), - ).toEqual({ allow: ["message.channel"] }); - expect( - resolveDiscordGroupToolPolicy({ - cfg: discordCfg, - groupSpace: "guild1", - groupId: "missing", - senderId: "user:guild-admin", - }), - ).toEqual({ allow: ["sessions.list"] }); - expect( - resolveDiscordGroupToolPolicy({ - cfg: discordCfg, - groupSpace: "guild1", - groupId: "missing", - senderId: "user:someone", - }), - ).toEqual({ allow: ["message.guild"] }); - }); -}); diff --git a/extensions/discord/src/targets.test.ts b/extensions/discord/src/targets.test.ts index fa8b739b3b5..29f02d6e73d 100644 --- a/extensions/discord/src/targets.test.ts +++ b/extensions/discord/src/targets.test.ts @@ -1,6 +1,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; import * as directoryLive from "./directory-live.js"; +import { + resolveDiscordGroupRequireMention, + resolveDiscordGroupToolPolicy, +} from "./group-policy.js"; import { normalizeDiscordMessagingTarget } from "./normalize.js"; import { parseDiscordTarget, resolveDiscordChannelId, resolveDiscordTarget } from "./targets.js"; @@ -105,3 +109,77 @@ describe("normalizeDiscordMessagingTarget", () => { expect(normalizeDiscordMessagingTarget("123")).toBe("channel:123"); }); }); + +describe("discord group policy", () => { + it("prefers channel policy, then guild policy, with sender-specific overrides", () => { + const discordCfg = { + channels: { + discord: { + token: "discord-test", + guilds: { + guild1: { + requireMention: false, + tools: { allow: ["message.guild"] }, + toolsBySender: { + "id:user:guild-admin": { allow: ["sessions.list"] }, + }, + channels: { + "123": { + requireMention: true, + tools: { allow: ["message.channel"] }, + toolsBySender: { + "id:user:channel-admin": { deny: ["exec"] }, + }, + }, + }, + }, + }, + }, + }, + // oxlint-disable-next-line typescript/no-explicit-any + } as any; + + expect( + resolveDiscordGroupRequireMention({ cfg: discordCfg, groupSpace: "guild1", groupId: "123" }), + ).toBe(true); + expect( + resolveDiscordGroupRequireMention({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "missing", + }), + ).toBe(false); + expect( + resolveDiscordGroupToolPolicy({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "123", + senderId: "user:channel-admin", + }), + ).toEqual({ deny: ["exec"] }); + expect( + resolveDiscordGroupToolPolicy({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "123", + senderId: "user:someone", + }), + ).toEqual({ allow: ["message.channel"] }); + expect( + resolveDiscordGroupToolPolicy({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "missing", + senderId: "user:guild-admin", + }), + ).toEqual({ allow: ["sessions.list"] }); + expect( + resolveDiscordGroupToolPolicy({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "missing", + senderId: "user:someone", + }), + ).toEqual({ allow: ["message.guild"] }); + }); +}); diff --git a/extensions/signal/src/channel.outbound.test.ts b/extensions/signal/src/channel.outbound.test.ts deleted file mode 100644 index f1ceafbcab2..00000000000 --- a/extensions/signal/src/channel.outbound.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { signalPlugin } from "./channel.js"; - -describe("signal outbound cfg threading", () => { - it("threads provided cfg into sendText deps call", async () => { - const cfg = { - channels: { - signal: { - accounts: { - work: { - mediaMaxMb: 12, - }, - }, - mediaMaxMb: 5, - }, - }, - }; - const sendSignal = vi.fn(async () => ({ messageId: "sig-1" })); - - const result = await signalPlugin.outbound!.sendText!({ - cfg, - to: "+15551230000", - text: "hello", - accountId: "work", - deps: { sendSignal }, - }); - - expect(sendSignal).toHaveBeenCalledWith("+15551230000", "hello", { - cfg, - maxBytes: 12 * 1024 * 1024, - accountId: "work", - }); - expect(result).toEqual({ channel: "signal", messageId: "sig-1" }); - }); - - it("threads cfg + mediaUrl into sendMedia deps call", async () => { - const cfg = { - channels: { - signal: { - mediaMaxMb: 7, - }, - }, - }; - const sendSignal = vi.fn(async () => ({ messageId: "sig-2" })); - - const result = await signalPlugin.outbound!.sendMedia!({ - cfg, - to: "+15559870000", - text: "photo", - mediaUrl: "https://example.com/a.jpg", - accountId: "default", - deps: { sendSignal }, - }); - - expect(sendSignal).toHaveBeenCalledWith("+15559870000", "photo", { - cfg, - mediaUrl: "https://example.com/a.jpg", - maxBytes: 7 * 1024 * 1024, - accountId: "default", - }); - expect(result).toEqual({ channel: "signal", messageId: "sig-2" }); - }); -}); diff --git a/extensions/signal/src/channel.test.ts b/extensions/signal/src/channel.test.ts deleted file mode 100644 index e951cef2e74..00000000000 --- a/extensions/signal/src/channel.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { signalPlugin } from "./channel.js"; - -describe("signalPlugin outbound sendMedia", () => { - it("forwards mediaLocalRoots to sendMessageSignal", async () => { - const sendSignal = vi.fn(async () => ({ messageId: "m1" })); - const mediaLocalRoots = ["/tmp/workspace"]; - - const sendMedia = signalPlugin.outbound?.sendMedia; - if (!sendMedia) { - throw new Error("signal outbound sendMedia is unavailable"); - } - - const result = await sendMedia({ - cfg: {} as never, - to: "signal:+15551234567", - text: "photo", - mediaUrl: "/tmp/workspace/photo.png", - mediaLocalRoots, - accountId: "default", - deps: { sendSignal }, - }); - - expect(sendSignal).toHaveBeenCalledWith( - "signal:+15551234567", - "photo", - expect.objectContaining({ - mediaUrl: "/tmp/workspace/photo.png", - mediaLocalRoots, - accountId: "default", - }), - ); - expect(result).toEqual({ channel: "signal", messageId: "m1" }); - }); -}); - -describe("signalPlugin actions", () => { - it("owns unified message tool discovery", () => { - const discovery = signalPlugin.actions?.describeMessageTool?.({ - cfg: { - channels: { - signal: { - actions: { reactions: false }, - accounts: { - work: { account: "+15550001111", actions: { reactions: true } }, - }, - }, - }, - } as never, - }); - - expect(discovery?.actions).toEqual(["send", "react"]); - }); -}); diff --git a/extensions/signal/src/core.test.ts b/extensions/signal/src/core.test.ts new file mode 100644 index 00000000000..4ef9866fa36 --- /dev/null +++ b/extensions/signal/src/core.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it, vi } from "vitest"; +import * as clientModule from "./client.js"; +import { classifySignalCliLogLine } from "./daemon.js"; +import { + looksLikeUuid, + resolveSignalPeerId, + resolveSignalRecipient, + resolveSignalSender, +} from "./identity.js"; +import { probeSignal } from "./probe.js"; +import { normalizeSignalAccountInput, parseSignalAllowFromEntries } from "./setup-core.js"; + +describe("looksLikeUuid", () => { + it("accepts hyphenated UUIDs", () => { + expect(looksLikeUuid("123e4567-e89b-12d3-a456-426614174000")).toBe(true); + }); + + it("accepts compact UUIDs", () => { + expect(looksLikeUuid("123e4567e89b12d3a456426614174000")).toBe(true); // pragma: allowlist secret + }); + + it("accepts uuid-like hex values with letters", () => { + expect(looksLikeUuid("abcd-1234")).toBe(true); + }); + + it("rejects numeric ids and phone-like values", () => { + expect(looksLikeUuid("1234567890")).toBe(false); + expect(looksLikeUuid("+15555551212")).toBe(false); + }); +}); + +describe("signal sender identity", () => { + it("prefers sourceNumber over sourceUuid", () => { + const sender = resolveSignalSender({ + sourceNumber: " +15550001111 ", + sourceUuid: "123e4567-e89b-12d3-a456-426614174000", + }); + expect(sender).toEqual({ + kind: "phone", + raw: "+15550001111", + e164: "+15550001111", + }); + }); + + it("uses sourceUuid when sourceNumber is missing", () => { + const sender = resolveSignalSender({ + sourceUuid: "123e4567-e89b-12d3-a456-426614174000", + }); + expect(sender).toEqual({ + kind: "uuid", + raw: "123e4567-e89b-12d3-a456-426614174000", + }); + }); + + it("maps uuid senders to recipient and peer ids", () => { + const sender = { kind: "uuid", raw: "123e4567-e89b-12d3-a456-426614174000" } as const; + expect(resolveSignalRecipient(sender)).toBe("123e4567-e89b-12d3-a456-426614174000"); + expect(resolveSignalPeerId(sender)).toBe("uuid:123e4567-e89b-12d3-a456-426614174000"); + }); +}); + +describe("probeSignal", () => { + it("extracts version from {version} result", async () => { + vi.spyOn(clientModule, "signalCheck").mockResolvedValueOnce({ + ok: true, + status: 200, + error: null, + }); + vi.spyOn(clientModule, "signalRpcRequest").mockResolvedValueOnce({ version: "0.13.22" }); + + const res = await probeSignal("http://127.0.0.1:8080", 1000); + + expect(res.ok).toBe(true); + expect(res.version).toBe("0.13.22"); + expect(res.status).toBe(200); + }); + + it("returns ok=false when /check fails", async () => { + vi.spyOn(clientModule, "signalCheck").mockResolvedValueOnce({ + ok: false, + status: 503, + error: "HTTP 503", + }); + + const res = await probeSignal("http://127.0.0.1:8080", 1000); + + expect(res.ok).toBe(false); + expect(res.status).toBe(503); + expect(res.version).toBe(null); + }); +}); + +describe("classifySignalCliLogLine", () => { + it("treats INFO/DEBUG as log", () => { + expect(classifySignalCliLogLine("INFO DaemonCommand - Started")).toBe("log"); + expect(classifySignalCliLogLine("DEBUG Something")).toBe("log"); + }); + + it("treats WARN/ERROR as error", () => { + expect(classifySignalCliLogLine("WARN Something")).toBe("error"); + expect(classifySignalCliLogLine("WARNING Something")).toBe("error"); + expect(classifySignalCliLogLine("ERROR Something")).toBe("error"); + }); + + it("treats failures without explicit severity as error", () => { + expect(classifySignalCliLogLine("Failed to initialize HTTP Server - oops")).toBe("error"); + expect(classifySignalCliLogLine('Exception in thread "main"')).toBe("error"); + }); + + it("returns null for empty lines", () => { + expect(classifySignalCliLogLine("")).toBe(null); + expect(classifySignalCliLogLine(" ")).toBe(null); + }); +}); + +describe("signal setup parsing", () => { + it("normalizes valid E.164 numbers", () => { + expect(normalizeSignalAccountInput(" +1 (555) 555-0123 ")).toBe("+15555550123"); + }); + + it("rejects invalid values", () => { + expect(normalizeSignalAccountInput("abc")).toBeNull(); + }); + + it("parses e164, uuid and wildcard entries", () => { + expect( + parseSignalAllowFromEntries("+15555550123, uuid:123e4567-e89b-12d3-a456-426614174000, *"), + ).toEqual({ + entries: ["+15555550123", "uuid:123e4567-e89b-12d3-a456-426614174000", "*"], + }); + }); + + it("normalizes bare uuid values", () => { + expect(parseSignalAllowFromEntries("123e4567-e89b-12d3-a456-426614174000")).toEqual({ + entries: ["uuid:123e4567-e89b-12d3-a456-426614174000"], + }); + }); + + it("returns validation errors for invalid entries", () => { + expect(parseSignalAllowFromEntries("uuid:")).toEqual({ + entries: [], + error: "Invalid uuid entry", + }); + expect(parseSignalAllowFromEntries("invalid")).toEqual({ + entries: [], + error: "Invalid entry: invalid", + }); + }); +}); diff --git a/extensions/signal/src/format.links.test.ts b/extensions/signal/src/format.links.test.ts deleted file mode 100644 index c6ec112a7df..00000000000 --- a/extensions/signal/src/format.links.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { markdownToSignalText } from "./format.js"; - -describe("markdownToSignalText", () => { - describe("duplicate URL display", () => { - it("does not duplicate URL for normalized equivalent labels", () => { - const equivalentCases = [ - { input: "[selfh.st](http://selfh.st)", expected: "selfh.st" }, - { input: "[example.com](https://example.com)", expected: "example.com" }, - { input: "[www.example.com](https://example.com)", expected: "www.example.com" }, - { input: "[example.com](https://example.com/)", expected: "example.com" }, - { input: "[example.com](https://example.com///)", expected: "example.com" }, - { input: "[example.com](https://www.example.com)", expected: "example.com" }, - { input: "[EXAMPLE.COM](https://example.com)", expected: "EXAMPLE.COM" }, - { input: "[example.com/page](https://example.com/page)", expected: "example.com/page" }, - ] as const; - - for (const { input, expected } of equivalentCases) { - const res = markdownToSignalText(input); - expect(res.text).toBe(expected); - } - }); - - it("still shows URL when label is meaningfully different", () => { - const res = markdownToSignalText("[click here](https://example.com)"); - expect(res.text).toBe("click here (https://example.com)"); - }); - - it("handles URL with path - should show URL when label is just domain", () => { - // Label is just domain, URL has path - these are meaningfully different - const res = markdownToSignalText("[example.com](https://example.com/page)"); - expect(res.text).toBe("example.com (https://example.com/page)"); - }); - }); -}); diff --git a/extensions/signal/src/format.test.ts b/extensions/signal/src/format.test.ts index e22a6607f99..1ed5785b65c 100644 --- a/extensions/signal/src/format.test.ts +++ b/extensions/signal/src/format.test.ts @@ -65,4 +65,79 @@ describe("markdownToSignalText", () => { expect(res.text).toBe(`${prefix}bold`); expect(res.styles).toEqual([{ start: prefix.length, length: 4, style: "BOLD" }]); }); + + describe("duplicate URL display", () => { + it("does not duplicate URL for normalized equivalent labels", () => { + const equivalentCases = [ + { input: "[selfh.st](http://selfh.st)", expected: "selfh.st" }, + { input: "[example.com](https://example.com)", expected: "example.com" }, + { input: "[www.example.com](https://example.com)", expected: "www.example.com" }, + { input: "[example.com](https://example.com/)", expected: "example.com" }, + { input: "[example.com](https://example.com///)", expected: "example.com" }, + { input: "[example.com](https://www.example.com)", expected: "example.com" }, + { input: "[EXAMPLE.COM](https://example.com)", expected: "EXAMPLE.COM" }, + { input: "[example.com/page](https://example.com/page)", expected: "example.com/page" }, + ] as const; + + for (const { input, expected } of equivalentCases) { + const res = markdownToSignalText(input); + expect(res.text).toBe(expected); + } + }); + + it("still shows URL when label is meaningfully different", () => { + const res = markdownToSignalText("[click here](https://example.com)"); + expect(res.text).toBe("click here (https://example.com)"); + }); + + it("shows URL when the label is only the domain but the URL has a path", () => { + const res = markdownToSignalText("[example.com](https://example.com/page)"); + expect(res.text).toBe("example.com (https://example.com/page)"); + }); + }); + + describe("visual distinctions", () => { + it("renders headings as bold text", () => { + const res = markdownToSignalText("# Heading 1"); + expect(res.text).toBe("Heading 1"); + expect(res.styles).toContainEqual({ start: 0, length: 9, style: "BOLD" }); + }); + + it("renders h2 headings as bold text", () => { + const res = markdownToSignalText("## Heading 2"); + expect(res.text).toBe("Heading 2"); + expect(res.styles).toContainEqual({ start: 0, length: 9, style: "BOLD" }); + }); + + it("renders h3 headings as bold text", () => { + const res = markdownToSignalText("### Heading 3"); + expect(res.text).toBe("Heading 3"); + expect(res.styles).toContainEqual({ start: 0, length: 9, style: "BOLD" }); + }); + + it("renders blockquotes with a visible prefix", () => { + const res = markdownToSignalText("> This is a quote"); + expect(res.text).toMatch(/^[│>]/); + expect(res.text).toContain("This is a quote"); + }); + + it("renders multi-line blockquotes with a visible prefix", () => { + const res = markdownToSignalText("> Line 1\n> Line 2"); + expect(res.text).toMatch(/^[│>]/); + expect(res.text).toContain("Line 1"); + expect(res.text).toContain("Line 2"); + }); + + it("renders horizontal rules as a visible separator", () => { + const res = markdownToSignalText("Para 1\n\n---\n\nPara 2"); + expect(res.text).toMatch(/[─—-]{3,}/); + }); + + it("renders horizontal rules between content", () => { + const res = markdownToSignalText("Above\n\n***\n\nBelow"); + expect(res.text).toContain("Above"); + expect(res.text).toContain("Below"); + expect(res.text).toMatch(/[─—-]{3,}/); + }); + }); }); diff --git a/extensions/signal/src/format.visual.test.ts b/extensions/signal/src/format.visual.test.ts deleted file mode 100644 index 78f913b7945..00000000000 --- a/extensions/signal/src/format.visual.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { markdownToSignalText } from "./format.js"; - -describe("markdownToSignalText", () => { - describe("headings visual distinction", () => { - it("renders headings as bold text", () => { - const res = markdownToSignalText("# Heading 1"); - expect(res.text).toBe("Heading 1"); - expect(res.styles).toContainEqual({ start: 0, length: 9, style: "BOLD" }); - }); - - it("renders h2 headings as bold text", () => { - const res = markdownToSignalText("## Heading 2"); - expect(res.text).toBe("Heading 2"); - expect(res.styles).toContainEqual({ start: 0, length: 9, style: "BOLD" }); - }); - - it("renders h3 headings as bold text", () => { - const res = markdownToSignalText("### Heading 3"); - expect(res.text).toBe("Heading 3"); - expect(res.styles).toContainEqual({ start: 0, length: 9, style: "BOLD" }); - }); - }); - - describe("blockquote visual distinction", () => { - it("renders blockquotes with a visible prefix", () => { - const res = markdownToSignalText("> This is a quote"); - // Should have some kind of prefix to distinguish it - expect(res.text).toMatch(/^[│>]/); - expect(res.text).toContain("This is a quote"); - }); - - it("renders multi-line blockquotes with prefix", () => { - const res = markdownToSignalText("> Line 1\n> Line 2"); - // Should start with the prefix - expect(res.text).toMatch(/^[│>]/); - expect(res.text).toContain("Line 1"); - expect(res.text).toContain("Line 2"); - }); - }); - - describe("horizontal rule rendering", () => { - it("renders horizontal rules as a visible separator", () => { - const res = markdownToSignalText("Para 1\n\n---\n\nPara 2"); - // Should contain some kind of visual separator like ─── - expect(res.text).toMatch(/[─—-]{3,}/); - }); - - it("renders horizontal rule between content", () => { - const res = markdownToSignalText("Above\n\n***\n\nBelow"); - expect(res.text).toContain("Above"); - expect(res.text).toContain("Below"); - // Should have a separator - expect(res.text).toMatch(/[─—-]{3,}/); - }); - }); -}); diff --git a/extensions/signal/src/identity.test.ts b/extensions/signal/src/identity.test.ts deleted file mode 100644 index a09f81910c6..00000000000 --- a/extensions/signal/src/identity.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - looksLikeUuid, - resolveSignalPeerId, - resolveSignalRecipient, - resolveSignalSender, -} from "./identity.js"; - -describe("looksLikeUuid", () => { - it("accepts hyphenated UUIDs", () => { - expect(looksLikeUuid("123e4567-e89b-12d3-a456-426614174000")).toBe(true); - }); - - it("accepts compact UUIDs", () => { - expect(looksLikeUuid("123e4567e89b12d3a456426614174000")).toBe(true); // pragma: allowlist secret - }); - - it("accepts uuid-like hex values with letters", () => { - expect(looksLikeUuid("abcd-1234")).toBe(true); - }); - - it("rejects numeric ids and phone-like values", () => { - expect(looksLikeUuid("1234567890")).toBe(false); - expect(looksLikeUuid("+15555551212")).toBe(false); - }); -}); - -describe("signal sender identity", () => { - it("prefers sourceNumber over sourceUuid", () => { - const sender = resolveSignalSender({ - sourceNumber: " +15550001111 ", - sourceUuid: "123e4567-e89b-12d3-a456-426614174000", - }); - expect(sender).toEqual({ - kind: "phone", - raw: "+15550001111", - e164: "+15550001111", - }); - }); - - it("uses sourceUuid when sourceNumber is missing", () => { - const sender = resolveSignalSender({ - sourceUuid: "123e4567-e89b-12d3-a456-426614174000", - }); - expect(sender).toEqual({ - kind: "uuid", - raw: "123e4567-e89b-12d3-a456-426614174000", - }); - }); - - it("maps uuid senders to recipient and peer ids", () => { - const sender = { kind: "uuid", raw: "123e4567-e89b-12d3-a456-426614174000" } as const; - expect(resolveSignalRecipient(sender)).toBe("123e4567-e89b-12d3-a456-426614174000"); - expect(resolveSignalPeerId(sender)).toBe("uuid:123e4567-e89b-12d3-a456-426614174000"); - }); -}); diff --git a/extensions/signal/src/outbound-adapter.test.ts b/extensions/signal/src/outbound-adapter.test.ts deleted file mode 100644 index 1251a29b1f9..00000000000 --- a/extensions/signal/src/outbound-adapter.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const sendMessageSignalMock = vi.fn(); - -vi.mock("./send.js", () => ({ - sendMessageSignal: (...args: unknown[]) => sendMessageSignalMock(...args), -})); - -import { signalOutbound } from "./outbound-adapter.js"; - -describe("signalOutbound", () => { - beforeEach(() => { - sendMessageSignalMock.mockReset(); - }); - - it("formats media captions and forwards mediaLocalRoots", async () => { - sendMessageSignalMock.mockResolvedValueOnce({ messageId: "sig-media" }); - - const result = await signalOutbound.sendFormattedMedia!({ - cfg: {} as never, - to: "signal:+15551234567", - text: "**bold** caption", - mediaUrl: "/tmp/workspace/photo.png", - mediaLocalRoots: ["/tmp/workspace"], - accountId: "default", - }); - - expect(sendMessageSignalMock).toHaveBeenCalledWith( - "signal:+15551234567", - "bold caption", - expect.objectContaining({ - mediaUrl: "/tmp/workspace/photo.png", - mediaLocalRoots: ["/tmp/workspace"], - accountId: "default", - textMode: "plain", - textStyles: [{ start: 0, length: 4, style: "BOLD" }], - }), - ); - expect(result).toEqual({ channel: "signal", messageId: "sig-media" }); - }); - - it("formats markdown text into plain Signal chunks with styles", async () => { - sendMessageSignalMock.mockResolvedValue({ messageId: "sig-text" }); - - const result = await signalOutbound.sendFormattedText!({ - cfg: {} as never, - to: "signal:+15557654321", - text: "hi _there_ **boss**", - accountId: "default", - }); - - expect(sendMessageSignalMock).toHaveBeenCalledTimes(1); - expect(sendMessageSignalMock).toHaveBeenCalledWith( - "signal:+15557654321", - "hi there boss", - expect.objectContaining({ - accountId: "default", - textMode: "plain", - textStyles: [ - { start: 3, length: 5, style: "ITALIC" }, - { start: 9, length: 4, style: "BOLD" }, - ], - }), - ); - expect(result).toEqual([{ channel: "signal", messageId: "sig-text" }]); - }); -}); diff --git a/extensions/signal/src/outbound.test.ts b/extensions/signal/src/outbound.test.ts new file mode 100644 index 00000000000..f15f45ec29c --- /dev/null +++ b/extensions/signal/src/outbound.test.ts @@ -0,0 +1,175 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { signalPlugin } from "./channel.js"; + +const sendMessageSignalMock = vi.fn(); + +vi.mock("./send.js", () => ({ + sendMessageSignal: (...args: unknown[]) => sendMessageSignalMock(...args), +})); + +import { signalOutbound } from "./outbound-adapter.js"; + +describe("signal outbound", () => { + beforeEach(() => { + sendMessageSignalMock.mockReset(); + }); + + it("formats media captions and forwards mediaLocalRoots", async () => { + sendMessageSignalMock.mockResolvedValueOnce({ messageId: "sig-media" }); + + const result = await signalOutbound.sendFormattedMedia!({ + cfg: {} as never, + to: "signal:+15551234567", + text: "**bold** caption", + mediaUrl: "/tmp/workspace/photo.png", + mediaLocalRoots: ["/tmp/workspace"], + accountId: "default", + }); + + expect(sendMessageSignalMock).toHaveBeenCalledWith( + "signal:+15551234567", + "bold caption", + expect.objectContaining({ + mediaUrl: "/tmp/workspace/photo.png", + mediaLocalRoots: ["/tmp/workspace"], + accountId: "default", + textMode: "plain", + textStyles: [{ start: 0, length: 4, style: "BOLD" }], + }), + ); + expect(result).toEqual({ channel: "signal", messageId: "sig-media" }); + }); + + it("formats markdown text into plain Signal chunks with styles", async () => { + sendMessageSignalMock.mockResolvedValue({ messageId: "sig-text" }); + + const result = await signalOutbound.sendFormattedText!({ + cfg: {} as never, + to: "signal:+15557654321", + text: "hi _there_ **boss**", + accountId: "default", + }); + + expect(sendMessageSignalMock).toHaveBeenCalledTimes(1); + expect(sendMessageSignalMock).toHaveBeenCalledWith( + "signal:+15557654321", + "hi there boss", + expect.objectContaining({ + accountId: "default", + textMode: "plain", + textStyles: [ + { start: 3, length: 5, style: "ITALIC" }, + { start: 9, length: 4, style: "BOLD" }, + ], + }), + ); + expect(result).toEqual([{ channel: "signal", messageId: "sig-text" }]); + }); + + it("threads provided cfg into sendText deps call", async () => { + const cfg = { + channels: { + signal: { + accounts: { + work: { + mediaMaxMb: 12, + }, + }, + mediaMaxMb: 5, + }, + }, + }; + const sendSignal = vi.fn(async () => ({ messageId: "sig-1" })); + + const result = await signalPlugin.outbound!.sendText!({ + cfg, + to: "+15551230000", + text: "hello", + accountId: "work", + deps: { sendSignal }, + }); + + expect(sendSignal).toHaveBeenCalledWith("+15551230000", "hello", { + cfg, + maxBytes: 12 * 1024 * 1024, + accountId: "work", + }); + expect(result).toEqual({ channel: "signal", messageId: "sig-1" }); + }); + + it("threads cfg + mediaUrl into sendMedia deps call", async () => { + const cfg = { + channels: { + signal: { + mediaMaxMb: 7, + }, + }, + }; + const sendSignal = vi.fn(async () => ({ messageId: "sig-2" })); + + const result = await signalPlugin.outbound!.sendMedia!({ + cfg, + to: "+15559870000", + text: "photo", + mediaUrl: "https://example.com/a.jpg", + accountId: "default", + deps: { sendSignal }, + }); + + expect(sendSignal).toHaveBeenCalledWith("+15559870000", "photo", { + cfg, + mediaUrl: "https://example.com/a.jpg", + maxBytes: 7 * 1024 * 1024, + accountId: "default", + }); + expect(result).toEqual({ channel: "signal", messageId: "sig-2" }); + }); + + it("forwards mediaLocalRoots to sendMedia deps", async () => { + const sendSignal = vi.fn(async () => ({ messageId: "m1" })); + const mediaLocalRoots = ["/tmp/workspace"]; + + const sendMedia = signalPlugin.outbound?.sendMedia; + if (!sendMedia) { + throw new Error("signal outbound sendMedia is unavailable"); + } + + const result = await sendMedia({ + cfg: {} as never, + to: "signal:+15551234567", + text: "photo", + mediaUrl: "/tmp/workspace/photo.png", + mediaLocalRoots, + accountId: "default", + deps: { sendSignal }, + }); + + expect(sendSignal).toHaveBeenCalledWith( + "signal:+15551234567", + "photo", + expect.objectContaining({ + mediaUrl: "/tmp/workspace/photo.png", + mediaLocalRoots, + accountId: "default", + }), + ); + expect(result).toEqual({ channel: "signal", messageId: "m1" }); + }); + + it("owns unified message tool discovery", () => { + const discovery = signalPlugin.actions?.describeMessageTool?.({ + cfg: { + channels: { + signal: { + actions: { reactions: false }, + accounts: { + work: { account: "+15550001111", actions: { reactions: true } }, + }, + }, + }, + } as never, + }); + + expect(discovery?.actions).toEqual(["send", "react"]); + }); +}); diff --git a/extensions/signal/src/probe.test.ts b/extensions/signal/src/probe.test.ts deleted file mode 100644 index 30816129107..00000000000 --- a/extensions/signal/src/probe.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as clientModule from "./client.js"; -import { classifySignalCliLogLine } from "./daemon.js"; -import { probeSignal } from "./probe.js"; - -describe("probeSignal", () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("extracts version from {version} result", async () => { - vi.spyOn(clientModule, "signalCheck").mockResolvedValueOnce({ - ok: true, - status: 200, - error: null, - }); - vi.spyOn(clientModule, "signalRpcRequest").mockResolvedValueOnce({ version: "0.13.22" }); - - const res = await probeSignal("http://127.0.0.1:8080", 1000); - - expect(res.ok).toBe(true); - expect(res.version).toBe("0.13.22"); - expect(res.status).toBe(200); - }); - - it("returns ok=false when /check fails", async () => { - vi.spyOn(clientModule, "signalCheck").mockResolvedValueOnce({ - ok: false, - status: 503, - error: "HTTP 503", - }); - - const res = await probeSignal("http://127.0.0.1:8080", 1000); - - expect(res.ok).toBe(false); - expect(res.status).toBe(503); - expect(res.version).toBe(null); - }); -}); - -describe("classifySignalCliLogLine", () => { - it("treats INFO/DEBUG as log (even if emitted on stderr)", () => { - expect(classifySignalCliLogLine("INFO DaemonCommand - Started")).toBe("log"); - expect(classifySignalCliLogLine("DEBUG Something")).toBe("log"); - }); - - it("treats WARN/ERROR as error", () => { - expect(classifySignalCliLogLine("WARN Something")).toBe("error"); - expect(classifySignalCliLogLine("WARNING Something")).toBe("error"); - expect(classifySignalCliLogLine("ERROR Something")).toBe("error"); - }); - - it("treats failures without explicit severity as error", () => { - expect(classifySignalCliLogLine("Failed to initialize HTTP Server - oops")).toBe("error"); - expect(classifySignalCliLogLine('Exception in thread "main"')).toBe("error"); - }); - - it("returns null for empty lines", () => { - expect(classifySignalCliLogLine("")).toBe(null); - expect(classifySignalCliLogLine(" ")).toBe(null); - }); -}); diff --git a/extensions/signal/src/setup-allow-from.test.ts b/extensions/signal/src/setup-allow-from.test.ts deleted file mode 100644 index c7532870109..00000000000 --- a/extensions/signal/src/setup-allow-from.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { normalizeSignalAccountInput, parseSignalAllowFromEntries } from "./setup-core.js"; - -describe("normalizeSignalAccountInput", () => { - it("normalizes valid E.164 numbers", () => { - expect(normalizeSignalAccountInput(" +1 (555) 555-0123 ")).toBe("+15555550123"); - }); - - it("rejects invalid values", () => { - expect(normalizeSignalAccountInput("abc")).toBeNull(); - }); -}); - -describe("parseSignalAllowFromEntries", () => { - it("parses e164, uuid and wildcard entries", () => { - expect( - parseSignalAllowFromEntries("+15555550123, uuid:123e4567-e89b-12d3-a456-426614174000, *"), - ).toEqual({ - entries: ["+15555550123", "uuid:123e4567-e89b-12d3-a456-426614174000", "*"], - }); - }); - - it("normalizes bare uuid values", () => { - expect(parseSignalAllowFromEntries("123e4567-e89b-12d3-a456-426614174000")).toEqual({ - entries: ["uuid:123e4567-e89b-12d3-a456-426614174000"], - }); - }); - - it("returns validation errors for invalid entries", () => { - expect(parseSignalAllowFromEntries("uuid:")).toEqual({ - entries: [], - error: "Invalid uuid entry", - }); - expect(parseSignalAllowFromEntries("invalid")).toEqual({ - entries: [], - error: "Invalid entry: invalid", - }); - }); -}); diff --git a/extensions/telegram/src/group-policy.test.ts b/extensions/telegram/src/group-policy.test.ts deleted file mode 100644 index c93018132bc..00000000000 --- a/extensions/telegram/src/group-policy.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - resolveTelegramGroupRequireMention, - resolveTelegramGroupToolPolicy, -} from "./group-policy.js"; - -describe("telegram group policy", () => { - it("resolves topic-level requireMention and chat-level tools for topic ids", () => { - const telegramCfg = { - channels: { - telegram: { - botToken: "telegram-test", - groups: { - "-1001": { - requireMention: true, - tools: { allow: ["message.send"] }, - topics: { - "77": { - requireMention: false, - }, - }, - }, - "*": { - requireMention: true, - }, - }, - }, - }, - // oxlint-disable-next-line typescript/no-explicit-any - } as any; - expect( - resolveTelegramGroupRequireMention({ cfg: telegramCfg, groupId: "-1001:topic:77" }), - ).toBe(false); - expect(resolveTelegramGroupToolPolicy({ cfg: telegramCfg, groupId: "-1001:topic:77" })).toEqual( - { - allow: ["message.send"], - }, - ); - }); -}); diff --git a/extensions/telegram/src/targets.test.ts b/extensions/telegram/src/targets.test.ts index 1cd28fa094e..22541fd0376 100644 --- a/extensions/telegram/src/targets.test.ts +++ b/extensions/telegram/src/targets.test.ts @@ -1,4 +1,8 @@ import { describe, expect, it } from "vitest"; +import { + resolveTelegramGroupRequireMention, + resolveTelegramGroupToolPolicy, +} from "./group-policy.js"; import { isNumericTelegramChatId, normalizeTelegramChatId, @@ -129,3 +133,38 @@ describe("isNumericTelegramChatId", () => { expect(isNumericTelegramChatId("t.me/mychannel")).toBe(false); }); }); + +describe("telegram group policy", () => { + it("resolves topic-level requireMention and chat-level tools for topic ids", () => { + const telegramCfg = { + channels: { + telegram: { + botToken: "telegram-test", + groups: { + "-1001": { + requireMention: true, + tools: { allow: ["message.send"] }, + topics: { + "77": { + requireMention: false, + }, + }, + }, + "*": { + requireMention: true, + }, + }, + }, + }, + // oxlint-disable-next-line typescript/no-explicit-any + } as any; + expect( + resolveTelegramGroupRequireMention({ cfg: telegramCfg, groupId: "-1001:topic:77" }), + ).toBe(false); + expect(resolveTelegramGroupToolPolicy({ cfg: telegramCfg, groupId: "-1001:topic:77" })).toEqual( + { + allow: ["message.send"], + }, + ); + }); +}); diff --git a/extensions/whatsapp/src/channel.directory.test.ts b/extensions/whatsapp/src/channel.directory.test.ts deleted file mode 100644 index d9a072c86f1..00000000000 --- a/extensions/whatsapp/src/channel.directory.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - createDirectoryTestRuntime, - expectDirectorySurface, -} from "../../../test/helpers/extensions/directory.ts"; -import { whatsappPlugin } from "./channel.js"; -import type { OpenClawConfig } from "./runtime-api.js"; - -describe("whatsapp directory", () => { - const runtimeEnv = createDirectoryTestRuntime() as never; - - it("lists peers and groups from config", async () => { - const cfg = { - channels: { - whatsapp: { - authDir: "/tmp/wa-auth", - allowFrom: [ - "whatsapp:+15551230001", - "15551230002@s.whatsapp.net", - "120363999999999999@g.us", - ], - groups: { - "120363111111111111@g.us": {}, - "120363222222222222@g.us": {}, - }, - }, - }, - } as unknown as OpenClawConfig; - - const directory = expectDirectorySurface(whatsappPlugin.directory); - - await expect( - directory.listPeers({ - cfg, - accountId: undefined, - query: undefined, - limit: undefined, - runtime: runtimeEnv, - }), - ).resolves.toEqual( - expect.arrayContaining([ - { kind: "user", id: "+15551230001" }, - { kind: "user", id: "+15551230002" }, - ]), - ); - - await expect( - directory.listGroups({ - cfg, - accountId: undefined, - query: undefined, - limit: undefined, - runtime: runtimeEnv, - }), - ).resolves.toEqual( - expect.arrayContaining([ - { kind: "group", id: "120363111111111111@g.us" }, - { kind: "group", id: "120363222222222222@g.us" }, - ]), - ); - }); -}); diff --git a/extensions/whatsapp/src/channel.outbound.test.ts b/extensions/whatsapp/src/channel.outbound.test.ts deleted file mode 100644 index 6bde780dd5f..00000000000 --- a/extensions/whatsapp/src/channel.outbound.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - createWhatsAppPollFixture, - expectWhatsAppPollSent, -} from "../../../src/test-helpers/whatsapp-outbound.js"; - -const hoisted = vi.hoisted(() => ({ - sendPollWhatsApp: vi.fn(async () => ({ messageId: "wa-poll-1", toJid: "1555@s.whatsapp.net" })), -})); - -vi.mock("./runtime.js", () => ({ - getWhatsAppRuntime: () => ({ - logging: { - shouldLogVerbose: () => false, - }, - channel: { - whatsapp: { - sendPollWhatsApp: hoisted.sendPollWhatsApp, - }, - }, - }), -})); - -let whatsappPlugin: typeof import("./channel.js").whatsappPlugin; - -describe("whatsappPlugin outbound sendPoll", () => { - beforeEach(async () => { - vi.resetModules(); - ({ whatsappPlugin } = await import("./channel.js")); - }); - - it("threads cfg into runtime sendPollWhatsApp call", async () => { - const { cfg, poll, to, accountId } = createWhatsAppPollFixture(); - - const result = await whatsappPlugin.outbound!.sendPoll!({ - cfg, - to, - poll, - accountId, - }); - - expectWhatsAppPollSent(hoisted.sendPollWhatsApp, { cfg, poll, to, accountId }); - expect(result).toEqual({ - channel: "whatsapp", - messageId: "wa-poll-1", - toJid: "1555@s.whatsapp.net", - }); - }); -}); diff --git a/extensions/whatsapp/src/channel.test.ts b/extensions/whatsapp/src/channel.test.ts index b1e13f87833..a9f3b9cfee4 100644 --- a/extensions/whatsapp/src/channel.test.ts +++ b/extensions/whatsapp/src/channel.test.ts @@ -1,5 +1,35 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + createWhatsAppPollFixture, + expectWhatsAppPollSent, +} from "../../../src/test-helpers/whatsapp-outbound.js"; +import { + createDirectoryTestRuntime, + expectDirectorySurface, +} from "../../../test/helpers/extensions/directory.ts"; import { whatsappPlugin } from "./channel.js"; +import { + resolveWhatsAppGroupRequireMention, + resolveWhatsAppGroupToolPolicy, +} from "./group-policy.js"; +import type { OpenClawConfig } from "./runtime-api.js"; + +const hoisted = vi.hoisted(() => ({ + sendPollWhatsApp: vi.fn(async () => ({ messageId: "wa-poll-1", toJid: "1555@s.whatsapp.net" })), +})); + +vi.mock("./runtime.js", () => ({ + getWhatsAppRuntime: () => ({ + logging: { + shouldLogVerbose: () => false, + }, + channel: { + whatsapp: { + sendPollWhatsApp: hoisted.sendPollWhatsApp, + }, + }, + }), +})); describe("whatsappPlugin outbound sendMedia", () => { it("forwards mediaLocalRoots to sendMessageWhatsApp", async () => { @@ -39,3 +69,113 @@ describe("whatsappPlugin outbound sendMedia", () => { expect(result).toMatchObject({ channel: "whatsapp", messageId: "msg-1" }); }); }); + +describe("whatsappPlugin outbound sendPoll", () => { + beforeEach(async () => { + vi.resetModules(); + }); + + it("threads cfg into runtime sendPollWhatsApp call", async () => { + const { cfg, poll, to, accountId } = createWhatsAppPollFixture(); + + const result = await whatsappPlugin.outbound!.sendPoll!({ + cfg, + to, + poll, + accountId, + }); + + expectWhatsAppPollSent(hoisted.sendPollWhatsApp, { cfg, poll, to, accountId }); + expect(result).toEqual({ + channel: "whatsapp", + messageId: "wa-poll-1", + toJid: "1555@s.whatsapp.net", + }); + }); +}); + +describe("whatsapp directory", () => { + const runtimeEnv = createDirectoryTestRuntime() as never; + + it("lists peers and groups from config", async () => { + const cfg = { + channels: { + whatsapp: { + authDir: "/tmp/wa-auth", + allowFrom: [ + "whatsapp:+15551230001", + "15551230002@s.whatsapp.net", + "120363999999999999@g.us", + ], + groups: { + "120363111111111111@g.us": {}, + "120363222222222222@g.us": {}, + }, + }, + }, + } as unknown as OpenClawConfig; + + const directory = expectDirectorySurface(whatsappPlugin.directory); + + await expect( + directory.listPeers({ + cfg, + accountId: undefined, + query: undefined, + limit: undefined, + runtime: runtimeEnv, + }), + ).resolves.toEqual( + expect.arrayContaining([ + { kind: "user", id: "+15551230001" }, + { kind: "user", id: "+15551230002" }, + ]), + ); + + await expect( + directory.listGroups({ + cfg, + accountId: undefined, + query: undefined, + limit: undefined, + runtime: runtimeEnv, + }), + ).resolves.toEqual( + expect.arrayContaining([ + { kind: "group", id: "120363111111111111@g.us" }, + { kind: "group", id: "120363222222222222@g.us" }, + ]), + ); + }); +}); + +describe("whatsapp group policy", () => { + it("uses generic channel group policy helpers", () => { + const cfg = { + channels: { + whatsapp: { + groups: { + "1203630@g.us": { + requireMention: false, + tools: { deny: ["exec"] }, + }, + "*": { + requireMention: true, + tools: { allow: ["message.send"] }, + }, + }, + }, + }, + // oxlint-disable-next-line typescript/no-explicit-any + } as any; + + expect(resolveWhatsAppGroupRequireMention({ cfg, groupId: "1203630@g.us" })).toBe(false); + expect(resolveWhatsAppGroupRequireMention({ cfg, groupId: "other@g.us" })).toBe(true); + expect(resolveWhatsAppGroupToolPolicy({ cfg, groupId: "1203630@g.us" })).toEqual({ + deny: ["exec"], + }); + expect(resolveWhatsAppGroupToolPolicy({ cfg, groupId: "other@g.us" })).toEqual({ + allow: ["message.send"], + }); + }); +}); diff --git a/extensions/whatsapp/src/group-policy.test.ts b/extensions/whatsapp/src/group-policy.test.ts deleted file mode 100644 index bd1aecdeaa5..00000000000 --- a/extensions/whatsapp/src/group-policy.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - resolveWhatsAppGroupRequireMention, - resolveWhatsAppGroupToolPolicy, -} from "./group-policy.js"; - -describe("whatsapp group policy", () => { - it("uses generic channel group policy helpers", () => { - const cfg = { - channels: { - whatsapp: { - groups: { - "1203630@g.us": { - requireMention: false, - tools: { deny: ["exec"] }, - }, - "*": { - requireMention: true, - tools: { allow: ["message.send"] }, - }, - }, - }, - }, - // oxlint-disable-next-line typescript/no-explicit-any - } as any; - - expect(resolveWhatsAppGroupRequireMention({ cfg, groupId: "1203630@g.us" })).toBe(false); - expect(resolveWhatsAppGroupRequireMention({ cfg, groupId: "other@g.us" })).toBe(true); - expect(resolveWhatsAppGroupToolPolicy({ cfg, groupId: "1203630@g.us" })).toEqual({ - deny: ["exec"], - }); - expect(resolveWhatsAppGroupToolPolicy({ cfg, groupId: "other@g.us" })).toEqual({ - allow: ["message.send"], - }); - }); -});