/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import test from 'node:test'; import assert from 'node:assert/strict'; import type { ChatMessage, FileAttachment } from './types.js'; import { buildStreamMessagesForModel, filterAttachmentsForModel } from './message-capabilities.js'; function makeImageAttachment(name: string): FileAttachment { return { id: `img:${name}`, name, type: 'image/png', size: 100, imageBase64: 'data:image/png;base64,abc', isImage: true, }; } function makeFileAttachment(name: string): FileAttachment { return { id: `file:${name}`, name, type: 'text/csv', size: 20, textContent: 'a,b\n1,2', }; } test('filters attachments by model capabilities', () => { const attachments = [makeImageAttachment('img.png'), makeFileAttachment('data.csv')]; const filtered = filterAttachmentsForModel(attachments, false, true); assert.equal(filtered.accepted.length, 1); assert.equal(filtered.accepted[0].name, 'data.csv'); assert.equal(filtered.droppedImages, 1); assert.equal(filtered.droppedFiles, 0); }); test('buildStreamMessagesForModel drops unsupported image payload parts', () => { const messages: ChatMessage[] = [ { id: 'm1', role: 'user', content: 'Hello', createdAt: Date.now(), attachments: [makeImageAttachment('img1.png')], }, ]; const result = buildStreamMessagesForModel(messages, 'data:image/png;base64,viewport', false); assert.equal(result.droppedInlineImages, 1); assert.equal(result.droppedViewportScreenshot, true); assert.equal(typeof result.messages[0].content, 'string'); assert.equal(result.messages[0].content, 'Hello'); }); test('buildStreamMessagesForModel keeps image payload parts when supported', () => { const messages: ChatMessage[] = [ { id: 'm1', role: 'user', content: 'Show this', createdAt: Date.now(), attachments: [makeImageAttachment('img1.png')], }, ]; const result = buildStreamMessagesForModel(messages, null, true); assert.equal(result.droppedInlineImages, 0); assert.equal(result.droppedViewportScreenshot, false); assert.ok(Array.isArray(result.messages[0].content)); const parts = result.messages[0].content as Array<{ type: string }>; assert.equal(parts[0].type, 'image_url'); }); test('buildStreamMessagesForModel keeps historical turns text-only', () => { const messages: ChatMessage[] = [ { id: 'm0', role: 'user', content: 'Earlier image', createdAt: Date.now() - 1000, attachments: [makeImageAttachment('old.png')], }, { id: 'm1', role: 'user', content: 'Latest image', createdAt: Date.now(), attachments: [makeImageAttachment('new.png')], }, ]; const result = buildStreamMessagesForModel(messages, null, true); assert.equal(typeof result.messages[0].content, 'string'); assert.equal(result.messages[0].content, 'Earlier image'); assert.ok(Array.isArray(result.messages[1].content)); const latestParts = result.messages[1].content as Array<{ type: string }>; assert.equal(latestParts[0].type, 'image_url'); }); test('buildStreamMessagesForModel attaches viewport screenshot only to latest turn', () => { const messages: ChatMessage[] = [ { id: 'm0', role: 'user', content: 'Earlier question', createdAt: Date.now() - 1000, }, { id: 'm1', role: 'user', content: 'Current question', createdAt: Date.now(), }, ]; const result = buildStreamMessagesForModel(messages, 'data:image/jpeg;base64,current', true); // Earlier turn remains plain text. assert.equal(typeof result.messages[0].content, 'string'); assert.equal(result.messages[0].content, 'Earlier question'); // Latest turn gets exactly one screenshot image part. assert.ok(Array.isArray(result.messages[1].content)); const parts = result.messages[1].content as Array<{ type: string }>; const imageParts = parts.filter((p) => p.type === 'image_url'); assert.equal(imageParts.length, 1); });