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