import { describe, expect, it, vi } from "vitest"; import { countOutboundMedia, deliverFormattedTextWithAttachments, deliverTextOrMediaReply, hasOutboundMedia, hasOutboundReplyContent, hasOutboundText, isNumericTargetId, resolveOutboundMediaUrls, resolveSendableOutboundReplyParts, resolveTextChunksWithFallback, sendMediaWithLeadingCaption, sendPayloadWithChunkedTextAndMedia, } from "./reply-payload.js"; describe("sendPayloadWithChunkedTextAndMedia", () => { it("returns empty result when payload has no text and no media", async () => { const result = await sendPayloadWithChunkedTextAndMedia({ ctx: { payload: {} }, sendText: async () => ({ channel: "test", messageId: "text" }), sendMedia: async () => ({ channel: "test", messageId: "media" }), emptyResult: { channel: "test", messageId: "" }, }); expect(result).toEqual({ channel: "test", messageId: "" }); }); it("sends first media with text and remaining media without text", async () => { const calls: Array<{ text: string; mediaUrl: string }> = []; const result = await sendPayloadWithChunkedTextAndMedia({ ctx: { payload: { text: "hello", mediaUrls: ["https://a", "https://b"] }, }, sendText: async () => ({ channel: "test", messageId: "text" }), sendMedia: async (ctx) => { calls.push({ text: ctx.text, mediaUrl: ctx.mediaUrl }); return { channel: "test", messageId: ctx.mediaUrl }; }, emptyResult: { channel: "test", messageId: "" }, }); expect(calls).toEqual([ { text: "hello", mediaUrl: "https://a" }, { text: "", mediaUrl: "https://b" }, ]); expect(result).toEqual({ channel: "test", messageId: "https://b" }); }); it("chunks text and sends each chunk", async () => { const chunks: string[] = []; const result = await sendPayloadWithChunkedTextAndMedia({ ctx: { payload: { text: "alpha beta gamma" } }, textChunkLimit: 5, chunker: () => ["alpha", "beta", "gamma"], sendText: async (ctx) => { chunks.push(ctx.text); return { channel: "test", messageId: ctx.text }; }, sendMedia: async () => ({ channel: "test", messageId: "media" }), emptyResult: { channel: "test", messageId: "" }, }); expect(chunks).toEqual(["alpha", "beta", "gamma"]); expect(result).toEqual({ channel: "test", messageId: "gamma" }); }); it("detects numeric target IDs", () => { expect(isNumericTargetId("12345")).toBe(true); expect(isNumericTargetId(" 987 ")).toBe(true); expect(isNumericTargetId("ab12")).toBe(false); expect(isNumericTargetId("")).toBe(false); }); }); describe("resolveOutboundMediaUrls", () => { it.each([ { name: "prefers mediaUrls over the legacy single-media field", payload: { mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"], mediaUrl: "https://example.com/legacy.png", }, expected: ["https://example.com/a.png", "https://example.com/b.png"], }, { name: "falls back to the legacy single-media field", payload: { mediaUrl: "https://example.com/legacy.png", }, expected: ["https://example.com/legacy.png"], }, ])("$name", ({ payload, expected }) => { expect(resolveOutboundMediaUrls(payload)).toEqual(expected); }); }); describe("countOutboundMedia", () => { it.each([ { name: "counts normalized media entries", payload: { mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"], }, expected: 2, }, { name: "counts legacy single-media payloads", payload: { mediaUrl: "https://example.com/legacy.png", }, expected: 1, }, ])("$name", ({ payload, expected }) => { expect(countOutboundMedia(payload)).toBe(expected); }); }); describe("hasOutboundMedia", () => { it("reports whether normalized payloads include media", () => { expect(hasOutboundMedia({ mediaUrls: ["https://example.com/a.png"] })).toBe(true); expect(hasOutboundMedia({ mediaUrl: "https://example.com/legacy.png" })).toBe(true); expect(hasOutboundMedia({})).toBe(false); }); }); describe("hasOutboundText", () => { it.each([ { name: "checks raw text presence by default", payload: { text: "hello" }, options: undefined, expected: true, }, { name: "treats whitespace-only text as present by default", payload: { text: " " }, options: undefined, expected: true, }, { name: "returns false when text is missing", payload: {}, options: undefined, expected: false, }, { name: "can trim whitespace-only text", payload: { text: " " }, options: { trim: true }, expected: false, }, { name: "keeps non-empty trimmed text", payload: { text: " hi " }, options: { trim: true }, expected: true, }, ])("$name", ({ payload, options, expected }) => { expect(hasOutboundText(payload, options)).toBe(expected); }); }); describe("hasOutboundReplyContent", () => { it.each([ { name: "detects text content", payload: { text: "hello" }, options: undefined, expected: true, }, { name: "detects media content", payload: { mediaUrl: "https://example.com/a.png" }, options: undefined, expected: true, }, { name: "returns false when text and media are both missing", payload: {}, options: undefined, expected: false, }, { name: "can ignore whitespace-only text", payload: { text: " " }, options: { trimText: true }, expected: false, }, { name: "still reports content when trimmed text is blank but media exists", payload: { text: " ", mediaUrls: ["https://example.com/a.png"] }, options: { trimText: true }, expected: true, }, ])("$name", ({ payload, options, expected }) => { expect(hasOutboundReplyContent(payload, options)).toBe(expected); }); }); describe("resolveSendableOutboundReplyParts", () => { it("normalizes missing text and trims media urls", () => { expect( resolveSendableOutboundReplyParts({ mediaUrls: [" https://example.com/a.png ", " "], }), ).toEqual({ text: "", trimmedText: "", mediaUrls: ["https://example.com/a.png"], mediaCount: 1, hasText: false, hasMedia: true, hasContent: true, }); }); it("accepts transformed text overrides", () => { expect( resolveSendableOutboundReplyParts( { text: "ignored", }, { text: " hello ", }, ), ).toEqual({ text: " hello ", trimmedText: "hello", mediaUrls: [], mediaCount: 0, hasText: true, hasMedia: false, hasContent: true, }); }); }); describe("resolveTextChunksWithFallback", () => { it.each([ { name: "returns existing chunks unchanged", text: "hello", chunks: ["a", "b"], expected: ["a", "b"], }, { name: "falls back to the full text when chunkers return nothing", text: "hello", chunks: [], expected: ["hello"], }, { name: "returns empty for empty text with no chunks", text: "", chunks: [], expected: [], }, ])("$name", ({ text, chunks, expected }) => { expect(resolveTextChunksWithFallback(text, chunks)).toEqual(expected); }); }); describe("deliverTextOrMediaReply", () => { it("sends media first with caption only on the first attachment", async () => { const sendMedia = vi.fn(async () => undefined); const sendText = vi.fn(async () => undefined); await expect( deliverTextOrMediaReply({ payload: { text: "hello", mediaUrls: ["https://a", "https://b"] }, text: "hello", sendText, sendMedia, }), ).resolves.toBe("media"); expect(sendMedia).toHaveBeenNthCalledWith(1, { mediaUrl: "https://a", caption: "hello", }); expect(sendMedia).toHaveBeenNthCalledWith(2, { mediaUrl: "https://b", caption: undefined, }); expect(sendText).not.toHaveBeenCalled(); }); it("falls back to chunked text delivery when there is no media", async () => { const sendMedia = vi.fn(async () => undefined); const sendText = vi.fn(async () => undefined); await expect( deliverTextOrMediaReply({ payload: { text: "alpha beta gamma" }, text: "alpha beta gamma", chunkText: () => ["alpha", "beta", "gamma"], sendText, sendMedia, }), ).resolves.toBe("text"); expect(sendText).toHaveBeenCalledTimes(3); expect(sendText).toHaveBeenNthCalledWith(1, "alpha"); expect(sendText).toHaveBeenNthCalledWith(2, "beta"); expect(sendText).toHaveBeenNthCalledWith(3, "gamma"); expect(sendMedia).not.toHaveBeenCalled(); }); it("returns empty when chunking produces no sendable text", async () => { const sendMedia = vi.fn(async () => undefined); const sendText = vi.fn(async () => undefined); await expect( deliverTextOrMediaReply({ payload: { text: " " }, text: " ", chunkText: () => [], sendText, sendMedia, }), ).resolves.toBe("empty"); expect(sendText).not.toHaveBeenCalled(); expect(sendMedia).not.toHaveBeenCalled(); }); it("ignores blank media urls before sending", async () => { const sendMedia = vi.fn(async () => undefined); const sendText = vi.fn(async () => undefined); await expect( deliverTextOrMediaReply({ payload: { text: "hello", mediaUrls: [" ", " https://a "] }, text: "hello", sendText, sendMedia, }), ).resolves.toBe("media"); expect(sendMedia).toHaveBeenCalledTimes(1); expect(sendMedia).toHaveBeenCalledWith({ mediaUrl: "https://a", caption: "hello", }); }); }); describe("sendMediaWithLeadingCaption", () => { it("passes leading-caption metadata to async error handlers", async () => { const send = vi .fn<({ mediaUrl, caption }: { mediaUrl: string; caption?: string }) => Promise>() .mockRejectedValueOnce(new Error("boom")) .mockResolvedValueOnce(undefined); const onError = vi.fn(async () => undefined); await expect( sendMediaWithLeadingCaption({ mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"], caption: "hello", send, onError, }), ).resolves.toBe(true); expect(onError).toHaveBeenCalledWith( expect.objectContaining({ mediaUrl: "https://example.com/a.png", caption: "hello", index: 0, isFirst: true, }), ); expect(send).toHaveBeenNthCalledWith(2, { mediaUrl: "https://example.com/b.png", caption: undefined, }); }); }); describe("deliverFormattedTextWithAttachments", () => { it("combines attachment links and forwards replyToId", async () => { const send = vi.fn(async () => undefined); await expect( deliverFormattedTextWithAttachments({ payload: { text: "hello", mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"], replyToId: "r1", }, send, }), ).resolves.toBe(true); expect(send).toHaveBeenCalledWith({ text: "hello\n\nAttachment: https://example.com/a.png\nAttachment: https://example.com/b.png", replyToId: "r1", }); }); });