UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

307 lines (277 loc) 9.01 kB
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; import { describe, expect, it, vi } from "vitest"; import { linePlugin } from "./channel.js"; import { setLineRuntime } from "./runtime.js"; type LineRuntimeMocks = { pushMessageLine: ReturnType<typeof vi.fn>; pushMessagesLine: ReturnType<typeof vi.fn>; pushFlexMessage: ReturnType<typeof vi.fn>; pushTemplateMessage: ReturnType<typeof vi.fn>; pushLocationMessage: ReturnType<typeof vi.fn>; pushTextMessageWithQuickReplies: ReturnType<typeof vi.fn>; createQuickReplyItems: ReturnType<typeof vi.fn>; buildTemplateMessageFromPayload: ReturnType<typeof vi.fn>; sendMessageLine: ReturnType<typeof vi.fn>; chunkMarkdownText: ReturnType<typeof vi.fn>; resolveLineAccount: ReturnType<typeof vi.fn>; resolveTextChunkLimit: ReturnType<typeof vi.fn>; }; function createRuntime(): { runtime: PluginRuntime; mocks: LineRuntimeMocks } { const pushMessageLine = vi.fn(async () => ({ messageId: "m-text", chatId: "c1" })); const pushMessagesLine = vi.fn(async () => ({ messageId: "m-batch", chatId: "c1" })); const pushFlexMessage = vi.fn(async () => ({ messageId: "m-flex", chatId: "c1" })); const pushTemplateMessage = vi.fn(async () => ({ messageId: "m-template", chatId: "c1" })); const pushLocationMessage = vi.fn(async () => ({ messageId: "m-loc", chatId: "c1" })); const pushTextMessageWithQuickReplies = vi.fn(async () => ({ messageId: "m-quick", chatId: "c1", })); const createQuickReplyItems = vi.fn((labels: string[]) => ({ items: labels })); const buildTemplateMessageFromPayload = vi.fn(() => ({ type: "buttons" })); const sendMessageLine = vi.fn(async () => ({ messageId: "m-media", chatId: "c1" })); const chunkMarkdownText = vi.fn((text: string) => [text]); const resolveTextChunkLimit = vi.fn(() => 123); const resolveLineAccount = vi.fn( ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => { const resolved = accountId ?? "default"; const lineConfig = (cfg.channels?.line ?? {}) as { accounts?: Record<string, Record<string, unknown>>; }; const accountConfig = resolved !== "default" ? (lineConfig.accounts?.[resolved] ?? {}) : {}; return { accountId: resolved, config: { ...lineConfig, ...accountConfig }, }; }, ); const runtime = { channel: { line: { pushMessageLine, pushMessagesLine, pushFlexMessage, pushTemplateMessage, pushLocationMessage, pushTextMessageWithQuickReplies, createQuickReplyItems, buildTemplateMessageFromPayload, sendMessageLine, resolveLineAccount, }, text: { chunkMarkdownText, resolveTextChunkLimit, }, }, } as unknown as PluginRuntime; return { runtime, mocks: { pushMessageLine, pushMessagesLine, pushFlexMessage, pushTemplateMessage, pushLocationMessage, pushTextMessageWithQuickReplies, createQuickReplyItems, buildTemplateMessageFromPayload, sendMessageLine, chunkMarkdownText, resolveLineAccount, resolveTextChunkLimit, }, }; } describe("linePlugin outbound.sendPayload", () => { it("sends flex message without dropping text", async () => { const { runtime, mocks } = createRuntime(); setLineRuntime(runtime); const cfg = { channels: { line: {} } } as OpenClawConfig; const payload = { text: "Now playing:", channelData: { line: { flexMessage: { altText: "Now playing", contents: { type: "bubble" }, }, }, }, }; await linePlugin.outbound.sendPayload({ to: "line:group:1", payload, accountId: "default", cfg, }); expect(mocks.pushFlexMessage).toHaveBeenCalledTimes(1); expect(mocks.pushMessageLine).toHaveBeenCalledWith("line:group:1", "Now playing:", { verbose: false, accountId: "default", }); }); it("sends template message without dropping text", async () => { const { runtime, mocks } = createRuntime(); setLineRuntime(runtime); const cfg = { channels: { line: {} } } as OpenClawConfig; const payload = { text: "Choose one:", channelData: { line: { templateMessage: { type: "confirm", text: "Continue?", confirmLabel: "Yes", confirmData: "yes", cancelLabel: "No", cancelData: "no", }, }, }, }; await linePlugin.outbound.sendPayload({ to: "line:user:1", payload, accountId: "default", cfg, }); expect(mocks.buildTemplateMessageFromPayload).toHaveBeenCalledTimes(1); expect(mocks.pushTemplateMessage).toHaveBeenCalledTimes(1); expect(mocks.pushMessageLine).toHaveBeenCalledWith("line:user:1", "Choose one:", { verbose: false, accountId: "default", }); }); it("attaches quick replies when no text chunks are present", async () => { const { runtime, mocks } = createRuntime(); setLineRuntime(runtime); const cfg = { channels: { line: {} } } as OpenClawConfig; const payload = { channelData: { line: { quickReplies: ["One", "Two"], flexMessage: { altText: "Card", contents: { type: "bubble" }, }, }, }, }; await linePlugin.outbound.sendPayload({ to: "line:user:2", payload, accountId: "default", cfg, }); expect(mocks.pushFlexMessage).not.toHaveBeenCalled(); expect(mocks.pushMessagesLine).toHaveBeenCalledWith( "line:user:2", [ { type: "flex", altText: "Card", contents: { type: "bubble" }, quickReply: { items: ["One", "Two"] }, }, ], { verbose: false, accountId: "default" }, ); expect(mocks.createQuickReplyItems).toHaveBeenCalledWith(["One", "Two"]); }); it("sends media before quick-reply text so buttons stay visible", async () => { const { runtime, mocks } = createRuntime(); setLineRuntime(runtime); const cfg = { channels: { line: {} } } as OpenClawConfig; const payload = { text: "Hello", mediaUrl: "https://example.com/img.jpg", channelData: { line: { quickReplies: ["One", "Two"], }, }, }; await linePlugin.outbound.sendPayload({ to: "line:user:3", payload, accountId: "default", cfg, }); expect(mocks.sendMessageLine).toHaveBeenCalledWith("line:user:3", "", { verbose: false, mediaUrl: "https://example.com/img.jpg", accountId: "default", }); expect(mocks.pushTextMessageWithQuickReplies).toHaveBeenCalledWith( "line:user:3", "Hello", ["One", "Two"], { verbose: false, accountId: "default" }, ); const mediaOrder = mocks.sendMessageLine.mock.invocationCallOrder[0]; const quickReplyOrder = mocks.pushTextMessageWithQuickReplies.mock.invocationCallOrder[0]; expect(mediaOrder).toBeLessThan(quickReplyOrder); }); it("uses configured text chunk limit for payloads", async () => { const { runtime, mocks } = createRuntime(); setLineRuntime(runtime); const cfg = { channels: { line: { textChunkLimit: 123 } } } as OpenClawConfig; const payload = { text: "Hello world", channelData: { line: { flexMessage: { altText: "Card", contents: { type: "bubble" }, }, }, }, }; await linePlugin.outbound.sendPayload({ to: "line:user:3", payload, accountId: "primary", cfg, }); expect(mocks.resolveTextChunkLimit).toHaveBeenCalledWith(cfg, "line", "primary", { fallbackLimit: 5000, }); expect(mocks.chunkMarkdownText).toHaveBeenCalledWith("Hello world", 123); }); }); describe("linePlugin config.formatAllowFrom", () => { it("strips line:user: prefixes without lowercasing", () => { const formatted = linePlugin.config.formatAllowFrom({ allowFrom: ["line:user:UABC", "line:UDEF"], }); expect(formatted).toEqual(["UABC", "UDEF"]); }); }); describe("linePlugin groups.resolveRequireMention", () => { it("uses account-level group settings when provided", () => { const { runtime } = createRuntime(); setLineRuntime(runtime); const cfg = { channels: { line: { groups: { "*": { requireMention: false }, }, accounts: { primary: { groups: { "group-1": { requireMention: true }, }, }, }, }, }, } as OpenClawConfig; const requireMention = linePlugin.groups.resolveRequireMention({ cfg, accountId: "primary", groupId: "group-1", }); expect(requireMention).toBe(true); }); });