agent-ecosystem/test/renderer/utils/attachmentRecipientCapabilities.test.ts

106 lines
3.7 KiB
TypeScript

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>): 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>): 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();
});
});