import { describe, expect, it } from 'vitest'; import { getAttachmentInputAcceptForMember, getMemberAttachmentUnavailableReason, validateAttachmentFilesForMember, validateAttachmentPayloadsForMember, } from '../../../src/renderer/utils/attachmentRecipientCapabilities'; import type { AttachmentPayload, ResolvedTeamMember } from '../../../src/shared/types'; function member(overrides: Partial): ResolvedTeamMember { return { name: 'bob', status: 'idle', currentTaskId: null, taskCount: 0, lastActiveAt: null, messageCount: 0, ...overrides, }; } function file(name: string, type: string, bytes = 12): File { return new File([new Uint8Array(bytes)], name, { type }); } function payload(overrides: Partial): AttachmentPayload { return { id: 'att-1', filename: 'diagram.png', mimeType: 'image/png', size: 12, data: 'aW1n', ...overrides, }; } describe('attachmentRecipientCapabilities', () => { it('blocks OpenCode non-vision models before file selection or send', () => { const bob = member({ providerId: 'opencode', model: 'openrouter/z-ai/glm-5.1', }); expect(getMemberAttachmentUnavailableReason(bob)).toBe( 'This OpenCode model is not verified for image attachments. Choose a vision-capable model or remove the image.' ); expect(validateAttachmentFilesForMember({ member: bob, files: [file('diagram.png', 'image/png')] })).toBe( 'This OpenCode model is not verified for image attachments. Choose a vision-capable model or remove the image.' ); expect(validateAttachmentPayloadsForMember({ member: bob, attachments: [payload({})] })).toBe( 'This OpenCode model is not verified for image attachments. Choose a vision-capable model or remove the image.' ); }); it('allows image picker input for verified OpenCode vision models', () => { const bob = member({ providerId: 'opencode', model: 'openrouter/moonshotai/kimi-k2.6', }); expect(getMemberAttachmentUnavailableReason(bob)).toBeNull(); expect(getAttachmentInputAcceptForMember(bob)).toBe('image/png,image/jpeg,image/webp'); expect(validateAttachmentFilesForMember({ member: bob, files: [file('diagram.png', 'image/png')] })).toBeNull(); expect(validateAttachmentPayloadsForMember({ member: bob, attachments: [payload({})] })).toBeNull(); }); it('blocks non-image files for image-only providers', () => { const codexLead = member({ name: 'lead', agentType: 'team-lead', providerId: 'codex', model: 'gpt-5.5', }); expect(validateAttachmentFilesForMember({ member: codexLead, files: [file('notes.md', 'text/markdown')] })).toBe( 'This provider path currently supports image attachments only. Non-image files are blocked before provider delivery.' ); expect( validateAttachmentPayloadsForMember({ member: codexLead, attachments: [payload({ filename: 'notes.md', mimeType: 'text/plain' })], }) ).toBe( 'This provider path currently supports image attachments only. Non-image files are blocked before provider delivery.' ); }); it('allows text/PDF files for Anthropic lead recipients', () => { const anthropicLead = member({ name: 'lead', agentType: 'team-lead', providerId: 'anthropic', model: 'claude-opus-4-6', }); expect(validateAttachmentFilesForMember({ member: anthropicLead, files: [file('brief.pdf', 'application/pdf')] })).toBeNull(); expect( validateAttachmentPayloadsForMember({ member: anthropicLead, attachments: [payload({ filename: 'brief.pdf', mimeType: 'application/pdf' })], }) ).toBeNull(); }); });