Files
Moltbot/src/gateway/chat-attachments.test.ts
2026-02-21 21:44:50 +00:00

196 lines
5.8 KiB
TypeScript

import { describe, expect, it, vi } 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/);
});
});
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("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);
});
});
describe("shared attachment validation", () => {
it("rejects invalid base64 content for both builder and parser", async () => {
const bad: ChatAttachment = {
type: "image",
mimeType: "image/png",
fileName: "dot.png",
content: "%not-base64%",
};
expect(() => buildMessageWithAttachments("x", [bad])).toThrow(/base64/i);
await expect(
parseMessageWithAttachments("x", [bad], { log: { warn: () => {} } }),
).rejects.toThrow(/base64/i);
});
it("rejects images over limit for both builder and parser without decoding base64", async () => {
const big = "A".repeat(10_000);
const att: ChatAttachment = {
type: "image",
mimeType: "image/png",
fileName: "big.png",
content: big,
};
const fromSpy = vi.spyOn(Buffer, "from");
try {
expect(() => buildMessageWithAttachments("x", [att], { maxBytes: 16 })).toThrow(
/exceeds size limit/i,
);
await expect(
parseMessageWithAttachments("x", [att], { maxBytes: 16, log: { warn: () => {} } }),
).rejects.toThrow(/exceeds size limit/i);
const base64Calls = fromSpy.mock.calls.filter((args) => (args as unknown[])[1] === "base64");
expect(base64Calls).toHaveLength(0);
} finally {
fromSpy.mockRestore();
}
});
});