import { describe, expect, test } from 'vitest'; import { buildAttachmentUploadMetadata, buildAttachmentUploadFormData, createUploadedAttachmentFromServer, getTextAttachmentType, isOfficeXml, normalizeAttachmentMimeType, toAttachmentUploadDescriptor } from './attachment-upload.js'; describe('toAttachmentUploadDescriptor', () => { test('preserves explicit names for non-File uploads', () => { const descriptor = toAttachmentUploadDescriptor({ data: 'data:image/webp;base64,Zm9v', mimeType: 'image/webp', name: 'capture.webp' }); expect(descriptor).toMatchObject({ fileName: 'capture.webp', mimeType: 'image/webp' }); }); test('falls back to mime-derived filenames for unnamed non-File uploads', () => { const descriptor = toAttachmentUploadDescriptor({ data: 'data:application/pdf;base64,Zm9v', mimeType: 'application/pdf' }); expect(descriptor).toMatchObject({ fileName: 'attachment.pdf', mimeType: 'application/pdf' }); }); test('normalizes browser ts mime types from filename when they are not text-like', () => { const descriptor = toAttachmentUploadDescriptor(new File(['const answer = 42;'], 'main.ts', { type: 'video/mp2t' })); expect(descriptor).toMatchObject({ fileName: 'main.ts', mimeType: 'text/plain' }); expect(descriptor?.blob.type).toBe('text/plain'); }); test('normalizes blank browser mime types for python files', () => { const descriptor = toAttachmentUploadDescriptor(new File(['print("hi")'], 'script.py', { type: '' })); expect(descriptor).toMatchObject({ fileName: 'script.py', mimeType: 'text/plain' }); expect(descriptor?.blob.type).toBe('text/plain'); }); }); describe('buildAttachmentUploadFormData', () => { test('appends the normalized filename to form data', () => { const descriptor = toAttachmentUploadDescriptor({ data: 'data:image/webp;base64,Zm9v', mimeType: 'image/webp', name: 'capture.webp' }); expect(descriptor).not.toBeNull(); const formData = buildAttachmentUploadFormData({ descriptor: descriptor!, prefix: 'captured-screenshot' }); const uploadedFile = formData.get('file'); expect(uploadedFile).toBeInstanceOf(File); expect((uploadedFile as File).name).toBe('capture.webp'); expect(formData.get('prefix')).toBe('captured-screenshot'); }); test('appends serialized metadata when provided', () => { const descriptor = toAttachmentUploadDescriptor({ data: 'data:text/plain;base64,Zm9v', mimeType: 'text/plain', name: 'notes.txt' }); expect(descriptor).not.toBeNull(); const formData = buildAttachmentUploadFormData({ descriptor: descriptor!, prefix: 'message-attachment', metadata: buildAttachmentUploadMetadata({ label: 'Error screenshot', insight: 'Shows a missing submit button' }) }); expect(formData.get('metadata')).toBe( JSON.stringify({ label: 'Error screenshot', insight: 'Shows a missing submit button' }) ); }); }); describe('createUploadedAttachmentFromServer', () => { test('falls back to the original mime type when the server omits one', () => { const uploadedAttachment = createUploadedAttachmentFromServer({ response: { attachmentId: '123', fileName: '', contentUrl: '/api/v1/files/content/123', mimeType: null, purpose: 'attachment', scopeType: 'app', signedUrl: 'https://signed.example/123', signedUrlExpiresAt: '2026-03-06T00:00:00.000Z', storageKey: 'message-attachment/123' }, fallbackFileName: 'spec.pdf', fallbackMimeType: 'application/pdf', label: 'Spec', insight: 'Reference doc' }); expect(uploadedAttachment).toEqual({ type: 'uploaded', url: '/api/v1/files/content/123', signedUrl: 'https://signed.example/123', mediaType: 'application/pdf', fileName: 'spec.pdf', label: 'Spec', insight: 'Reference doc', storageKey: 'message-attachment/123', signedUrlExpiresAt: '2026-03-06T00:00:00.000Z', scopeType: 'app', applicationId: undefined }); }); }); describe('buildAttachmentUploadMetadata', () => { test('normalizes non-empty label and insight fields', () => { expect( buildAttachmentUploadMetadata({ label: 'Error screenshot', insight: 'Shows a missing submit button' }) ).toEqual({ label: 'Error screenshot', insight: 'Shows a missing submit button' }); }); }); describe('normalizeAttachmentMimeType', () => { test('prefers text-safe mime types for text-like filenames', () => { expect( normalizeAttachmentMimeType({ fileName: 'component.tsx', mimeType: 'video/mp2t' }) ).toBe('text/plain'); }); test('normalizes browser-reported application/x-sh for shell scripts', () => { expect( normalizeAttachmentMimeType({ fileName: 'deploy.sh', mimeType: 'application/x-sh' }) ).toBe('text/plain'); }); test('normalizes application/x-sh with no filename', () => { expect(normalizeAttachmentMimeType({ mimeType: 'application/x-sh' })).toBe('text/plain'); }); test('coerces non-shell text-like mime types with no filename', () => { expect(normalizeAttachmentMimeType({ mimeType: 'application/javascript' })).toBe('text/plain'); }); test('does not coerce application/x-shockwave-flash', () => { expect(normalizeAttachmentMimeType({ fileName: 'flash.swf', mimeType: 'application/x-shockwave-flash' })).toBe( 'application/x-shockwave-flash' ); }); test('does not coerce image/svg+xml to text/plain', () => { expect(normalizeAttachmentMimeType({ fileName: 'logo.svg', mimeType: 'image/svg+xml' })).toBe('image/svg+xml'); }); test('does not coerce image/svg+xml even without a filename', () => { expect(normalizeAttachmentMimeType({ mimeType: 'image/svg+xml' })).toBe('image/svg+xml'); }); test('does not coerce Office OpenXML documents to text/plain', () => { const docxMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; expect(normalizeAttachmentMimeType({ fileName: 'report.docx', mimeType: docxMimeType })).toBe(docxMimeType); expect(normalizeAttachmentMimeType({ mimeType: docxMimeType })).toBe(docxMimeType); }); test('does not coerce Office OpenXML spreadsheets to text/plain', () => { const xlsxMimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; expect(normalizeAttachmentMimeType({ mimeType: xlsxMimeType })).toBe(xlsxMimeType); }); }); describe('getTextAttachmentType', () => { test('returns null for office/openxml files even when mime is text-like', () => { expect( getTextAttachmentType({ fileName: 'proposal.docx', mimeType: 'text/plain' }) ).toBeNull(); }); test('still classifies markdown as text', () => { expect( getTextAttachmentType({ fileName: 'readme.md', mimeType: 'text/markdown' }) ).toBe('txt'); }); }); describe('isOfficeXml', () => { test('returns true for office file extensions even with text mime type', () => { expect( isOfficeXml({ fileName: 'requirements.docx', mimeType: 'text/plain' }) ).toBe(true); }); test('returns true for office mime types without filename', () => { expect( isOfficeXml({ mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }) ).toBe(true); }); test('returns false for markdown files', () => { expect( isOfficeXml({ fileName: 'readme.md', mimeType: 'text/markdown' }) ).toBe(false); }); });