

























@@ -35,6 +35,28 @@ import {
3535const PNG_1x1 =
3636"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
373738+type ParsedAttachments = Awaited<ReturnType<typeof parseMessageWithAttachments>>;
39+40+function pngAttachment(overrides: Partial<ChatAttachment> = {}): ChatAttachment {
41+return {
42+type: "image",
43+mimeType: "image/png",
44+fileName: "dot.png",
45+content: PNG_1x1,
46+ ...overrides,
47+};
48+}
49+50+function pdfAttachment(overrides: Partial<ChatAttachment> = {}): ChatAttachment {
51+return {
52+type: "file",
53+mimeType: "application/pdf",
54+fileName: "report.pdf",
55+content: Buffer.from("%PDF-1.4\n").toString("base64"),
56+ ...overrides,
57+};
58+}
59+3860async function parseWithWarnings(
3961message: string,
4062attachments: ChatAttachment[],
@@ -48,6 +70,21 @@ async function parseWithWarnings(
4870return { parsed, logs };
4971}
507273+async function parseTextOnlyAttachments(
74+message: string,
75+attachments: ChatAttachment[],
76+logs: string[],
77+infos?: string[],
78+) {
79+const log: NonNullable<Parameters<typeof parseMessageWithAttachments>[2]>["log"] = {
80+warn: (warning) => logs.push(warning),
81+};
82+if (infos) {
83+log.info = (info) => infos.push(info);
84+}
85+return parseMessageWithAttachments(message, attachments, { log, supportsImages: false });
86+}
87+5188async function cleanupOffloadedRefs(refs: { id: string }[]) {
5289await Promise.allSettled(refs.map((ref) => deleteMediaBufferMock(ref.id, "inbound")));
5390}
@@ -56,6 +93,30 @@ function savedMime() {
5693return saveMediaBufferMock.mock.calls[0]?.[1];
5794}
589596+function expectSingleInlinePng(parsed: ParsedAttachments) {
97+expect(parsed.images).toHaveLength(1);
98+expect(parsed.images[0]?.mimeType).toBe("image/png");
99+}
100+101+async function expectUnsupportedAttachmentReason(
102+attachments: ChatAttachment[],
103+opts: Parameters<typeof parseMessageWithAttachments>[2],
104+reason: UnsupportedAttachmentError["reason"],
105+) {
106+let caught: unknown;
107+try {
108+await parseMessageWithAttachments("x", attachments, {
109+log: { warn: () => {} },
110+ ...opts,
111+});
112+} catch (err) {
113+caught = err;
114+}
115+expect(caught).toBeInstanceOf(UnsupportedAttachmentError);
116+expect((caught as UnsupportedAttachmentError).reason).toBe(reason);
117+expect(saveMediaBufferMock).not.toHaveBeenCalled();
118+}
119+59120beforeEach(() => {
60121saveMediaBufferMock.mockClear();
61122deleteMediaBufferMock.mockClear();
@@ -67,14 +128,7 @@ afterEach(() => {
6712868129describe("buildMessageWithAttachments", () => {
69130it("embeds a single image as data URL", () => {
70-const msg = buildMessageWithAttachments("see this", [
71-{
72-type: "image",
73-mimeType: "image/png",
74-fileName: "dot.png",
75-content: PNG_1x1,
76-},
77-]);
131+const msg = buildMessageWithAttachments("see this", [pngAttachment()]);
78132expect(msg).toContain("see this");
79133expect(msg).toContain(`data:image/png;base64,${PNG_1x1}`);
80134expect(msg).toContain("![dot.png]");
@@ -95,46 +149,25 @@ describe("parseMessageWithAttachments", () => {
95149it("strips data URL prefix", async () => {
96150const parsed = await parseMessageWithAttachments(
97151"see this",
98-[
99-{
100-type: "image",
101-mimeType: "image/png",
102-fileName: "dot.png",
103-content: `data:image/png;base64,${PNG_1x1}`,
104-},
105-],
152+[pngAttachment({ content: `data:image/png;base64,${PNG_1x1}` })],
106153{ log: { warn: () => {} } },
107154);
108-expect(parsed.images).toHaveLength(1);
109-expect(parsed.images[0]?.mimeType).toBe("image/png");
155+expectSingleInlinePng(parsed);
110156expect(parsed.images[0]?.data).toBe(PNG_1x1);
111157});
112158113159it("sniffs mime when missing", async () => {
114160const { parsed, logs } = await parseWithWarnings("see this", [
115-{
116-type: "image",
117-fileName: "dot.png",
118-content: PNG_1x1,
119-},
161+pngAttachment({ mimeType: undefined }),
120162]);
121163expect(parsed.message).toBe("see this");
122-expect(parsed.images).toHaveLength(1);
123-expect(parsed.images[0]?.mimeType).toBe("image/png");
164+expectSingleInlinePng(parsed);
124165expect(parsed.images[0]?.data).toBe(PNG_1x1);
125166expect(logs).toHaveLength(0);
126167});
127168128169it("accepts non-image payloads and offloads them via the media store", async () => {
129-const pdf = Buffer.from("%PDF-1.4\n%µ¶\n1 0 obj\n<<>>\nendobj\n").toString("base64");
130-const { parsed, logs } = await parseWithWarnings("read this", [
131-{
132-type: "file",
133-mimeType: "application/pdf",
134-fileName: "report.pdf",
135-content: pdf,
136-},
137-]);
170+const { parsed, logs } = await parseWithWarnings("read this", [pdfAttachment()]);
138171expect(parsed.images).toHaveLength(0);
139172expect(parsed.offloadedRefs).toHaveLength(1);
140173const ref = parsed.offloadedRefs[0];
@@ -166,37 +199,16 @@ describe("parseMessageWithAttachments", () => {
166199167200it("prefers sniffed mime type and logs mismatch", async () => {
168201const { parsed, logs } = await parseWithWarnings("x", [
169-{
170-type: "image",
171-mimeType: "image/jpeg",
172-fileName: "dot.png",
173-content: PNG_1x1,
174-},
202+pngAttachment({ mimeType: "image/jpeg" }),
175203]);
176-expect(parsed.images).toHaveLength(1);
177-expect(parsed.images[0]?.mimeType).toBe("image/png");
204+expectSingleInlinePng(parsed);
178205expect(logs).toHaveLength(1);
179206expect(logs[0]).toMatch(/mime mismatch/i);
180207});
181208182209it("keeps image inline and offloads non-image side by side", async () => {
183-const pdf = Buffer.from("%PDF-1.4\n").toString("base64");
184-const { parsed } = await parseWithWarnings("x", [
185-{
186-type: "image",
187-mimeType: "image/png",
188-fileName: "dot.png",
189-content: PNG_1x1,
190-},
191-{
192-type: "file",
193-mimeType: "application/pdf",
194-fileName: "report.pdf",
195-content: pdf,
196-},
197-]);
198-expect(parsed.images).toHaveLength(1);
199-expect(parsed.images[0]?.mimeType).toBe("image/png");
210+const { parsed } = await parseWithWarnings("x", [pngAttachment(), pdfAttachment()]);
211+expectSingleInlinePng(parsed);
200212expect(parsed.offloadedRefs).toHaveLength(1);
201213expect(parsed.offloadedRefs[0]?.mimeType).toBe("application/pdf");
202214expect(parsed.imageOrder).toEqual(["inline"]);
@@ -215,14 +227,9 @@ describe("parseMessageWithAttachments", () => {
215227const bigPng = Buffer.alloc(2_100_000);
216228bigPng.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0);
217229const { parsed } = await parseWithWarnings("x", [
218-{ type: "file", mimeType: "application/pdf", fileName: "report.pdf", content: pdf },
219-{ type: "image", mimeType: "image/png", fileName: "dot.png", content: PNG_1x1 },
220-{
221-type: "image",
222-mimeType: "image/png",
223-fileName: "big.png",
224-content: bigPng.toString("base64"),
225-},
230+pdfAttachment({ content: pdf }),
231+pngAttachment(),
232+pngAttachment({ fileName: "big.png", content: bigPng.toString("base64") }),
226233]);
227234expect(parsed.images).toHaveLength(1);
228235expect(parsed.offloadedRefs.map((ref) => ref.mimeType)).toEqual([
@@ -333,79 +340,43 @@ describe("parseMessageWithAttachments validation errors", () => {
333340});
334341335342it("throws UnsupportedAttachmentError on non-image when acceptNonImage is false", async () => {
336-const pdf = Buffer.from("%PDF-1.4\n").toString("base64");
337-let caught: unknown;
338-try {
339-await parseMessageWithAttachments(
340-"x",
341-[{ type: "file", mimeType: "application/pdf", fileName: "a.pdf", content: pdf }],
342-{ log: { warn: () => {} }, acceptNonImage: false },
343-);
344-} catch (err) {
345-caught = err;
346-}
347-expect(caught).toBeInstanceOf(UnsupportedAttachmentError);
348-expect((caught as UnsupportedAttachmentError).reason).toBe("unsupported-non-image");
349-expect(saveMediaBufferMock).not.toHaveBeenCalled();
343+await expectUnsupportedAttachmentReason(
344+[pdfAttachment({ fileName: "a.pdf" })],
345+{ acceptNonImage: false },
346+"unsupported-non-image",
347+);
350348});
351349352350it("rejects generic-container payloads mislabeled as images when acceptNonImage is false", async () => {
353351const docx = Buffer.from("PK\u0003\u0004fake-docx-content").toString("base64");
354-let caught: unknown;
355-try {
356-await parseMessageWithAttachments(
357-"x",
358-[{ type: "file", mimeType: "image/png", fileName: "report.docx", content: docx }],
359-{ log: { warn: () => {} }, acceptNonImage: false },
360-);
361-} catch (err) {
362-caught = err;
363-}
364-expect(caught).toBeInstanceOf(UnsupportedAttachmentError);
365-expect((caught as UnsupportedAttachmentError).reason).toBe("unsupported-non-image");
366-expect(saveMediaBufferMock).not.toHaveBeenCalled();
352+await expectUnsupportedAttachmentReason(
353+[pdfAttachment({ mimeType: "image/png", fileName: "report.docx", content: docx })],
354+{ acceptNonImage: false },
355+"unsupported-non-image",
356+);
367357});
368358369359it("rejects generic-container payloads with image mime and image filename when acceptNonImage is false", async () => {
370360const zip = Buffer.from("PK\u0003\u0004zip-archive-bytes").toString("base64");
371-let caught: unknown;
372-try {
373-await parseMessageWithAttachments(
374-"x",
375-[{ type: "image", mimeType: "image/png", fileName: "fake.png", content: zip }],
376-{ log: { warn: () => {} }, acceptNonImage: false },
377-);
378-} catch (err) {
379-caught = err;
380-}
381-expect(caught).toBeInstanceOf(UnsupportedAttachmentError);
382-expect((caught as UnsupportedAttachmentError).reason).toBe("unsupported-non-image");
383-expect(saveMediaBufferMock).not.toHaveBeenCalled();
361+await expectUnsupportedAttachmentReason(
362+[pngAttachment({ fileName: "fake.png", content: zip })],
363+{ acceptNonImage: false },
364+"unsupported-non-image",
365+);
384366});
385367386368it("throws UnsupportedAttachmentError on image when supportsInlineImages is false", async () => {
387-let caught: unknown;
388-try {
389-await parseMessageWithAttachments(
390-"x",
391-[{ type: "image", mimeType: "image/png", fileName: "dot.png", content: PNG_1x1 }],
392-{ log: { warn: () => {} }, supportsInlineImages: false },
393-);
394-} catch (err) {
395-caught = err;
396-}
397-expect(caught).toBeInstanceOf(UnsupportedAttachmentError);
398-expect((caught as UnsupportedAttachmentError).reason).toBe("text-only-image");
399-expect(saveMediaBufferMock).not.toHaveBeenCalled();
369+await expectUnsupportedAttachmentReason(
370+[pngAttachment()],
371+{ supportsInlineImages: false },
372+"text-only-image",
373+);
400374});
401375402376it("still offloads non-image attachments when supportsInlineImages is false", async () => {
403-const pdf = Buffer.from("%PDF-1.4\n").toString("base64");
404-const { parsed } = await parseWithWarnings(
405-"x",
406-[{ type: "file", mimeType: "application/pdf", fileName: "a.pdf", content: pdf }],
407-{ supportsInlineImages: false },
408-);
377+const { parsed } = await parseWithWarnings("x", [pdfAttachment({ fileName: "a.pdf" })], {
378+supportsInlineImages: false,
379+});
409380expect(parsed.offloadedRefs).toHaveLength(1);
410381expect(parsed.offloadedRefs[0]?.mimeType).toBe("application/pdf");
411382expect(saveMediaBufferMock).toHaveBeenCalledOnce();
@@ -447,37 +418,17 @@ describe("parseMessageWithAttachments validation errors", () => {
447418448419it("keeps image sniff fallback for generic image attachments", async () => {
449420const { parsed, logs } = await parseWithWarnings("see this", [
450-{
451-type: "file",
452-mimeType: "application/octet-stream",
453-fileName: "dot",
454-content: PNG_1x1,
455-},
421+pngAttachment({ type: "file", mimeType: "application/octet-stream", fileName: "dot" }),
456422]);
457-expect(parsed.images).toHaveLength(1);
458-expect(parsed.images[0]?.mimeType).toBe("image/png");
423+expectSingleInlinePng(parsed);
459424expect(parsed.offloadedRefs).toHaveLength(0);
460425expect(logs).toHaveLength(0);
461426});
462427463428it("offloads images for text-only models instead of dropping them", async () => {
464429const logs: string[] = [];
465430const infos: string[] = [];
466-const parsed = await parseMessageWithAttachments(
467-"see this",
468-[
469-{
470-type: "image",
471-mimeType: "image/png",
472-fileName: "dot.png",
473-content: PNG_1x1,
474-},
475-],
476-{
477-log: { info: (message) => infos.push(message), warn: (warning) => logs.push(warning) },
478-supportsImages: false,
479-},
480-);
431+const parsed = await parseTextOnlyAttachments("see this", [pngAttachment()], logs, infos);
481432482433try {
483434expect(parsed.images).toHaveLength(0);
@@ -503,10 +454,7 @@ describe("parseMessageWithAttachments validation errors", () => {
503454content: PNG_1x1,
504455}),
505456);
506-const parsed = await parseMessageWithAttachments("see these", attachments, {
507-log: { warn: (warning) => logs.push(warning) },
508-supportsImages: false,
509-});
457+const parsed = await parseTextOnlyAttachments("see these", attachments, logs);
510458511459try {
512460expect(parsed.images).toHaveLength(0);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。