import { describe, expect, test } from "bun:test"; import type { ComposerAttachment } from "../src/app/types"; import { buildChatAttachmentInboxPath, composerAttachmentsToWorkspaceFileParts, composerAttachmentToFilePart, modelFacingAttachmentMime, resolveAttachmentFileMetadata, safeAttachmentFilename, workspaceInboxPath, type ChatAttachmentWorkspaceEndpoint, } from "../src/react-app/domains/session/sync/attachment-file-part"; async function requireFilePart(attachment: ComposerAttachment) { const part = await composerAttachmentToFilePart(attachment); if (!part) throw new Error("Expected a model-facing file part"); return part; } const JPEG_BYTES = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]); const DOCX_BYTES = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x77, 0x6f, 0x72, 0x64]); const PPTX_BYTES = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x70, 0x70, 0x74, 0x78]); const XLSX_BYTES = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x78, 0x6c, 0x73, 0x78]); const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; const PPTX_MIME = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; const XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; const PDF_BYTES = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x34, 0x0a, 0x25, 0xe2, 0xe3, 0xcf, 0xd3]); type UploadCall = { workspaceId: string; path: string; filename: string; bytes: number[]; }; function attachmentFor(file: File, metadata: Partial> = {}): ComposerAttachment { return { id: "attachment-1", name: metadata.name ?? file.name, mimeType: metadata.mimeType ?? file.type, size: file.size, kind: metadata.kind ?? (file.type.startsWith("image/") ? "image" : "file"), file, }; } function decodedDataUrlBytes(url: string) { const marker = ";base64,"; const markerIndex = url.indexOf(marker); expect(markerIndex).toBeGreaterThan(0); const binary = atob(url.slice(markerIndex + marker.length)); return Uint8Array.from(binary, (char) => char.charCodeAt(0)); } function uploadRecorder(workspaceId: string) { const calls: UploadCall[] = []; const endpoint: ChatAttachmentWorkspaceEndpoint = { workspaceId, client: { uploadInbox: async (id, file, options) => { const bytes = new Uint8Array(await file.arrayBuffer()); const path = options?.path?.trim() || file.name; calls.push({ workspaceId: id, path, filename: file.name, bytes: Array.from(bytes), }); return { ok: true, path, bytes: file.size }; }, }, }; return { endpoint, calls }; } function textPart(parts: Awaited>) { const part = parts[0]; if (!part || part.type !== "text") throw new Error("Expected first attachment part to be a text note"); return part; } function textPartText(parts: Awaited>) { return textPart(parts).text; } function filePartUrl(parts: Awaited>, index: number) { const part = parts[index]; if (!part || part.type !== "file") throw new Error(`Expected attachment part ${index} to be a file`); return part.url; } describe("composer attachment file parts", () => { test("preserves JPEG filename, mime, data URL, and exact bytes", async () => { const file = new File([JPEG_BYTES], "PassaportoPaolo_small.jpg", { type: "image/jpeg" }); const part = await requireFilePart(attachmentFor(file)); expect(part.filename).toBe("PassaportoPaolo_small.jpg"); expect(part.mime).toBe("image/jpeg"); expect(part.url.startsWith("data:image/jpeg;base64,")).toBe(true); expect(Array.from(decodedDataUrlBytes(part.url))).toEqual(Array.from(JPEG_BYTES)); }); test("stale ComposerAttachment PDF metadata cannot override an underlying JPEG File", async () => { const file = new File([JPEG_BYTES], "PassaportoPaolo_small.jpg", { type: "image/jpeg" }); const part = await requireFilePart(attachmentFor(file, { name: "PassaportoPaolo_small.pdf", mimeType: "application/pdf", kind: "file", })); expect(part.filename).toBe("PassaportoPaolo_small.jpg"); expect(part.mime).toBe("image/jpeg"); expect(part.url.startsWith("data:image/jpeg;base64,")).toBe(true); }); test("generic MIME resolves from supported .pdf and .png extensions", () => { expect(resolveAttachmentFileMetadata(new File([JPEG_BYTES], "scan.pdf", { type: "application/octet-stream" }))).toMatchObject({ filename: "scan.pdf", mime: "application/pdf", kind: "file", }); expect(resolveAttachmentFileMetadata(new File([JPEG_BYTES], "scan.png", { type: "" }))).toMatchObject({ filename: "scan.png", mime: "image/png", kind: "image", }); }); test("preserves canonical Office MIME, data URL headers, filenames, and exact bytes", async () => { const docx = new File([DOCX_BYTES], "PlanningMemo.docx", { type: DOCX_MIME }); const pptx = new File([PPTX_BYTES], "RoadshowDeck.pptx", { type: PPTX_MIME }); const xlsx = new File([XLSX_BYTES], "RevenueWorkbook.xlsx", { type: XLSX_MIME }); expect(resolveAttachmentFileMetadata(docx)).toMatchObject({ filename: "PlanningMemo.docx", mime: DOCX_MIME, kind: "file", }); expect(resolveAttachmentFileMetadata(pptx)).toMatchObject({ filename: "RoadshowDeck.pptx", mime: PPTX_MIME, kind: "file", }); expect(resolveAttachmentFileMetadata(xlsx)).toMatchObject({ filename: "RevenueWorkbook.xlsx", mime: XLSX_MIME, kind: "file", }); const docxPart = await requireFilePart(attachmentFor(docx)); const pptxPart = await requireFilePart(attachmentFor(pptx)); const xlsxPart = await requireFilePart(attachmentFor(xlsx)); expect(docxPart.filename).toBe("PlanningMemo.docx"); expect(docxPart.mime).toBe(DOCX_MIME); expect(docxPart.url.startsWith(`data:${DOCX_MIME};base64,`)).toBe(true); expect(Array.from(decodedDataUrlBytes(docxPart.url))).toEqual(Array.from(DOCX_BYTES)); expect(pptxPart.filename).toBe("RoadshowDeck.pptx"); expect(pptxPart.mime).toBe(PPTX_MIME); expect(pptxPart.url.startsWith(`data:${PPTX_MIME};base64,`)).toBe(true); expect(Array.from(decodedDataUrlBytes(pptxPart.url))).toEqual(Array.from(PPTX_BYTES)); expect(xlsxPart.filename).toBe("RevenueWorkbook.xlsx"); expect(xlsxPart.mime).toBe(XLSX_MIME); expect(xlsxPart.url.startsWith(`data:${XLSX_MIME};base64,`)).toBe(true); expect(Array.from(decodedDataUrlBytes(xlsxPart.url))).toEqual(Array.from(XLSX_BYTES)); }); test("resolves generic Office MIME from case-insensitive extensions without coercing bytes to text", async () => { const docx = new File([DOCX_BYTES], "QuarterlyReport.DOCX", { type: "application/octet-stream" }); const pptx = new File([PPTX_BYTES], "LaunchPlan.PPTX", { type: "" }); const xlsx = new File([XLSX_BYTES], "BudgetModel.XLSX", { type: "application/octet-stream" }); expect(resolveAttachmentFileMetadata(docx)).toMatchObject({ filename: "QuarterlyReport.DOCX", mime: DOCX_MIME, kind: "file", }); expect(resolveAttachmentFileMetadata(pptx)).toMatchObject({ filename: "LaunchPlan.PPTX", mime: PPTX_MIME, kind: "file", }); expect(resolveAttachmentFileMetadata(xlsx)).toMatchObject({ filename: "BudgetModel.XLSX", mime: XLSX_MIME, kind: "file", }); const docxPart = await requireFilePart(attachmentFor(docx)); const pptxPart = await requireFilePart(attachmentFor(pptx)); const xlsxPart = await requireFilePart(attachmentFor(xlsx)); expect(docxPart.filename).toBe("QuarterlyReport.DOCX"); expect(docxPart.mime).toBe(DOCX_MIME); expect(docxPart.url.startsWith(`data:${DOCX_MIME};base64,`)).toBe(true); expect(Array.from(decodedDataUrlBytes(docxPart.url))).toEqual(Array.from(DOCX_BYTES)); expect(pptxPart.filename).toBe("LaunchPlan.PPTX"); expect(pptxPart.mime).toBe(PPTX_MIME); expect(pptxPart.url.startsWith(`data:${PPTX_MIME};base64,`)).toBe(true); expect(Array.from(decodedDataUrlBytes(pptxPart.url))).toEqual(Array.from(PPTX_BYTES)); expect(xlsxPart.filename).toBe("BudgetModel.XLSX"); expect(xlsxPart.mime).toBe(XLSX_MIME); expect(xlsxPart.url.startsWith(`data:${XLSX_MIME};base64,`)).toBe(true); expect(Array.from(decodedDataUrlBytes(xlsxPart.url))).toEqual(Array.from(XLSX_BYTES)); }); test("normalizes Office filename extensions from canonical MIME when filenames disagree", async () => { const docxNamedBin = new File([DOCX_BYTES], "PlanningMemo.bin", { type: DOCX_MIME }); const pptxWithoutExtension = new File([PPTX_BYTES], "RoadshowDeck", { type: PPTX_MIME }); const xlsxWithoutExtension = new File([XLSX_BYTES], "RevenueWorkbook", { type: XLSX_MIME }); expect((await requireFilePart(attachmentFor(docxNamedBin))).filename).toBe("PlanningMemo.docx"); expect((await requireFilePart(attachmentFor(pptxWithoutExtension))).filename).toBe("RoadshowDeck.pptx"); expect((await requireFilePart(attachmentFor(xlsxWithoutExtension))).filename).toBe("RevenueWorkbook.xlsx"); }); test("routes model-facing mimes: providers only accept image/*, PDF, and text/plain file parts", () => { // Text-like content is re-mimed to text/plain so opencode inlines it (the @file mechanism). expect(modelFacingAttachmentMime("text/xml")).toBe("text/plain"); expect(modelFacingAttachmentMime("text/csv")).toBe("text/plain"); expect(modelFacingAttachmentMime("application/json")).toBe("text/plain"); expect(modelFacingAttachmentMime("application/xml")).toBe("text/plain"); expect(modelFacingAttachmentMime("application/javascript")).toBe("text/plain"); expect(modelFacingAttachmentMime("application/ld+json")).toBe("text/plain"); expect(modelFacingAttachmentMime("image/svg+xml")).toBe("text/plain"); expect(modelFacingAttachmentMime("text/plain; charset=utf-8")).toBe("text/plain"); // Provider-native formats pass through (Office is rewritten server-side by the plugin). expect(modelFacingAttachmentMime("image/png")).toBe("image/png"); expect(modelFacingAttachmentMime("application/pdf")).toBe("application/pdf"); expect(modelFacingAttachmentMime(DOCX_MIME)).toBe(DOCX_MIME); // Binary/unknown formats get no model-facing file part; tools use the workspace path. expect(modelFacingAttachmentMime("application/octet-stream")).toBeNull(); expect(modelFacingAttachmentMime("application/zip")).toBeNull(); expect(modelFacingAttachmentMime("audio/mpeg")).toBeNull(); expect(modelFacingAttachmentMime("application/x-iwork-keynote-sffkey")).toBeNull(); }); test("accepts unsupported binary attachments without producing a model-facing file part", async () => { expect(resolveAttachmentFileMetadata(new File([PPTX_BYTES], "archive.zip", { type: "" }))).toMatchObject({ filename: "archive.zip", mime: "application/octet-stream", kind: "file", }); const binary = new File([PPTX_BYTES], "board.key", { type: "application/x-iwork-keynote-sffkey" }); expect(await composerAttachmentToFilePart(attachmentFor(binary))).toBeNull(); }); test("text-like attachments are sent as text/plain file parts with original filenames", async () => { const xml = new File(["Kickoff"], "agenda.xml", { type: "text/xml" }); const part = await requireFilePart(attachmentFor(xml)); expect(part.filename).toBe("agenda.xml"); expect(part.mime).toBe("text/plain"); expect(part.url.startsWith("data:text/plain;base64,")).toBe(true); const csv = new File(["a,b\n1,2\n"], "rows.csv", { type: "" }); expect((await requireFilePart(attachmentFor(csv))).mime).toBe("text/plain"); const json = new File(["{}"], "config.json", { type: "application/json" }); expect((await requireFilePart(attachmentFor(json))).mime).toBe("text/plain"); }); test("known MIME and filename extension conflicts normalize outbound filename extension", async () => { const imageNamedPdf = new File([JPEG_BYTES], "PassaportoPaolo_small.pdf", { type: "image/jpeg" }); const pdfNamedPng = new File([JPEG_BYTES], "scan.png", { type: "application/pdf" }); expect((await requireFilePart(attachmentFor(imageNamedPdf))).filename).toBe("PassaportoPaolo_small.jpg"); expect((await requireFilePart(attachmentFor(pdfNamedPng))).filename).toBe("scan.pdf"); }); test("sanitizes desktop-style filenames without leaking local path segments", () => { expect(safeAttachmentFilename("C:\\Users\\omar\\Scans\\scan one 李?.pdf")).toBe("scan one 李_.pdf"); expect(resolveAttachmentFileMetadata(new File([PDF_BYTES], "C:\\Users\\omar\\scan one 李.pdf", { type: "application/pdf" }))).toMatchObject({ filename: "scan one 李.pdf", mime: "application/pdf", }); }); test("generates session-scoped inbox paths under the workspace inbox", () => { const inboxPath = buildChatAttachmentInboxPath({ sessionId: "ses_123", id: "nonce-abc", filename: "scan one 李.pdf", }); expect(inboxPath).toBe("chat-attachments/ses_123/nonce-abc-scan one 李.pdf"); expect(workspaceInboxPath(inboxPath)).toBe(".opencode/openwork/inbox/chat-attachments/ses_123/nonce-abc-scan one 李.pdf"); }); test("bounds long ASCII attachment names while preserving extension and unique id", () => { const filename = `${"a".repeat(400)}.pdf`; const first = buildChatAttachmentInboxPath({ sessionId: "ses_long", id: "nonce-a", filename }); const repeated = buildChatAttachmentInboxPath({ sessionId: "ses_long", id: "nonce-a", filename }); const second = buildChatAttachmentInboxPath({ sessionId: "ses_long", id: "nonce-b", filename }); const basename = first.split("/").pop(); if (!basename) throw new Error("Expected attachment basename"); expect(new TextEncoder().encode(basename).byteLength).toBe(255); expect(basename.startsWith("nonce-a-")).toBe(true); expect(basename.endsWith(".pdf")).toBe(true); expect(first).toBe(repeated); expect(first).not.toBe(second); }); test("bounds long multibyte attachment names on UTF-8 character boundaries", () => { const filename = `${"李".repeat(200)}.pdf`; const path = buildChatAttachmentInboxPath({ sessionId: "ses_long", id: "nonce-a", filename }); const basename = path.split("/").pop(); if (!basename) throw new Error("Expected attachment basename"); expect(new TextEncoder().encode(basename).byteLength).toBe(255); expect(basename.startsWith("nonce-a-")).toBe(true); expect(basename.endsWith(".pdf")).toBe(true); expect(basename).not.toContain("�"); }); test("uploads exact bytes to the endpoint workspace id and exposes a worker file URL plus path note", async () => { const { endpoint, calls } = uploadRecorder("server-workspace-42"); const file = new File([PDF_BYTES], "image-only scan.pdf", { type: "application/pdf" }); const parts = await composerAttachmentsToWorkspaceFileParts({ attachments: [attachmentFor(file)], endpoint, sessionId: "ses_abc", workspaceRoot: "/workspaces/Worker Root", createId: () => "nonce-a", }); expect(calls).toEqual([{ workspaceId: "server-workspace-42", path: "chat-attachments/ses_abc/nonce-a-image-only scan.pdf", filename: "image-only scan.pdf", bytes: Array.from(PDF_BYTES), }]); expect(textPart(parts)).toMatchObject({ type: "text", synthetic: true, }); expect(textPartText(parts).startsWith("Attached files were copied")).toBe(true); expect(textPartText(parts)).toContain(".opencode/openwork/inbox/chat-attachments/ses_abc/nonce-a-image-only scan.pdf"); expect(textPartText(parts)).toContain("Read/Bash/MCP/Docling"); expect(filePartUrl(parts, 1)).toBe("file:///workspaces/Worker%20Root/.opencode/openwork/inbox/chat-attachments/ses_abc/nonce-a-image-only%20scan.pdf"); expect(parts[1]).toMatchObject({ type: "file", filename: "image-only scan.pdf", mime: "application/pdf", }); }); test("workspace image attachments use displayable data URLs while keeping a synthetic path note for tools", async () => { const { endpoint, calls } = uploadRecorder("server-workspace-42"); const file = new File([JPEG_BYTES], "shot.png", { type: "image/png" }); const parts = await composerAttachmentsToWorkspaceFileParts({ attachments: [attachmentFor(file)], endpoint, sessionId: "ses_img", workspaceRoot: "/workspaces/Worker Root", createId: () => "nonce-img", }); expect(calls).toEqual([{ workspaceId: "server-workspace-42", path: "chat-attachments/ses_img/nonce-img-shot.png", filename: "shot.png", bytes: Array.from(JPEG_BYTES), }]); expect(textPart(parts)).toMatchObject({ type: "text", synthetic: true }); expect(textPartText(parts)).toContain(".opencode/openwork/inbox/chat-attachments/ses_img/nonce-img-shot.png"); expect(textPartText(parts)).toContain("file:///workspaces/Worker%20Root/.opencode/openwork/inbox/chat-attachments/ses_img/nonce-img-shot.png"); expect(filePartUrl(parts, 1).startsWith("data:image/png;base64,")).toBe(true); expect(Array.from(decodedDataUrlBytes(filePartUrl(parts, 1)))).toEqual(Array.from(JPEG_BYTES)); expect(parts[1]).toMatchObject({ type: "file", filename: "shot.png", mime: "image/png", }); }); test("workspace text-like attachments become text/plain file parts pointing at the workspace copy", async () => { const { endpoint, calls } = uploadRecorder("server-workspace-42"); const file = new File([""], "sitemap.xml", { type: "text/xml" }); const parts = await composerAttachmentsToWorkspaceFileParts({ attachments: [attachmentFor(file)], endpoint, sessionId: "ses_xml", workspaceRoot: "/workspaces/Worker Root", createId: () => "nonce-xml", }); expect(calls).toHaveLength(1); expect(filePartUrl(parts, 1)).toBe("file:///workspaces/Worker%20Root/.opencode/openwork/inbox/chat-attachments/ses_xml/nonce-xml-sitemap.xml"); expect(parts[1]).toMatchObject({ type: "file", filename: "sitemap.xml", mime: "text/plain", }); }); test("workspace binary attachments upload for tool access and emit a Read-mediated text/plain file part", async () => { const { endpoint, calls } = uploadRecorder("server-workspace-42"); const file = new File([PPTX_BYTES], "recording.zip", { type: "application/zip" }); const parts = await composerAttachmentsToWorkspaceFileParts({ attachments: [attachmentFor(file)], endpoint, sessionId: "ses_bin", workspaceRoot: "/workspaces/Worker Root", createId: () => "nonce-bin", }); expect(calls).toEqual([{ workspaceId: "server-workspace-42", path: "chat-attachments/ses_bin/nonce-bin-recording.zip", filename: "recording.zip", bytes: Array.from(PPTX_BYTES), }]); expect(textPart(parts)).toMatchObject({ type: "text", synthetic: true }); expect(textPartText(parts)).toContain(".opencode/openwork/inbox/chat-attachments/ses_bin/nonce-bin-recording.zip"); expect(textPartText(parts)).toContain("Read/Bash/MCP/Docling"); // text/plain file parts never reach the provider (opencode expands them // through the Read tool), so binaries keep a transcript badge safely. expect(filePartUrl(parts, 1)).toBe("file:///workspaces/Worker%20Root/.opencode/openwork/inbox/chat-attachments/ses_bin/nonce-bin-recording.zip"); expect(parts[1]).toMatchObject({ type: "file", filename: "recording.zip", mime: "text/plain", }); }); test("uploads duplicate filenames to distinct non-overwriting paths", async () => { const { endpoint, calls } = uploadRecorder("workspace-a"); const first = new File([PDF_BYTES], "scan.pdf", { type: "application/pdf" }); const second = new File([PDF_BYTES], "scan.pdf", { type: "application/pdf" }); const ids = ["nonce-a", "nonce-b"]; const parts = await composerAttachmentsToWorkspaceFileParts({ attachments: [attachmentFor(first), attachmentFor(second)], endpoint, sessionId: "ses_dupes", workspaceRoot: "C:\\Users\\Ada Lovelace\\工作区", createId: () => { const id = ids.shift(); if (!id) throw new Error("missing nonce"); return id; }, }); expect(calls.map((call) => call.path)).toEqual([ "chat-attachments/ses_dupes/nonce-a-scan.pdf", "chat-attachments/ses_dupes/nonce-b-scan.pdf", ]); expect(new Set(calls.map((call) => call.path)).size).toBe(2); expect(filePartUrl(parts, 1)).toBe("file:///C:/Users/Ada%20Lovelace/%E5%B7%A5%E4%BD%9C%E5%8C%BA/.opencode/openwork/inbox/chat-attachments/ses_dupes/nonce-a-scan.pdf"); expect(filePartUrl(parts, 2)).toBe("file:///C:/Users/Ada%20Lovelace/%E5%B7%A5%E4%BD%9C%E5%8C%BA/.opencode/openwork/inbox/chat-attachments/ses_dupes/nonce-b-scan.pdf"); }); test("fails before producing prompt parts when workspace upload fails", async () => { const endpoint: ChatAttachmentWorkspaceEndpoint = { workspaceId: "workspace-a", client: { uploadInbox: async () => { throw new Error("disk full"); }, }, }; const file = new File([PDF_BYTES], "scan.pdf", { type: "application/pdf" }); await expect(composerAttachmentsToWorkspaceFileParts({ attachments: [attachmentFor(file)], endpoint, sessionId: "ses_fail", workspaceRoot: "/workspace/a", createId: () => "nonce-a", })).rejects.toThrow("Failed to copy attachment \"scan.pdf\" into this worker workspace: disk full"); }); test("treats an ok:false upload result as a hard failure", async () => { const endpoint: ChatAttachmentWorkspaceEndpoint = { workspaceId: "workspace-a", client: { uploadInbox: async (_workspaceId, file, options) => ({ ok: false, path: options?.path?.trim() || file.name, bytes: file.size, }), }, }; const file = new File([PDF_BYTES], "scan.pdf", { type: "application/pdf" }); await expect(composerAttachmentsToWorkspaceFileParts({ attachments: [attachmentFor(file)], endpoint, sessionId: "ses_rejected", workspaceRoot: "/workspace/a", createId: () => "nonce-a", })).rejects.toThrow("Failed to copy attachment \"scan.pdf\" into this worker workspace: upload was rejected"); }); });