@ank1015/providers 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +453 -0
- package/biome.json +43 -0
- package/dist/agent/agent-loop.d.ts +5 -0
- package/dist/agent/agent-loop.d.ts.map +1 -0
- package/dist/agent/agent-loop.js +219 -0
- package/dist/agent/agent-loop.js.map +1 -0
- package/dist/agent/types.d.ts +67 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +3 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +3 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.generated.d.ts +247 -0
- package/dist/models.generated.d.ts.map +1 -0
- package/dist/models.generated.js +315 -0
- package/dist/models.generated.js.map +1 -0
- package/dist/models.js +41 -0
- package/dist/models.js.map +1 -0
- package/dist/providers/convert.d.ts +6 -0
- package/dist/providers/convert.d.ts.map +1 -0
- package/dist/providers/convert.js +207 -0
- package/dist/providers/convert.js.map +1 -0
- package/dist/providers/google.d.ts +26 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +434 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/openai.d.ts +17 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +396 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/stream.d.ts +4 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +40 -0
- package/dist/stream.js.map +1 -0
- package/dist/test-google-agent-loop.d.ts +2 -0
- package/dist/test-google-agent-loop.d.ts.map +1 -0
- package/dist/test-google-agent-loop.js +186 -0
- package/dist/test-google-agent-loop.js.map +1 -0
- package/dist/test-google.d.ts +2 -0
- package/dist/test-google.d.ts.map +1 -0
- package/dist/test-google.js +41 -0
- package/dist/test-google.js.map +1 -0
- package/dist/types.d.ts +187 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/event-stream.d.ts +16 -0
- package/dist/utils/event-stream.d.ts.map +1 -0
- package/dist/utils/event-stream.js +61 -0
- package/dist/utils/event-stream.js.map +1 -0
- package/dist/utils/json-parse.d.ts +9 -0
- package/dist/utils/json-parse.d.ts.map +1 -0
- package/dist/utils/json-parse.js +32 -0
- package/dist/utils/json-parse.js.map +1 -0
- package/dist/utils/sanitize-unicode.d.ts +22 -0
- package/dist/utils/sanitize-unicode.d.ts.map +1 -0
- package/dist/utils/sanitize-unicode.js +29 -0
- package/dist/utils/sanitize-unicode.js.map +1 -0
- package/dist/utils/validation.d.ts +11 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +61 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +33 -0
- package/src/agent/agent-loop.ts +275 -0
- package/src/agent/types.ts +80 -0
- package/src/index.ts +72 -0
- package/src/models.generated.ts +314 -0
- package/src/models.ts +45 -0
- package/src/providers/convert.ts +222 -0
- package/src/providers/google.ts +496 -0
- package/src/providers/openai.ts +437 -0
- package/src/stream.ts +60 -0
- package/src/types.ts +198 -0
- package/src/utils/event-stream.ts +60 -0
- package/src/utils/json-parse.ts +28 -0
- package/src/utils/sanitize-unicode.ts +25 -0
- package/src/utils/validation.ts +69 -0
- package/test/core/agent-loop.test.ts +958 -0
- package/test/core/stream.test.ts +409 -0
- package/test/data/red-circle.png +0 -0
- package/test/data/superintelligentwill.pdf +0 -0
- package/test/edge-cases/general.test.ts +565 -0
- package/test/integration/e2e.test.ts +530 -0
- package/test/models/cost.test.ts +499 -0
- package/test/models/registry.test.ts +298 -0
- package/test/providers/convert.test.ts +846 -0
- package/test/providers/google-schema.test.ts +666 -0
- package/test/providers/google-stream.test.ts +369 -0
- package/test/providers/openai-stream.test.ts +251 -0
- package/test/utils/event-stream.test.ts +289 -0
- package/test/utils/json-parse.test.ts +344 -0
- package/test/utils/sanitize-unicode.test.ts +329 -0
- package/test/utils/validation.test.ts +614 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import { validateToolArguments } from '../../src/utils/validation';
|
|
4
|
+
import { sanitizeSurrogates } from '../../src/utils/sanitize-unicode';
|
|
5
|
+
import { parseStreamingJson } from '../../src/utils/json-parse';
|
|
6
|
+
import { buildOpenAIMessages, buildGoogleMessages } from '../../src/providers/convert';
|
|
7
|
+
import { Context, Model, Tool } from '../../src/types';
|
|
8
|
+
|
|
9
|
+
const mockOpenAIModel: Model<'openai'> = {
|
|
10
|
+
id: 'test',
|
|
11
|
+
name: 'Test',
|
|
12
|
+
api: 'openai',
|
|
13
|
+
baseUrl: 'https://api.openai.com',
|
|
14
|
+
reasoning: false,
|
|
15
|
+
input: ['text', 'image', 'file'],
|
|
16
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
17
|
+
contextWindow: 128000,
|
|
18
|
+
maxTokens: 4096,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mockGoogleModel: Model<'google'> = {
|
|
22
|
+
id: 'test',
|
|
23
|
+
name: 'Test',
|
|
24
|
+
api: 'google',
|
|
25
|
+
baseUrl: 'https://googleapis.com',
|
|
26
|
+
reasoning: false,
|
|
27
|
+
input: ['text', 'image', 'file'],
|
|
28
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
29
|
+
contextWindow: 128000,
|
|
30
|
+
maxTokens: 8192,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('Edge Cases - Context and Messages', () => {
|
|
34
|
+
it('should handle empty context (no messages, no system prompt)', () => {
|
|
35
|
+
const context: Context = {
|
|
36
|
+
messages: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
40
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
41
|
+
|
|
42
|
+
const openaiResult = buildOpenAIMessages(mockOpenAIModel, context);
|
|
43
|
+
const googleResult = buildGoogleMessages(mockGoogleModel, context);
|
|
44
|
+
|
|
45
|
+
expect(openaiResult).toEqual([]);
|
|
46
|
+
expect(googleResult).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should handle context with only system prompt', () => {
|
|
50
|
+
const context: Context = {
|
|
51
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
52
|
+
messages: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
56
|
+
|
|
57
|
+
const result = buildOpenAIMessages(mockOpenAIModel, context);
|
|
58
|
+
expect(result).toHaveLength(1);
|
|
59
|
+
expect((result[0] as any).role).toBe('developer');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle very long system prompt', () => {
|
|
63
|
+
const longPrompt = 'A'.repeat(10000);
|
|
64
|
+
const context: Context = {
|
|
65
|
+
systemPrompt: longPrompt,
|
|
66
|
+
messages: [],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
70
|
+
const result = buildOpenAIMessages(mockOpenAIModel, context);
|
|
71
|
+
expect((result[0] as any).content.length).toBeGreaterThan(5000);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle very long single message', () => {
|
|
75
|
+
const longText = 'B'.repeat(50000);
|
|
76
|
+
const context: Context = {
|
|
77
|
+
messages: [
|
|
78
|
+
{
|
|
79
|
+
role: 'user',
|
|
80
|
+
content: [{ type: 'text', content: longText }],
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
87
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Edge Cases - Unicode and Special Characters', () => {
|
|
92
|
+
it('should handle emojis in messages', () => {
|
|
93
|
+
const context: Context = {
|
|
94
|
+
messages: [
|
|
95
|
+
{
|
|
96
|
+
role: 'user',
|
|
97
|
+
content: [{ type: 'text', content: 'Hello 👋 🌍 ✨' }],
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const openaiResult = buildOpenAIMessages(mockOpenAIModel, context);
|
|
104
|
+
const googleResult = buildGoogleMessages(mockGoogleModel, context);
|
|
105
|
+
|
|
106
|
+
expect(openaiResult).toBeDefined();
|
|
107
|
+
expect(googleResult).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle special characters in tool arguments', () => {
|
|
111
|
+
const tool: Tool = {
|
|
112
|
+
name: 'test',
|
|
113
|
+
description: 'Test',
|
|
114
|
+
parameters: Type.Object({
|
|
115
|
+
text: Type.String(),
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const toolCall = {
|
|
120
|
+
type: 'toolCall' as const,
|
|
121
|
+
name: 'test',
|
|
122
|
+
arguments: {
|
|
123
|
+
text: '!@#$%^&*()_+-=[]{}|;:,.<>?/~`\'"\\',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
expect(() => validateToolArguments(tool, toolCall)).not.toThrow();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should handle Unicode surrogate pairs (emoji)', () => {
|
|
131
|
+
const text = '👨👩👧👦'; // Family emoji with ZWJ
|
|
132
|
+
const result = sanitizeSurrogates(text);
|
|
133
|
+
expect(result).toBe(text);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should remove unpaired surrogates', () => {
|
|
137
|
+
const text = 'Hello\uD800World'; // Unpaired high surrogate
|
|
138
|
+
const result = sanitizeSurrogates(text);
|
|
139
|
+
expect(result).toBe('HelloWorld');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('Edge Cases - Malformed Data', () => {
|
|
144
|
+
it('should handle malformed base64 in image content', () => {
|
|
145
|
+
const context: Context = {
|
|
146
|
+
messages: [
|
|
147
|
+
{
|
|
148
|
+
role: 'user',
|
|
149
|
+
content: [
|
|
150
|
+
{ type: 'text', content: 'Image:' },
|
|
151
|
+
{ type: 'image', data: 'not-valid-base64!@#', mimeType: 'image/png' },
|
|
152
|
+
],
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Should not throw, even with invalid base64
|
|
159
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
160
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should handle invalid MIME types', () => {
|
|
164
|
+
const context: Context = {
|
|
165
|
+
messages: [
|
|
166
|
+
{
|
|
167
|
+
role: 'user',
|
|
168
|
+
content: [
|
|
169
|
+
{ type: 'image', data: 'data', mimeType: 'invalid/mimetype/extra' },
|
|
170
|
+
],
|
|
171
|
+
timestamp: Date.now(),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
177
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should handle null/undefined in content arrays (defensive)', () => {
|
|
181
|
+
const context: Context = {
|
|
182
|
+
messages: [
|
|
183
|
+
{
|
|
184
|
+
role: 'user',
|
|
185
|
+
content: [
|
|
186
|
+
{ type: 'text', content: 'Valid text' },
|
|
187
|
+
],
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('Edge Cases - Tool Validation', () => {
|
|
198
|
+
it('should handle very large tool argument objects', () => {
|
|
199
|
+
const tool: Tool = {
|
|
200
|
+
name: 'test',
|
|
201
|
+
description: 'Test',
|
|
202
|
+
parameters: Type.Object({
|
|
203
|
+
data: Type.Any(),
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const largeData: Record<string, number> = {};
|
|
208
|
+
for (let i = 0; i < 1000; i++) {
|
|
209
|
+
largeData[`key${i}`] = i;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const toolCall = {
|
|
213
|
+
type: 'toolCall' as const,
|
|
214
|
+
name: 'test',
|
|
215
|
+
arguments: { data: largeData },
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const result = validateToolArguments(tool, toolCall);
|
|
219
|
+
expect((result as any).data).toBeDefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should handle tool with no parameters', () => {
|
|
223
|
+
const tool: Tool = {
|
|
224
|
+
name: 'test',
|
|
225
|
+
description: 'Test',
|
|
226
|
+
parameters: Type.Object({}),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const toolCall = {
|
|
230
|
+
type: 'toolCall' as const,
|
|
231
|
+
name: 'test',
|
|
232
|
+
arguments: {},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
expect(() => validateToolArguments(tool, toolCall)).not.toThrow();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should handle tool with only optional parameters', () => {
|
|
239
|
+
const tool: Tool = {
|
|
240
|
+
name: 'test',
|
|
241
|
+
description: 'Test',
|
|
242
|
+
parameters: Type.Object({
|
|
243
|
+
optional1: Type.Optional(Type.String()),
|
|
244
|
+
optional2: Type.Optional(Type.Number()),
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const toolCall = {
|
|
249
|
+
type: 'toolCall' as const,
|
|
250
|
+
name: 'test',
|
|
251
|
+
arguments: {},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
expect(() => validateToolArguments(tool, toolCall)).not.toThrow();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle nested optional parameters', () => {
|
|
258
|
+
const tool: Tool = {
|
|
259
|
+
name: 'test',
|
|
260
|
+
description: 'Test',
|
|
261
|
+
parameters: Type.Object({
|
|
262
|
+
level1: Type.Optional(
|
|
263
|
+
Type.Object({
|
|
264
|
+
level2: Type.Optional(
|
|
265
|
+
Type.Object({
|
|
266
|
+
value: Type.String(),
|
|
267
|
+
})
|
|
268
|
+
),
|
|
269
|
+
})
|
|
270
|
+
),
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const toolCall = {
|
|
275
|
+
type: 'toolCall' as const,
|
|
276
|
+
name: 'test',
|
|
277
|
+
arguments: {},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
expect(() => validateToolArguments(tool, toolCall)).not.toThrow();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should handle circular reference detection (if applicable)', () => {
|
|
284
|
+
const tool: Tool = {
|
|
285
|
+
name: 'test',
|
|
286
|
+
description: 'Test',
|
|
287
|
+
parameters: Type.Object({
|
|
288
|
+
data: Type.Any(),
|
|
289
|
+
}),
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const circular: any = { a: 1 };
|
|
293
|
+
circular.self = circular;
|
|
294
|
+
|
|
295
|
+
const toolCall = {
|
|
296
|
+
type: 'toolCall' as const,
|
|
297
|
+
name: 'test',
|
|
298
|
+
arguments: { data: circular },
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Validation should handle this gracefully (may fail validation or handle it)
|
|
302
|
+
// The important thing is it shouldn't crash
|
|
303
|
+
expect(() => {
|
|
304
|
+
try {
|
|
305
|
+
validateToolArguments(tool, toolCall);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
// It's okay if it throws a validation error
|
|
308
|
+
expect(e).toBeDefined();
|
|
309
|
+
}
|
|
310
|
+
}).not.toThrow(TypeError);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('Edge Cases - JSON Parsing', () => {
|
|
315
|
+
it('should handle malformed JSON gracefully', () => {
|
|
316
|
+
const malformed = '{name: "test"}'; // Missing quotes
|
|
317
|
+
const result = parseStreamingJson(malformed);
|
|
318
|
+
expect(result).toEqual({});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should handle incomplete JSON object', () => {
|
|
322
|
+
const incomplete = '{"name": "test", "age":';
|
|
323
|
+
const result = parseStreamingJson(incomplete);
|
|
324
|
+
expect(result).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should handle incomplete JSON array', () => {
|
|
328
|
+
const incomplete = '[1, 2, 3';
|
|
329
|
+
const result = parseStreamingJson(incomplete);
|
|
330
|
+
expect(result).toBeDefined();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should handle empty string', () => {
|
|
334
|
+
const result = parseStreamingJson('');
|
|
335
|
+
expect(result).toEqual({});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should handle null input (defensive)', () => {
|
|
339
|
+
const result = parseStreamingJson(null as any);
|
|
340
|
+
expect(result).toBeDefined();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should handle very deeply nested JSON', () => {
|
|
344
|
+
let json = '';
|
|
345
|
+
for (let i = 0; i < 100; i++) {
|
|
346
|
+
json += '{"a":';
|
|
347
|
+
}
|
|
348
|
+
json += '"value"';
|
|
349
|
+
for (let i = 0; i < 100; i++) {
|
|
350
|
+
json += '}';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
expect(() => parseStreamingJson(json)).not.toThrow();
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('Edge Cases - Message Content', () => {
|
|
358
|
+
it('should handle empty content array', () => {
|
|
359
|
+
const context: Context = {
|
|
360
|
+
messages: [
|
|
361
|
+
{
|
|
362
|
+
role: 'user',
|
|
363
|
+
content: [],
|
|
364
|
+
timestamp: Date.now(),
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
370
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should handle mix of all content types', () => {
|
|
374
|
+
const context: Context = {
|
|
375
|
+
messages: [
|
|
376
|
+
{
|
|
377
|
+
role: 'user',
|
|
378
|
+
content: [
|
|
379
|
+
{ type: 'text', content: 'Text' },
|
|
380
|
+
{ type: 'image', data: 'img', mimeType: 'image/png' },
|
|
381
|
+
{ type: 'file', data: 'file', mimeType: 'application/pdf' },
|
|
382
|
+
{ type: 'text', content: 'More text' },
|
|
383
|
+
],
|
|
384
|
+
timestamp: Date.now(),
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
390
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should handle very long base64 strings (large images)', () => {
|
|
394
|
+
const largeBase64 = 'A'.repeat(1000000); // 1MB of data
|
|
395
|
+
const context: Context = {
|
|
396
|
+
messages: [
|
|
397
|
+
{
|
|
398
|
+
role: 'user',
|
|
399
|
+
content: [
|
|
400
|
+
{ type: 'image', data: largeBase64, mimeType: 'image/jpeg' },
|
|
401
|
+
],
|
|
402
|
+
timestamp: Date.now(),
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
408
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('Edge Cases - Tool Results', () => {
|
|
413
|
+
it('should handle tool result without call ID', () => {
|
|
414
|
+
const context: Context = {
|
|
415
|
+
messages: [
|
|
416
|
+
{
|
|
417
|
+
role: 'toolResult',
|
|
418
|
+
toolName: 'test',
|
|
419
|
+
content: [{ type: 'text', content: 'Result' }],
|
|
420
|
+
isError: false,
|
|
421
|
+
timestamp: Date.now(),
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Should handle gracefully (may use undefined or empty string)
|
|
427
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
428
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should handle tool result with empty content', () => {
|
|
432
|
+
const context: Context = {
|
|
433
|
+
messages: [
|
|
434
|
+
{
|
|
435
|
+
role: 'toolResult',
|
|
436
|
+
toolName: 'test',
|
|
437
|
+
toolCallId: 'call_1',
|
|
438
|
+
content: [],
|
|
439
|
+
isError: false,
|
|
440
|
+
timestamp: Date.now(),
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
446
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should handle tool result with very long error message', () => {
|
|
450
|
+
const longError = 'E'.repeat(10000);
|
|
451
|
+
const context: Context = {
|
|
452
|
+
messages: [
|
|
453
|
+
{
|
|
454
|
+
role: 'toolResult',
|
|
455
|
+
toolName: 'test',
|
|
456
|
+
toolCallId: 'call_1',
|
|
457
|
+
content: [{ type: 'text', content: longError }],
|
|
458
|
+
isError: true,
|
|
459
|
+
error: {
|
|
460
|
+
message: longError,
|
|
461
|
+
name: 'Error',
|
|
462
|
+
},
|
|
463
|
+
timestamp: Date.now(),
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
469
|
+
expect(() => buildGoogleMessages(mockGoogleModel, context)).not.toThrow();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe('Edge Cases - Special Characters in Strings', () => {
|
|
474
|
+
it('should handle newlines and tabs', () => {
|
|
475
|
+
const context: Context = {
|
|
476
|
+
systemPrompt: 'Line1\nLine2\tTabbed',
|
|
477
|
+
messages: [],
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should handle quotes and apostrophes', () => {
|
|
484
|
+
const context: Context = {
|
|
485
|
+
systemPrompt: `"Hello" 'World' \\"Escaped\\"`,
|
|
486
|
+
messages: [],
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should handle backslashes', () => {
|
|
493
|
+
const context: Context = {
|
|
494
|
+
systemPrompt: 'Path: C:\\Users\\Test\\File.txt',
|
|
495
|
+
messages: [],
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should handle null characters (if they somehow appear)', () => {
|
|
502
|
+
const context: Context = {
|
|
503
|
+
systemPrompt: 'Before\0After',
|
|
504
|
+
messages: [],
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('Edge Cases - Extreme Values', () => {
|
|
512
|
+
it('should handle timestamp with very large value', () => {
|
|
513
|
+
const context: Context = {
|
|
514
|
+
messages: [
|
|
515
|
+
{
|
|
516
|
+
role: 'user',
|
|
517
|
+
content: [{ type: 'text', content: 'Test' }],
|
|
518
|
+
timestamp: Number.MAX_SAFE_INTEGER,
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should handle timestamp with zero', () => {
|
|
527
|
+
const context: Context = {
|
|
528
|
+
messages: [
|
|
529
|
+
{
|
|
530
|
+
role: 'user',
|
|
531
|
+
content: [{ type: 'text', content: 'Test' }],
|
|
532
|
+
timestamp: 0,
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
expect(() => buildOpenAIMessages(mockOpenAIModel, context)).not.toThrow();
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe('Edge Cases - Model Configuration', () => {
|
|
542
|
+
it('should handle model with text-only input', () => {
|
|
543
|
+
const textOnlyModel: Model<'openai'> = {
|
|
544
|
+
...mockOpenAIModel,
|
|
545
|
+
input: ['text'],
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const context: Context = {
|
|
549
|
+
messages: [
|
|
550
|
+
{
|
|
551
|
+
role: 'user',
|
|
552
|
+
content: [
|
|
553
|
+
{ type: 'text', content: 'Text' },
|
|
554
|
+
{ type: 'image', data: 'img', mimeType: 'image/png' },
|
|
555
|
+
],
|
|
556
|
+
timestamp: Date.now(),
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const result = buildOpenAIMessages(textOnlyModel, context);
|
|
562
|
+
// Should only include text content
|
|
563
|
+
expect((result[0] as any).content.length).toBe(1);
|
|
564
|
+
});
|
|
565
|
+
});
|