import { describe, expect, it } from "vitest"; import { buildMessageWithAttachments, type ChatAttachment, parseMessageWithAttachments, } from "./chat-attachments.js"; const PNG_1x1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII="; describe("buildMessageWithAttachments", () => { it("embeds a single image as data URL", () => { const msg = buildMessageWithAttachments("see this", [ { type: "image", mimeType: "image/png", fileName: "dot.png", content: PNG_1x1, }, ]); expect(msg).toContain("see this"); expect(msg).toContain(`data:image/png;base64,${PNG_1x1}`); expect(msg).toContain("![dot.png]"); }); it("rejects non-image mime types", () => { const bad: ChatAttachment = { type: "file", mimeType: "application/pdf", fileName: "a.pdf", content: "AAA", }; expect(() => buildMessageWithAttachments("x", [bad])).toThrow(/image/); }); it("rejects invalid base64 content", () => { const bad: ChatAttachment = { type: "image", mimeType: "image/png", fileName: "dot.png", content: "%not-base64%", }; expect(() => buildMessageWithAttachments("x", [bad])).toThrow(/base64/); }); it("rejects images over limit", () => { const big = Buffer.alloc(6_000_000, 0).toString("base64"); const att: ChatAttachment = { type: "image", mimeType: "image/png", fileName: "big.png", content: big, }; expect(() => buildMessageWithAttachments("x", [att], { maxBytes: 5_000_000 })).toThrow( /exceeds size limit/i, ); }); }); describe("parseMessageWithAttachments", () => { it("strips data URL prefix", async () => { const parsed = await parseMessageWithAttachments( "see this", [ { type: "image", mimeType: "image/png", fileName: "dot.png", content: `data:image/png;base64,${PNG_1x1}`, }, ], { log: { warn: () => {} } }, ); expect(parsed.images).toHaveLength(1); expect(parsed.images[0]?.mimeType).toBe("image/png"); expect(parsed.images[0]?.data).toBe(PNG_1x1); }); it("rejects invalid base64 content", async () => { await expect( parseMessageWithAttachments( "x", [ { type: "image", mimeType: "image/png", fileName: "dot.png", content: "%not-base64%", }, ], { log: { warn: () => {} } }, ), ).rejects.toThrow(/base64/i); }); it("rejects images over limit", async () => { const big = Buffer.alloc(6_000_000, 0).toString("base64"); await expect( parseMessageWithAttachments( "x", [ { type: "image", mimeType: "image/png", fileName: "big.png", content: big, }, ], { maxBytes: 5_000_000, log: { warn: () => {} } }, ), ).rejects.toThrow(/exceeds size limit/i); }); it("sniffs mime when missing", async () => { const logs: string[] = []; const parsed = await parseMessageWithAttachments( "see this", [ { type: "image", fileName: "dot.png", content: PNG_1x1, }, ], { log: { warn: (message) => logs.push(message) } }, ); expect(parsed.message).toBe("see this"); expect(parsed.images).toHaveLength(1); expect(parsed.images[0]?.mimeType).toBe("image/png"); expect(parsed.images[0]?.data).toBe(PNG_1x1); expect(logs).toHaveLength(0); }); it("drops non-image payloads and logs", async () => { const logs: string[] = []; const pdf = Buffer.from("%PDF-1.4\n").toString("base64"); const parsed = await parseMessageWithAttachments( "x", [ { type: "file", mimeType: "image/png", fileName: "not-image.pdf", content: pdf, }, ], { log: { warn: (message) => logs.push(message) } }, ); expect(parsed.images).toHaveLength(0); expect(logs).toHaveLength(1); expect(logs[0]).toMatch(/non-image/i); }); it("prefers sniffed mime type and logs mismatch", async () => { const logs: string[] = []; const parsed = await parseMessageWithAttachments( "x", [ { type: "image", mimeType: "image/jpeg", fileName: "dot.png", content: PNG_1x1, }, ], { log: { warn: (message) => logs.push(message) } }, ); expect(parsed.images).toHaveLength(1); expect(parsed.images[0]?.mimeType).toBe("image/png"); expect(logs).toHaveLength(1); expect(logs[0]).toMatch(/mime mismatch/i); }); it("drops unknown mime when sniff fails and logs", async () => { const logs: string[] = []; const unknown = Buffer.from("not an image").toString("base64"); const parsed = await parseMessageWithAttachments( "x", [{ type: "file", fileName: "unknown.bin", content: unknown }], { log: { warn: (message) => logs.push(message) } }, ); expect(parsed.images).toHaveLength(0); expect(logs).toHaveLength(1); expect(logs[0]).toMatch(/unable to detect image mime type/i); }); it("keeps valid images and drops invalid ones", async () => { const logs: string[] = []; const pdf = Buffer.from("%PDF-1.4\n").toString("base64"); const parsed = await parseMessageWithAttachments( "x", [ { type: "image", mimeType: "image/png", fileName: "dot.png", content: PNG_1x1, }, { type: "file", mimeType: "image/png", fileName: "not-image.pdf", content: pdf, }, ], { log: { warn: (message) => logs.push(message) } }, ); expect(parsed.images).toHaveLength(1); expect(parsed.images[0]?.mimeType).toBe("image/png"); expect(parsed.images[0]?.data).toBe(PNG_1x1); expect(logs.some((l) => /non-image/i.test(l))).toBe(true); }); });