Path: blob/main/extensions/copilot/src/platform/endpoint/node/test/imageLimits.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Raw } from '@vscode/prompt-tsx';6import { describe, expect, it } from 'vitest';7import { filterHistoryImages } from '../imageLimits';89const createUserImageMessage = (imageCount: number = 1): Raw.ChatMessage => ({10role: Raw.ChatRole.User,11content: [12{ type: Raw.ChatCompletionContentPartKind.Text, text: 'What is in this image?' },13...Array.from({ length: imageCount }, () => ({14type: Raw.ChatCompletionContentPartKind.Image as const,15imageUrl: { url: 'data:image/png;base64,test' }16}))17]18});1920const createAssistantMessage = (): Raw.ChatMessage => ({21role: Raw.ChatRole.Assistant,22content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'I see an image.' }]23});2425const createToolImageMessage = (): Raw.ChatMessage => ({26role: Raw.ChatRole.Tool,27toolCallId: 'tool-1',28content: [29{ type: Raw.ChatCompletionContentPartKind.Image, imageUrl: { url: 'https://example.com/tool.png' } }30]31});3233const countImages = (messages: Raw.ChatMessage[]): number => {34let count = 0;35for (const msg of messages) {36if (Array.isArray(msg.content)) {37for (const part of msg.content) {38if (part.type === Raw.ChatCompletionContentPartKind.Image) {39count++;40}41}42}43}44return count;45};4647describe('filterHistoryImages', () => {48it('returns the original array by reference when within the limit', () => {49const messages = [createUserImageMessage(), createUserImageMessage()];50expect(filterHistoryImages(messages, 5)).toBe(messages);51});5253it('silently filters oldest history images when total exceeds the limit', () => {54// 2 history user messages with 1 image each + current user message with 2 images = 4 total > 3 limit55const messages = [56createUserImageMessage(),57createAssistantMessage(),58createUserImageMessage(),59createAssistantMessage(),60createUserImageMessage(2),61];62const filtered = filterHistoryImages(messages, 3);63expect(countImages(filtered)).toBeLessThanOrEqual(3);64// Current user message (last) must retain all 2 of its images.65expect(countImages([filtered[filtered.length - 1]])).toBe(2);66// Original messages must not be mutated.67expect(countImages(messages)).toBe(4);68});6970it('replaces dropped images with a text placeholder', () => {71const messages = [72createUserImageMessage(),73createAssistantMessage(),74createUserImageMessage(1),75];76const filtered = filterHistoryImages(messages, 1);77const droppedMessage = filtered[0];78if (!Array.isArray(droppedMessage.content)) {79throw new Error('expected array content');80}81const placeholder = droppedMessage.content.find(p => p.type === Raw.ChatCompletionContentPartKind.Text && p.text.includes('Image omitted'));82expect(placeholder).toBeDefined();83});8485it('filters tool-result images in history the same as user images', () => {86// 2 tool-result images in history + 1 current user image = 3 total > 2 limit87const messages: Raw.ChatMessage[] = [88createToolImageMessage(),89createAssistantMessage(),90createToolImageMessage(),91createAssistantMessage(),92createUserImageMessage(1),93];94const filtered = filterHistoryImages(messages, 2);95expect(countImages(filtered)).toBeLessThanOrEqual(2);96// Original messages must not be mutated.97expect(countImages(messages)).toBe(3);98});99100it('throws a clear error including the per-model limit when the current turn alone exceeds it', () => {101// Current user message has 11 images. The error must mention the exact102// model-scoped limit (10 for Gemini, 20 for Anthropic Messages API).103const messages = [createUserImageMessage(11)];104expect(() => filterHistoryImages(messages, 10)).toThrow(/11 images provided.*maximum of 10 images/);105106const many = [createUserImageMessage(25)];107expect(() => filterHistoryImages(many, 20)).toThrow(/25 images provided.*maximum of 20 images/);108});109110it('handles conversations with no user message by treating the last message as current', () => {111const messages: Raw.ChatMessage[] = [112createToolImageMessage(),113createToolImageMessage(),114createToolImageMessage(),115];116const filtered = filterHistoryImages(messages, 1);117// Last message preserved; earlier tool-result images filtered.118expect(countImages(filtered)).toBeLessThanOrEqual(1);119expect(countImages([filtered[filtered.length - 1]])).toBe(1);120});121122it('does not mutate the original messages array or its contents', () => {123// History has 2 images, current turn has 1, limit is 1 → history filters silently.124const messages = [125createUserImageMessage(),126createAssistantMessage(),127createUserImageMessage(),128createAssistantMessage(),129createUserImageMessage(1),130];131const snapshot = JSON.stringify(messages);132filterHistoryImages(messages, 1);133expect(JSON.stringify(messages)).toBe(snapshot);134});135136it('passes through messages with non-array content', () => {137const messages: Raw.ChatMessage[] = [138{ role: Raw.ChatRole.System, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'system' }] },139createUserImageMessage(2),140];141// Total = 2 images, within limit of 2 → returned unchanged.142const filtered = filterHistoryImages(messages, 2);143expect(filtered).toBe(messages);144});145});146147148