mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-26 09:21:55 +00:00
test: collapse messaging target test suites
This commit is contained in:
@@ -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"] });
|
||||
});
|
||||
});
|
||||
@@ -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"] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
149
extensions/signal/src/core.test.ts
Normal file
149
extensions/signal/src/core.test.ts
Normal file
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,}/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,}/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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" }]);
|
||||
});
|
||||
});
|
||||
175
extensions/signal/src/outbound.test.ts
Normal file
175
extensions/signal/src/outbound.test.ts
Normal file
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user