| import { describe, expect, it } from "vitest"; |
| import type { OpenClawConfig } from "../config/config.js"; |
| import { |
| buildIMessageInboundContext, |
| resolveIMessageInboundDecision, |
| } from "./monitor/inbound-processing.js"; |
| import { parseIMessageNotification } from "./monitor/parse-notification.js"; |
| import type { IMessagePayload } from "./monitor/types.js"; |
|
|
| function baseCfg(): OpenClawConfig { |
| return { |
| channels: { |
| imessage: { |
| dmPolicy: "open", |
| allowFrom: ["*"], |
| groupPolicy: "open", |
| groups: { "*": { requireMention: true } }, |
| }, |
| }, |
| session: { mainKey: "main" }, |
| messages: { |
| groupChat: { mentionPatterns: ["@openclaw"] }, |
| }, |
| } as unknown as OpenClawConfig; |
| } |
|
|
| function resolve(params: { |
| cfg?: OpenClawConfig; |
| message: IMessagePayload; |
| storeAllowFrom?: string[]; |
| }) { |
| const cfg = params.cfg ?? baseCfg(); |
| const groupHistories = new Map(); |
| return resolveIMessageInboundDecision({ |
| cfg, |
| accountId: "default", |
| message: params.message, |
| opts: {}, |
| messageText: (params.message.text ?? "").trim(), |
| bodyText: (params.message.text ?? "").trim(), |
| allowFrom: ["*"], |
| groupAllowFrom: [], |
| groupPolicy: cfg.channels?.imessage?.groupPolicy ?? "open", |
| dmPolicy: cfg.channels?.imessage?.dmPolicy ?? "pairing", |
| storeAllowFrom: params.storeAllowFrom ?? [], |
| historyLimit: 0, |
| groupHistories, |
| }); |
| } |
|
|
| function resolveDispatchDecision(params: { |
| cfg: OpenClawConfig; |
| message: IMessagePayload; |
| groupHistories?: Parameters<typeof resolveIMessageInboundDecision>[0]["groupHistories"]; |
| }) { |
| const groupHistories = params.groupHistories ?? new Map(); |
| const decision = resolveIMessageInboundDecision({ |
| cfg: params.cfg, |
| accountId: "default", |
| message: params.message, |
| opts: {}, |
| messageText: params.message.text ?? "", |
| bodyText: params.message.text ?? "", |
| allowFrom: ["*"], |
| groupAllowFrom: [], |
| groupPolicy: "open", |
| dmPolicy: "open", |
| storeAllowFrom: [], |
| historyLimit: 0, |
| groupHistories, |
| }); |
| expect(decision.kind).toBe("dispatch"); |
| if (decision.kind !== "dispatch") { |
| throw new Error("expected dispatch decision"); |
| } |
| return { decision, groupHistories }; |
| } |
|
|
| function buildDispatchContextPayload(params: { cfg: OpenClawConfig; message: IMessagePayload }) { |
| const { cfg, message } = params; |
| const { decision, groupHistories } = resolveDispatchDecision({ cfg, message }); |
|
|
| const { ctxPayload } = buildIMessageInboundContext({ |
| cfg, |
| decision, |
| message, |
| historyLimit: 0, |
| groupHistories, |
| }); |
|
|
| return ctxPayload; |
| } |
|
|
| describe("imessage monitor gating + envelope builders", () => { |
| it("parseIMessageNotification rejects malformed payloads", () => { |
| expect( |
| parseIMessageNotification({ |
| message: { chat_id: 1, sender: { nested: "nope" } }, |
| }), |
| ).toBeNull(); |
| }); |
|
|
| it("drops group messages without mention by default", () => { |
| const decision = resolve({ |
| message: { |
| id: 1, |
| chat_id: 99, |
| sender: "+15550001111", |
| is_from_me: false, |
| text: "hello group", |
| is_group: true, |
| }, |
| }); |
| expect(decision.kind).toBe("drop"); |
| if (decision.kind !== "drop") { |
| throw new Error("expected drop decision"); |
| } |
| expect(decision.reason).toBe("no mention"); |
| }); |
|
|
| it("dispatches group messages with mention and builds a group envelope", () => { |
| const cfg = baseCfg(); |
| const message: IMessagePayload = { |
| id: 3, |
| chat_id: 42, |
| sender: "+15550002222", |
| is_from_me: false, |
| text: "@openclaw ping", |
| is_group: true, |
| chat_name: "Lobster Squad", |
| participants: ["+1555", "+1556"], |
| }; |
| const ctxPayload = buildDispatchContextPayload({ cfg, message }); |
|
|
| expect(ctxPayload.ChatType).toBe("group"); |
| expect(ctxPayload.SessionKey).toBe("agent:main:imessage:group:42"); |
| expect(String(ctxPayload.Body ?? "")).toContain("+15550002222:"); |
| expect(String(ctxPayload.Body ?? "")).not.toContain("[from:"); |
| expect(ctxPayload.To).toBe("chat_id:42"); |
| }); |
|
|
| it("includes reply-to context fields + suffix", () => { |
| const cfg = baseCfg(); |
| const message: IMessagePayload = { |
| id: 5, |
| chat_id: 55, |
| sender: "+15550001111", |
| is_from_me: false, |
| text: "replying now", |
| is_group: false, |
| reply_to_id: 9001, |
| reply_to_text: "original message", |
| reply_to_sender: "+15559998888", |
| }; |
| const ctxPayload = buildDispatchContextPayload({ cfg, message }); |
|
|
| expect(ctxPayload.ReplyToId).toBe("9001"); |
| expect(ctxPayload.ReplyToBody).toBe("original message"); |
| expect(ctxPayload.ReplyToSender).toBe("+15559998888"); |
| expect(String(ctxPayload.Body ?? "")).toContain("[Replying to +15559998888 id:9001]"); |
| expect(String(ctxPayload.Body ?? "")).toContain("original message"); |
| }); |
|
|
| it("treats configured chat_id as a group session even when is_group is false", () => { |
| const cfg = baseCfg(); |
| cfg.channels ??= {}; |
| cfg.channels.imessage ??= {}; |
| cfg.channels.imessage.groups = { "2": { requireMention: false } }; |
|
|
| const groupHistories = new Map(); |
| const message: IMessagePayload = { |
| id: 14, |
| chat_id: 2, |
| sender: "+15550001111", |
| is_from_me: false, |
| text: "hello", |
| is_group: false, |
| }; |
| const { decision } = resolveDispatchDecision({ cfg, message, groupHistories }); |
| expect(decision.isGroup).toBe(true); |
| expect(decision.route.sessionKey).toBe("agent:main:imessage:group:2"); |
| }); |
|
|
| it("allows group messages when requireMention is true but no mentionPatterns exist", () => { |
| const cfg = baseCfg(); |
| cfg.messages ??= {}; |
| cfg.messages.groupChat ??= {}; |
| cfg.messages.groupChat.mentionPatterns = []; |
|
|
| const groupHistories = new Map(); |
| const decision = resolveIMessageInboundDecision({ |
| cfg, |
| accountId: "default", |
| message: { |
| id: 12, |
| chat_id: 777, |
| sender: "+15550001111", |
| is_from_me: false, |
| text: "hello group", |
| is_group: true, |
| }, |
| opts: {}, |
| messageText: "hello group", |
| bodyText: "hello group", |
| allowFrom: ["*"], |
| groupAllowFrom: [], |
| groupPolicy: "open", |
| dmPolicy: "open", |
| storeAllowFrom: [], |
| historyLimit: 0, |
| groupHistories, |
| }); |
| expect(decision.kind).toBe("dispatch"); |
| }); |
|
|
| it("blocks group messages when imessage.groups is set without a wildcard", () => { |
| const cfg = baseCfg(); |
| cfg.channels ??= {}; |
| cfg.channels.imessage ??= {}; |
| cfg.channels.imessage.groups = { "99": { requireMention: false } }; |
|
|
| const groupHistories = new Map(); |
| const decision = resolveIMessageInboundDecision({ |
| cfg, |
| accountId: "default", |
| message: { |
| id: 13, |
| chat_id: 123, |
| sender: "+15550001111", |
| is_from_me: false, |
| text: "@openclaw hello", |
| is_group: true, |
| }, |
| opts: {}, |
| messageText: "@openclaw hello", |
| bodyText: "@openclaw hello", |
| allowFrom: ["*"], |
| groupAllowFrom: [], |
| groupPolicy: "open", |
| dmPolicy: "open", |
| storeAllowFrom: [], |
| historyLimit: 0, |
| groupHistories, |
| }); |
| expect(decision.kind).toBe("drop"); |
| }); |
|
|
| it("honors group allowlist and ignores pairing-store senders in groups", () => { |
| const cfg = baseCfg(); |
| cfg.channels ??= {}; |
| cfg.channels.imessage ??= {}; |
| cfg.channels.imessage.groupPolicy = "allowlist"; |
|
|
| const groupHistories = new Map(); |
| const denied = resolveIMessageInboundDecision({ |
| cfg, |
| accountId: "default", |
| message: { |
| id: 3, |
| chat_id: 202, |
| sender: "+15550003333", |
| is_from_me: false, |
| text: "@openclaw hi", |
| is_group: true, |
| }, |
| opts: {}, |
| messageText: "@openclaw hi", |
| bodyText: "@openclaw hi", |
| allowFrom: ["*"], |
| groupAllowFrom: ["chat_id:101"], |
| groupPolicy: "allowlist", |
| dmPolicy: "pairing", |
| storeAllowFrom: ["+15550003333"], |
| historyLimit: 0, |
| groupHistories, |
| }); |
| expect(denied.kind).toBe("drop"); |
|
|
| const allowed = resolveIMessageInboundDecision({ |
| cfg, |
| accountId: "default", |
| message: { |
| id: 33, |
| chat_id: 101, |
| sender: "+15550003333", |
| is_from_me: false, |
| text: "@openclaw ok", |
| is_group: true, |
| }, |
| opts: {}, |
| messageText: "@openclaw ok", |
| bodyText: "@openclaw ok", |
| allowFrom: ["*"], |
| groupAllowFrom: ["chat_id:101"], |
| groupPolicy: "allowlist", |
| dmPolicy: "pairing", |
| storeAllowFrom: ["+15550003333"], |
| historyLimit: 0, |
| groupHistories, |
| }); |
| expect(allowed.kind).toBe("dispatch"); |
| }); |
|
|
| it("blocks group messages when groupPolicy is disabled", () => { |
| const cfg = baseCfg(); |
| cfg.channels ??= {}; |
| cfg.channels.imessage ??= {}; |
| cfg.channels.imessage.groupPolicy = "disabled"; |
|
|
| const groupHistories = new Map(); |
| const decision = resolveIMessageInboundDecision({ |
| cfg, |
| accountId: "default", |
| message: { |
| id: 10, |
| chat_id: 303, |
| sender: "+15550003333", |
| is_from_me: false, |
| text: "@openclaw hi", |
| is_group: true, |
| }, |
| opts: {}, |
| messageText: "@openclaw hi", |
| bodyText: "@openclaw hi", |
| allowFrom: ["*"], |
| groupAllowFrom: [], |
| groupPolicy: "disabled", |
| dmPolicy: "open", |
| storeAllowFrom: [], |
| historyLimit: 0, |
| groupHistories, |
| }); |
| expect(decision.kind).toBe("drop"); |
| }); |
| }); |
|
|