import { EnvironmentId, PROVIDER_SEND_TURN_MAX_FILE_BYTES } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import type { ComposerFileAttachment, ComposerImageAttachment } from "../../composerDraftStore"; import { isVideoAttachment, videoMimeType } from "../../types"; import { attachmentsToReleaseOnUploadCapabilityLoss, classifyComposerAttachmentFile, fileAttachmentCapabilityBlockReason, fileAttachmentStagingLimit, inferImageMimeTypeFromName, isPreviewableComposerVideo, normalizeComposerImageFileMimeType, shouldHandleComposerAttachmentPaste, } from "./composerAttachmentFiles"; describe("composer attachment files", () => { it("keeps supported images and HEIC photos on the image path", () => { expect(classifyComposerAttachmentFile({ name: "photo.png", type: "image/png" })).toBe("image"); expect(classifyComposerAttachmentFile({ name: "photo.heic", type: "" })).toBe("image"); }); it("rejects unsupported image types instead of attaching them as generic files", () => { expect(classifyComposerAttachmentFile({ name: "diagram.svg", type: "image/svg+xml" })).toBe( "unsupported-image", ); expect(classifyComposerAttachmentFile({ name: "photo.tiff", type: "image/tiff" })).toBe( "unsupported-image", ); expect(classifyComposerAttachmentFile({ name: "report.pdf", type: "application/pdf" })).toBe( "file", ); }); it("preserves text paste when an application adds a synthetic generic file", () => { const file = new File(["clipboard"], "clipboard.rtf", { type: "application/rtf" }); expect( shouldHandleComposerAttachmentPaste({ files: [file], plainText: "Copied text", }), ).toBe(false); }); it("claims unsupported image pastes so the composer can report them", () => { const images = [ new File(["svg"], "diagram.svg", { type: "image/svg+xml" }), new File(["tiff"], "photo.tiff", { type: "image/tiff" }), ]; for (const image of images) { expect( shouldHandleComposerAttachmentPaste({ files: [image], plainText: "Image caption", }), ).toBe(true); } }); it("claims generic file-only pastes so the composer can report validation errors", () => { const file = new File(["report"], "report.pdf", { type: "application/pdf" }); expect(shouldHandleComposerAttachmentPaste({ files: [file], plainText: "" })).toBe(true); }); it("routes empty and oversized generic files to composer feedback", () => { const empty = new File([], "empty.txt", { type: "text/plain" }); const oversized = new File([new Uint8Array(1024)], "large.zip", { type: "application/zip", }); expect(shouldHandleComposerAttachmentPaste({ files: [empty], plainText: "" })).toBe(true); expect(shouldHandleComposerAttachmentPaste({ files: [oversized], plainText: "" })).toBe(true); }); it("ignores an empty clipboard", () => { expect(shouldHandleComposerAttachmentPaste({ files: [], plainText: "" })).toBe(false); }); it("falls back to the extension when an image arrives without a MIME type", () => { expect(classifyComposerAttachmentFile({ name: "photo.jpg", type: "" })).toBe("image"); expect(classifyComposerAttachmentFile({ name: "shot.PNG", type: "" })).toBe("image"); expect(classifyComposerAttachmentFile({ name: "archive.zip", type: "" })).toBe("file"); expect(classifyComposerAttachmentFile({ name: "no-extension", type: "" })).toBe("file"); expect(inferImageMimeTypeFromName("photo.jpg")).toBe("image/jpeg"); expect(inferImageMimeTypeFromName("archive.zip")).toBeNull(); }); it("infers supported image types from octet-stream files", () => { const jpeg = new File(["jpeg"], "photo.jpg", { type: "application/octet-stream" }); const png = new File(["png"], "shot.PNG", { type: "application/octet-stream" }); expect(classifyComposerAttachmentFile(jpeg)).toBe("image"); expect(classifyComposerAttachmentFile(png)).toBe("image"); expect(normalizeComposerImageFileMimeType(jpeg).type).toBe("image/jpeg"); expect(normalizeComposerImageFileMimeType(png).type).toBe("image/png"); }); it("does not infer images for unknown extensions or specific conflicting MIME types", () => { const binary = new File(["binary"], "archive.bin", { type: "application/octet-stream" }); const unknownDocument = new File(["pdf"], "report.pdf", { type: "application/octet-stream", }); const document = new File(["pdf"], "photo.jpg", { type: "application/pdf" }); const explicitImage = new File(["png"], "photo.jpg", { type: "image/png" }); expect(classifyComposerAttachmentFile(binary)).toBe("file"); expect(classifyComposerAttachmentFile(unknownDocument)).toBe("file"); expect(classifyComposerAttachmentFile(document)).toBe("file"); expect(classifyComposerAttachmentFile(explicitImage)).toBe("image"); expect(normalizeComposerImageFileMimeType(binary)).toBe(binary); expect(normalizeComposerImageFileMimeType(document)).toBe(document); expect(normalizeComposerImageFileMimeType(explicitImage)).toBe(explicitImage); }); it("uses the hard local limit while server config is unknown", () => { expect( fileAttachmentStagingLimit({ attachmentUploadsCapabilityKnown: false, supportsAttachmentUploads: false, maxFileAttachmentBytes: null, }), ).toBe(PROVIDER_SEND_TURN_MAX_FILE_BYTES); expect( fileAttachmentCapabilityBlockReason({ files: [{ name: "pending.zip", sizeBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES }], attachmentUploadsCapabilityKnown: false, supportsAttachmentUploads: false, maxFileAttachmentBytes: null, }), ).toBe("Waiting for the server before file attachments can send"); }); it("rejects local staging and send when known config has no file support", () => { const unsupportedReason = "This server does not accept file attachments right now. Remove the files to send."; expect( fileAttachmentStagingLimit({ attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: true, maxFileAttachmentBytes: null, }), ).toBeNull(); expect( fileAttachmentStagingLimit({ attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: false, maxFileAttachmentBytes: 50 * 1024 * 1024, }), ).toBeNull(); expect( fileAttachmentCapabilityBlockReason({ files: [{ name: "report.pdf", sizeBytes: 1024 }], attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: true, maxFileAttachmentBytes: null, }), ).toBe(unsupportedReason); expect( fileAttachmentCapabilityBlockReason({ files: [{ name: "report.pdf", sizeBytes: 1024 }], attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: false, maxFileAttachmentBytes: 50 * 1024 * 1024, }), ).toBe(unsupportedReason); }); it("blocks retained files that exceed a newly lower server limit", () => { expect( fileAttachmentCapabilityBlockReason({ files: [{ name: "large.zip", sizeBytes: 2 * 1024 * 1024 }], attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: true, maxFileAttachmentBytes: 1024 * 1024, }), ).toBe("'large.zip' exceeds the 1 MB attachment limit."); }); it("uses the confirmed server limit without exceeding the hard cap", () => { expect( fileAttachmentStagingLimit({ attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: true, maxFileAttachmentBytes: 1024 * 1024, }), ).toBe(1024 * 1024); expect( fileAttachmentStagingLimit({ attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: true, maxFileAttachmentBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES * 2, }), ).toBe(PROVIDER_SEND_TURN_MAX_FILE_BYTES); expect( fileAttachmentCapabilityBlockReason({ files: [{ name: "report.pdf", sizeBytes: 1024 }], attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: true, maxFileAttachmentBytes: 50 * 1024 * 1024, }), ).toBeNull(); }); it("does not block empty or image-only composers on legacy servers", () => { expect( fileAttachmentCapabilityBlockReason({ files: [], attachmentUploadsCapabilityKnown: true, supportsAttachmentUploads: false, maxFileAttachmentBytes: null, }), ).toBeNull(); expect( fileAttachmentCapabilityBlockReason({ files: [], attachmentUploadsCapabilityKnown: false, supportsAttachmentUploads: false, maxFileAttachmentBytes: null, }), ).toBeNull(); }); it("keeps draft-persisted file uploads when the upload capability flips off", () => { const environmentId = EnvironmentId.make("environment-1"); const image: ComposerImageAttachment = { type: "image", id: "image-1", name: "photo.png", mimeType: "image/png", sizeBytes: 3, previewUrl: "blob:photo", file: new File([new Uint8Array([1, 2, 3])], "photo.png", { type: "image/png" }), }; const uploadingFile: ComposerFileAttachment = { type: "file", id: "file-uploading", name: "fresh.pdf", mimeType: "application/pdf", sizeBytes: 3, file: new File([new Uint8Array([1, 2, 3])], "fresh.pdf", { type: "application/pdf" }), }; const hydratedFile: ComposerFileAttachment = { type: "file", id: "file-hydrated", name: "report.pdf", mimeType: "application/pdf", sizeBytes: 3, file: null, uploadedAttachmentId: "pending-report-pdf", uploadEnvironmentId: environmentId, }; const uploadedLocalFile: ComposerFileAttachment = { ...uploadingFile, id: "file-uploaded-local", uploadedAttachmentId: "pending-fresh-pdf", uploadEnvironmentId: environmentId, }; const released = attachmentsToReleaseOnUploadCapabilityLoss([ image, uploadingFile, hydratedFile, uploadedLocalFile, ]); expect(released.map((attachment) => attachment.id)).toEqual(["image-1", "file-uploading"]); }); it("keeps restored videos on the preview path in their upload environment", () => { const environmentId = EnvironmentId.make("environment-1"); const video: ComposerFileAttachment = { type: "file", id: "video-1", name: "clip.mp4", mimeType: "video/mp4", sizeBytes: 3, file: null, uploadedAttachmentId: "uploaded-video-1", uploadEnvironmentId: environmentId, }; expect(isPreviewableComposerVideo(video, environmentId)).toBe(true); expect(isPreviewableComposerVideo(video, EnvironmentId.make("environment-2"))).toBe(false); }); it("recognizes common video extensions when the browser omits the MIME type", () => { const formats = [ ["clip.mp4", "video/mp4"], ["clip.mov", "video/quicktime"], ["clip.webm", "video/webm"], ["clip.m4v", "video/mp4"], ["clip.mkv", "video/x-matroska"], ["clip.avi", "video/x-msvideo"], ["clip.ogv", "video/ogg"], ] as const; for (const [name, expectedMimeType] of formats) { expect( isVideoAttachment({ type: "file", id: name, name, mimeType: "application/octet-stream", sizeBytes: 1, }), ).toBe(true); expect(videoMimeType({ name, mimeType: "application/octet-stream" })).toBe(expectedMimeType); } }); it("claims image pastes even when clipboard text is present", () => { const image = new File(["image"], "photo.heic", { type: "image/heic" }); expect( shouldHandleComposerAttachmentPaste({ files: [image], plainText: "Image caption", }), ).toBe(true); }); });