// Imessage tests cover monitor.media policy plugin behavior. import type { waitForTransportReady } from "openclaw/plugin-sdk/transport-ready-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { createIMessageRpcClient } from "./client.js"; import { monitorIMessageProvider } from "./monitor.js"; import type { stageIMessageAttachments } from "./monitor/media-staging.js"; import { formatIMessageInboundMediaBody, resolveIMessageInboundMediaInput, } from "./monitor/monitor-provider.js"; const waitForTransportReadyMock = vi.hoisted(() => vi.fn(async () => {}), ); const createIMessageRpcClientMock = vi.hoisted(() => vi.fn()); const stageIMessageAttachmentsMock = vi.hoisted(() => vi.fn()); const readChannelAllowFromStoreMock = vi.hoisted(() => vi.fn(async () => [] as string[])); vi.mock("openclaw/plugin-sdk/transport-ready-runtime", () => ({ waitForTransportReady: waitForTransportReadyMock, })); vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, readChannelAllowFromStore: readChannelAllowFromStoreMock, recordInboundSession: vi.fn(), upsertChannelPairingRequest: vi.fn(), }; }); vi.mock("openclaw/plugin-sdk/channel-inbound", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, createChannelInboundDebouncer: vi.fn((opts) => ({ debouncer: { enqueue: async (entry: unknown) => await opts.onFlush([entry]), }, })), shouldDebounceTextInbound: vi.fn(() => false), }; }); vi.mock("./client.js", () => ({ createIMessageRpcClient: createIMessageRpcClientMock, })); vi.mock("./monitor/abort-handler.js", () => ({ attachIMessageMonitorAbortHandler: vi.fn(() => () => {}), })); vi.mock("./monitor/media-staging.js", () => ({ stageIMessageAttachments: stageIMessageAttachmentsMock, })); describe("iMessage monitor attachment policy", () => { beforeEach(() => { createIMessageRpcClientMock.mockReset(); stageIMessageAttachmentsMock.mockReset(); readChannelAllowFromStoreMock.mockReset().mockResolvedValue([]); }); it("does not stage local attachments for messages dropped by inbound policy", async () => { stageIMessageAttachmentsMock.mockResolvedValue({ attachments: [], unavailableCount: 0 }); readChannelAllowFromStoreMock.mockResolvedValue([]); const attachmentPath = "/Users/openclaw/Library/Messages/Attachments/AA/BB/photo.heic"; let onNotification: | ((message: { method: string; params: unknown }) => void | Promise) | undefined; const client = { request: vi.fn(async () => ({ subscription: 1 })), waitForClose: vi.fn(async () => { void onNotification?.({ method: "message", params: { message: { id: 1, chat_id: 123, sender: "+15550001111", is_from_me: false, is_group: true, text: "no mention here", attachments: [ { original_path: attachmentPath, mime_type: "image/heic", missing: false, }, ], }, }, }); await Promise.resolve(); await Promise.resolve(); }), stop: vi.fn(async () => {}), }; createIMessageRpcClientMock.mockImplementation(async (params) => { if (!params?.onNotification) { throw new Error("expected iMessage notification handler"); } onNotification = params.onNotification; return client as never; }); await monitorIMessageProvider({ config: { channels: { imessage: { includeAttachments: true, attachmentRoots: ["/Users/*/Library/Messages/Attachments"], dmPolicy: "open", groupPolicy: "open", groups: { "*": { requireMention: true } }, }, }, messages: { groupChat: { mentionPatterns: ["@openclaw"] } }, session: { mainKey: "main" }, } as never, }); await vi.waitFor(() => expect(readChannelAllowFromStoreMock).toHaveBeenCalled()); expect(stageIMessageAttachmentsMock).not.toHaveBeenCalled(); }); it("admits attachment-only messages that are marked missing", async () => { const attachment = { original_path: "/Users/openclaw/Library/Messages/Attachments/missing.heic", mime_type: "image/heic", missing: true, }; expect( resolveIMessageInboundMediaInput({ messageText: "", attachments: [attachment], effectiveAttachmentRoots: [], }), ).toEqual({ bodyText: "", mediaPlaceholder: "", mediaCandidates: [attachment], rawMediaAttachments: [], }); }); it("uses the first materialized attachment type when earlier media is unavailable", () => { const missingImage = { original_path: "/Users/openclaw/Library/Messages/Attachments/missing.heic", mime_type: "image/heic", missing: true, }; const availableDocument = { original_path: "/Users/openclaw/Library/Messages/Attachments/report.pdf", mime_type: "application/pdf", missing: false, }; expect( resolveIMessageInboundMediaInput({ messageText: "", attachments: [missingImage, availableDocument], effectiveAttachmentRoots: ["/Users/openclaw/Library/Messages/Attachments"], }), ).toMatchObject({ bodyText: "", mediaPlaceholder: "", mediaCandidates: [missingImage, availableDocument], rawMediaAttachments: [ { path: availableDocument.original_path, contentType: "application/pdf" }, ], }); expect( formatIMessageInboundMediaBody({ messageText: "", optimisticPlaceholder: "", mediaAttachments: [{ contentType: "application/pdf" }], unavailableCount: 1, }), ).toBe("\n\n[imessage attachment unavailable]"); }); });