@aj-archipelago/cortex 1.4.6 → 1.4.7

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.
Files changed (38) hide show
  1. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  2. package/helper-apps/cortex-file-handler/package.json +1 -1
  3. package/helper-apps/cortex-file-handler/src/index.js +27 -4
  4. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +74 -10
  5. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +23 -2
  6. package/helper-apps/cortex-file-handler/src/start.js +2 -0
  7. package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +287 -0
  8. package/helper-apps/cortex-file-handler/tests/start.test.js +1 -1
  9. package/lib/entityConstants.js +1 -1
  10. package/lib/fileUtils.js +1481 -0
  11. package/lib/pathwayTools.js +7 -1
  12. package/lib/util.js +2 -313
  13. package/package.json +4 -3
  14. package/pathways/image_qwen.js +1 -1
  15. package/pathways/system/entity/memory/sys_read_memory.js +17 -3
  16. package/pathways/system/entity/memory/sys_save_memory.js +22 -6
  17. package/pathways/system/entity/sys_entity_agent.js +21 -4
  18. package/pathways/system/entity/tools/sys_tool_analyzefile.js +171 -0
  19. package/pathways/system/entity/tools/sys_tool_codingagent.js +38 -4
  20. package/pathways/system/entity/tools/sys_tool_editfile.js +403 -0
  21. package/pathways/system/entity/tools/sys_tool_file_collection.js +433 -0
  22. package/pathways/system/entity/tools/sys_tool_image.js +172 -10
  23. package/pathways/system/entity/tools/sys_tool_image_gemini.js +123 -10
  24. package/pathways/system/entity/tools/sys_tool_readfile.js +217 -124
  25. package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
  26. package/pathways/system/entity/tools/sys_tool_writefile.js +211 -0
  27. package/pathways/system/workspaces/run_workspace_prompt.js +4 -3
  28. package/pathways/transcribe_gemini.js +2 -1
  29. package/server/executeWorkspace.js +1 -1
  30. package/server/plugins/neuralSpacePlugin.js +2 -6
  31. package/server/plugins/openAiWhisperPlugin.js +2 -1
  32. package/server/plugins/replicateApiPlugin.js +4 -14
  33. package/server/typeDef.js +10 -1
  34. package/tests/integration/features/tools/fileCollection.test.js +858 -0
  35. package/tests/integration/features/tools/fileOperations.test.js +851 -0
  36. package/tests/integration/features/tools/writefile.test.js +350 -0
  37. package/tests/unit/core/fileCollection.test.js +259 -0
  38. package/tests/unit/core/util.test.js +320 -1
@@ -0,0 +1,350 @@
1
+ // writefile.test.js
2
+ // Integration tests for WriteFile tool
3
+
4
+ import test from 'ava';
5
+ import serverFactory from '../../../../index.js';
6
+ import { callPathway } from '../../../../lib/pathwayTools.js';
7
+
8
+ let testServer;
9
+
10
+ test.before(async () => {
11
+ const { server, startServer } = await serverFactory();
12
+ if (startServer) {
13
+ await startServer();
14
+ }
15
+ testServer = server;
16
+ });
17
+
18
+ test.after.always('cleanup', async () => {
19
+ if (testServer) {
20
+ await testServer.stop();
21
+ }
22
+ });
23
+
24
+ // Helper to create a test context
25
+ const createTestContext = () => {
26
+ const contextId = `test-writefile-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
27
+ return contextId;
28
+ };
29
+
30
+ // Helper to extract files array from stored format (handles both old array format and new {version, files} format)
31
+ const extractFilesFromStored = (stored) => {
32
+ if (!stored) return [];
33
+ const parsed = typeof stored === 'string' ? JSON.parse(stored) : stored;
34
+ // Handle new format: { version, files }
35
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.files) {
36
+ return Array.isArray(parsed.files) ? parsed.files : [];
37
+ }
38
+ // Handle old format: just an array
39
+ if (Array.isArray(parsed)) {
40
+ return parsed;
41
+ }
42
+ return [];
43
+ };
44
+
45
+ // Helper to clean up test data
46
+ const cleanup = async (contextId, contextKey = null) => {
47
+ try {
48
+ const { keyValueStorageClient } = await import('../../../../lib/keyValueStorageClient.js');
49
+ // Delete the key entirely instead of setting to empty array
50
+ await keyValueStorageClient.delete(`${contextId}-memoryFiles`);
51
+ } catch (e) {
52
+ // Ignore cleanup errors
53
+ }
54
+ };
55
+
56
+ test('WriteFile: Write and upload text file', async t => {
57
+ const contextId = createTestContext();
58
+
59
+ try {
60
+ const content = 'Hello, world!\nThis is a test file.';
61
+ const filename = 'test.txt';
62
+
63
+ const result = await callPathway('sys_tool_writefile', {
64
+ contextId,
65
+ content,
66
+ filename,
67
+ userMessage: 'Writing test file'
68
+ });
69
+
70
+ const parsed = JSON.parse(result);
71
+
72
+ // Skip test if file handler is not configured
73
+ if (!parsed.success && parsed.error?.includes('WHISPER_MEDIA_API_URL')) {
74
+ t.log('Test skipped - file handler URL not configured');
75
+ t.pass();
76
+ return;
77
+ }
78
+
79
+ t.is(parsed.success, true);
80
+ t.is(parsed.filename, filename);
81
+ t.truthy(parsed.url);
82
+ t.is(parsed.size, Buffer.byteLength(content, 'utf8'));
83
+ t.true(parsed.message.includes('written and uploaded successfully'));
84
+
85
+ // Verify it was added to file collection
86
+ const saved = await callPathway('sys_read_memory', {
87
+ contextId,
88
+ section: 'memoryFiles'
89
+ });
90
+ const collection = extractFilesFromStored(saved);
91
+ t.is(collection.length, 1);
92
+ t.is(collection[0].filename, filename);
93
+ t.is(collection[0].url, parsed.url);
94
+ t.truthy(collection[0].hash);
95
+ } finally {
96
+ await cleanup(contextId);
97
+ }
98
+ });
99
+
100
+ test('WriteFile: Write JSON file with tags and notes', async t => {
101
+ const contextId = createTestContext();
102
+
103
+ try {
104
+ const content = JSON.stringify({ name: 'Test', value: 42 }, null, 2);
105
+ const filename = 'data.json';
106
+ const tags = ['data', 'test'];
107
+ const notes = 'Test JSON file';
108
+
109
+ const result = await callPathway('sys_tool_writefile', {
110
+ contextId,
111
+ content,
112
+ filename,
113
+ tags,
114
+ notes,
115
+ userMessage: 'Writing JSON file'
116
+ });
117
+
118
+ const parsed = JSON.parse(result);
119
+
120
+ // Skip test if file handler is not configured
121
+ if (!parsed.success && parsed.error?.includes('WHISPER_MEDIA_API_URL')) {
122
+ t.log('Test skipped - file handler URL not configured');
123
+ t.pass();
124
+ return;
125
+ }
126
+
127
+ t.is(parsed.success, true);
128
+ t.is(parsed.filename, filename);
129
+ t.truthy(parsed.url);
130
+ t.truthy(parsed.hash);
131
+ t.is(parsed.size, Buffer.byteLength(content, 'utf8'));
132
+
133
+ // Verify it was added to file collection with metadata
134
+ const saved = await callPathway('sys_read_memory', {
135
+ contextId,
136
+ section: 'memoryFiles'
137
+ });
138
+ const collection = extractFilesFromStored(saved);
139
+ t.is(collection.length, 1);
140
+ t.is(collection[0].filename, filename);
141
+ t.deepEqual(collection[0].tags, tags);
142
+ t.is(collection[0].notes, notes);
143
+ } finally {
144
+ await cleanup(contextId);
145
+ }
146
+ });
147
+
148
+ test('WriteFile: Write file without contextId (no collection)', async t => {
149
+ try {
150
+ const content = 'Standalone file content';
151
+ const filename = 'standalone.txt';
152
+
153
+ const result = await callPathway('sys_tool_writefile', {
154
+ content,
155
+ filename,
156
+ userMessage: 'Writing standalone file'
157
+ });
158
+
159
+ const parsed = JSON.parse(result);
160
+ // This test may fail if WHISPER_MEDIA_API_URL is not set
161
+ if (!parsed.success && parsed.error?.includes('WHISPER_MEDIA_API_URL')) {
162
+ t.log('Test skipped - file handler URL not configured');
163
+ t.pass();
164
+ } else {
165
+ t.is(parsed.success, true);
166
+ t.is(parsed.filename, filename);
167
+ t.truthy(parsed.url);
168
+ t.is(parsed.fileId, null); // Should be null since no contextId
169
+ }
170
+ } catch (error) {
171
+ // This is expected if WHISPER_MEDIA_API_URL is not set in test environment
172
+ t.log('Test skipped - file handler URL not configured');
173
+ t.pass();
174
+ }
175
+ });
176
+
177
+ test('WriteFile: Error handling - missing content', async t => {
178
+ const contextId = createTestContext();
179
+
180
+ try {
181
+ const result = await callPathway('sys_tool_writefile', {
182
+ contextId,
183
+ filename: 'test.txt',
184
+ userMessage: 'Missing content'
185
+ });
186
+
187
+ const parsed = JSON.parse(result);
188
+ t.is(parsed.success, false);
189
+ t.true(parsed.error?.includes('content is required') || parsed.error?.includes('required'));
190
+ } finally {
191
+ await cleanup(contextId);
192
+ }
193
+ });
194
+
195
+ test('WriteFile: Error handling - missing filename', async t => {
196
+ const contextId = createTestContext();
197
+
198
+ try {
199
+ const result = await callPathway('sys_tool_writefile', {
200
+ contextId,
201
+ content: 'Some content',
202
+ userMessage: 'Missing filename'
203
+ });
204
+
205
+ const parsed = JSON.parse(result);
206
+ t.is(parsed.success, false);
207
+ t.true(parsed.error?.includes('filename is required') || parsed.error?.includes('required'));
208
+ } finally {
209
+ await cleanup(contextId);
210
+ }
211
+ });
212
+
213
+ test('WriteFile: Different file types and MIME types', async t => {
214
+ const contextId = createTestContext();
215
+
216
+ try {
217
+ const testCases = [
218
+ { content: 'console.log("hello");', filename: 'script.js', expectedMime: 'application/javascript' },
219
+ { content: 'def hello(): pass', filename: 'script.py', expectedMime: 'text/x-python' },
220
+ { content: '# Hello', filename: 'readme.md', expectedMime: 'text/markdown' },
221
+ { content: '<html></html>', filename: 'page.html', expectedMime: 'text/html' },
222
+ { content: 'name,value\nTest,42', filename: 'data.csv', expectedMime: 'text/csv' }
223
+ ];
224
+
225
+ let successCount = 0;
226
+ for (const testCase of testCases) {
227
+ const result = await callPathway('sys_tool_writefile', {
228
+ contextId,
229
+ content: testCase.content,
230
+ filename: testCase.filename,
231
+ userMessage: `Writing ${testCase.filename}`
232
+ });
233
+
234
+ const parsed = JSON.parse(result);
235
+
236
+ // Skip test if file handler is not configured
237
+ if (!parsed.success && parsed.error?.includes('WHISPER_MEDIA_API_URL')) {
238
+ t.log('Test skipped - file handler URL not configured');
239
+ t.pass();
240
+ return;
241
+ }
242
+
243
+ t.is(parsed.success, true);
244
+ t.is(parsed.filename, testCase.filename);
245
+ t.truthy(parsed.url);
246
+ successCount++;
247
+ }
248
+
249
+ // Verify all files were added
250
+ const saved = await callPathway('sys_read_memory', {
251
+ contextId,
252
+ section: 'memoryFiles'
253
+ });
254
+ const collection = extractFilesFromStored(saved);
255
+ t.is(collection.length, successCount);
256
+ } finally {
257
+ await cleanup(contextId);
258
+ }
259
+ });
260
+
261
+ test('WriteFile: Large content', async t => {
262
+ const contextId = createTestContext();
263
+
264
+ try {
265
+ // Create a large content string (100KB)
266
+ const largeContent = 'A'.repeat(100 * 1024);
267
+ const filename = 'large.txt';
268
+
269
+ const result = await callPathway('sys_tool_writefile', {
270
+ contextId,
271
+ content: largeContent,
272
+ filename,
273
+ userMessage: 'Writing large file'
274
+ });
275
+
276
+ const parsed = JSON.parse(result);
277
+
278
+ // Skip test if file handler is not configured
279
+ if (!parsed.success && parsed.error?.includes('WHISPER_MEDIA_API_URL')) {
280
+ t.log('Test skipped - file handler URL not configured');
281
+ t.pass();
282
+ return;
283
+ }
284
+
285
+ t.is(parsed.success, true);
286
+ t.is(parsed.filename, filename);
287
+ t.is(parsed.size, Buffer.byteLength(largeContent, 'utf8'));
288
+ t.truthy(parsed.url);
289
+ t.truthy(parsed.hash);
290
+ } finally {
291
+ await cleanup(contextId);
292
+ }
293
+ });
294
+
295
+ test('WriteFile: Duplicate content (same hash)', async t => {
296
+ const contextId = createTestContext();
297
+
298
+ try {
299
+ const content = 'Duplicate test content';
300
+ const filename1 = 'file1.txt';
301
+ const filename2 = 'file2.txt';
302
+
303
+ // Write first file
304
+ const result1 = await callPathway('sys_tool_writefile', {
305
+ contextId,
306
+ content,
307
+ filename: filename1,
308
+ userMessage: 'Writing first file'
309
+ });
310
+
311
+ const parsed1 = JSON.parse(result1);
312
+
313
+ // Skip test if file handler is not configured
314
+ if (!parsed1.success && parsed1.error?.includes('WHISPER_MEDIA_API_URL')) {
315
+ t.log('Test skipped - file handler URL not configured');
316
+ t.pass();
317
+ return;
318
+ }
319
+
320
+ t.is(parsed1.success, true);
321
+ t.truthy(parsed1.hash);
322
+ const firstHash = parsed1.hash;
323
+
324
+ // Write second file with same content (should reuse hash)
325
+ const result2 = await callPathway('sys_tool_writefile', {
326
+ contextId,
327
+ content,
328
+ filename: filename2,
329
+ userMessage: 'Writing duplicate file'
330
+ });
331
+
332
+ const parsed2 = JSON.parse(result2);
333
+ t.is(parsed2.success, true);
334
+ t.is(parsed2.hash, firstHash); // Should have same hash
335
+
336
+ // Both files should be in collection with different filenames but same hash
337
+ const saved = await callPathway('sys_read_memory', {
338
+ contextId,
339
+ section: 'memoryFiles'
340
+ });
341
+ const collection = extractFilesFromStored(saved);
342
+ t.is(collection.length, 2);
343
+ t.true(collection.some(f => f.filename === filename1));
344
+ t.true(collection.some(f => f.filename === filename2));
345
+ t.is(collection[0].hash, collection[1].hash); // Same hash
346
+ } finally {
347
+ await cleanup(contextId);
348
+ }
349
+ });
350
+
@@ -0,0 +1,259 @@
1
+ // fileCollection.test.js
2
+ // Tests for file collection utility functions
3
+
4
+ import test from 'ava';
5
+ import {
6
+ extractFilesFromChatHistory,
7
+ formatFilesForTemplate
8
+ } from '../../../lib/fileUtils.js';
9
+
10
+ // Test extractFilesFromChatHistory
11
+ test('extractFilesFromChatHistory should extract files from array content', t => {
12
+ const chatHistory = [
13
+ {
14
+ role: 'user',
15
+ content: [
16
+ { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' }, gcs: 'gs://bucket/image.jpg', originalFilename: 'image.jpg' },
17
+ { type: 'file', url: 'https://example.com/doc.pdf', gcs: 'gs://bucket/doc.pdf', originalFilename: 'doc.pdf' }
18
+ ]
19
+ }
20
+ ];
21
+
22
+ const files = extractFilesFromChatHistory(chatHistory);
23
+ t.is(files.length, 2);
24
+ t.is(files[0].url, 'https://example.com/image.jpg');
25
+ t.is(files[0].gcs, 'gs://bucket/image.jpg');
26
+ t.is(files[0].filename, 'image.jpg');
27
+ t.is(files[1].url, 'https://example.com/doc.pdf');
28
+ t.is(files[1].gcs, 'gs://bucket/doc.pdf');
29
+ t.is(files[1].filename, 'doc.pdf');
30
+ });
31
+
32
+ test('extractFilesFromChatHistory should extract files from string JSON content', t => {
33
+ const chatHistory = [
34
+ {
35
+ role: 'user',
36
+ content: JSON.stringify({
37
+ type: 'image_url',
38
+ image_url: { url: 'https://example.com/image.jpg' },
39
+ gcs: 'gs://bucket/image.jpg',
40
+ originalFilename: 'image.jpg'
41
+ })
42
+ }
43
+ ];
44
+
45
+ const files = extractFilesFromChatHistory(chatHistory);
46
+ t.is(files.length, 1);
47
+ t.is(files[0].url, 'https://example.com/image.jpg');
48
+ t.is(files[0].gcs, 'gs://bucket/image.jpg');
49
+ });
50
+
51
+ test('extractFilesFromChatHistory should extract files from array content with file type', t => {
52
+ const chatHistory = [
53
+ {
54
+ role: 'user',
55
+ content: [
56
+ {
57
+ type: 'file',
58
+ url: 'https://example.com/doc.pdf',
59
+ gcs: 'gs://bucket/doc.pdf',
60
+ originalFilename: 'doc.pdf',
61
+ hash: 'abc123'
62
+ }
63
+ ]
64
+ }
65
+ ];
66
+
67
+ const files = extractFilesFromChatHistory(chatHistory);
68
+ t.is(files.length, 1);
69
+ t.is(files[0].url, 'https://example.com/doc.pdf');
70
+ t.is(files[0].hash, 'abc123');
71
+ });
72
+
73
+ test('extractFilesFromChatHistory should handle empty chat history', t => {
74
+ t.deepEqual(extractFilesFromChatHistory([]), []);
75
+ t.deepEqual(extractFilesFromChatHistory(null), []);
76
+ t.deepEqual(extractFilesFromChatHistory(undefined), []);
77
+ });
78
+
79
+ test('extractFilesFromChatHistory should handle messages without content', t => {
80
+ const chatHistory = [
81
+ { role: 'user' },
82
+ { role: 'assistant', content: 'Hello' }
83
+ ];
84
+
85
+ const files = extractFilesFromChatHistory(chatHistory);
86
+ t.is(files.length, 0);
87
+ });
88
+
89
+ test('extractFilesFromChatHistory should handle invalid JSON gracefully', t => {
90
+ const chatHistory = [
91
+ {
92
+ role: 'user',
93
+ content: 'not valid json {'
94
+ }
95
+ ];
96
+
97
+ const files = extractFilesFromChatHistory(chatHistory);
98
+ t.is(files.length, 0);
99
+ });
100
+
101
+
102
+ // Test formatFilesForTemplate
103
+ test('formatFilesForTemplate should format files correctly', t => {
104
+ const collection = [
105
+ {
106
+ id: 'file-1',
107
+ url: 'https://example.com/image.jpg',
108
+ gcs: 'gs://bucket/image.jpg',
109
+ filename: 'image.jpg',
110
+ hash: 'abc123',
111
+ addedDate: '2024-01-01T00:00:00Z',
112
+ lastAccessed: '2024-01-02T00:00:00Z',
113
+ tags: ['photo'],
114
+ notes: 'Test image'
115
+ },
116
+ {
117
+ id: 'file-2',
118
+ url: 'https://example.com/doc.pdf',
119
+ filename: 'doc.pdf',
120
+ hash: 'def456',
121
+ addedDate: '2024-01-02T00:00:00Z',
122
+ lastAccessed: '2024-01-03T00:00:00Z'
123
+ }
124
+ ];
125
+
126
+ const result = formatFilesForTemplate(collection);
127
+ t.true(result.includes('Hash | Filename | URL | Date Added | Notes'));
128
+ t.true(result.includes('def456 | doc.pdf |'));
129
+ t.true(result.includes('abc123 | image.jpg |'));
130
+ t.true(result.includes('Test image'));
131
+ // Should be sorted by lastAccessed (most recent first)
132
+ const docIndex = result.indexOf('def456');
133
+ const imageIndex = result.indexOf('abc123');
134
+ t.true(docIndex < imageIndex, 'More recently accessed file should appear first');
135
+ });
136
+
137
+ test('formatFilesForTemplate should handle empty collection', t => {
138
+ t.is(formatFilesForTemplate([]), 'No files available.');
139
+ t.is(formatFilesForTemplate(null), 'No files available.');
140
+ });
141
+
142
+ test('formatFilesForTemplate should handle files without optional fields', t => {
143
+ const collection = [
144
+ {
145
+ id: 'file-1',
146
+ url: 'https://example.com/image.jpg',
147
+ filename: 'image.jpg',
148
+ addedDate: '2024-01-01T00:00:00Z'
149
+ }
150
+ ];
151
+
152
+ const result = formatFilesForTemplate(collection);
153
+ t.true(result.includes('Hash | Filename | URL | Date Added | Notes'));
154
+ t.true(result.includes(' | image.jpg |'));
155
+ t.false(result.includes('Azure URL'));
156
+ t.false(result.includes('GCS URL'));
157
+ t.false(result.includes('Tags'));
158
+ });
159
+
160
+ test('formatFilesForTemplate should limit to 10 files and show note', t => {
161
+ const collection = Array.from({ length: 15 }, (_, i) => ({
162
+ id: `file-${i}`,
163
+ filename: `file${i}.txt`,
164
+ hash: `hash${i}`,
165
+ addedDate: `2024-01-${String(i + 1).padStart(2, '0')}T00:00:00Z`,
166
+ lastAccessed: `2024-01-${String(i + 1).padStart(2, '0')}T00:00:00Z`
167
+ }));
168
+
169
+ const result = formatFilesForTemplate(collection);
170
+ // Should only show 10 files - count file lines (excluding header, separator, and note)
171
+ const lines = result.split('\n');
172
+ // Find the separator line index
173
+ const separatorIndex = lines.findIndex(line => line.startsWith('-'));
174
+ // Count file lines (between separator and note, or end of result)
175
+ const fileLines = lines.slice(separatorIndex + 1).filter(line =>
176
+ line.includes('|') && !line.startsWith('Note:')
177
+ );
178
+ const fileCount = fileLines.length;
179
+ t.is(fileCount, 10);
180
+ // Should include note about more files
181
+ t.true(result.includes('Note: Showing the last 10 most recently used files'));
182
+ t.true(result.includes('5 more file(s) are available'));
183
+ });
184
+
185
+ test('extractFilesFromChatHistory should handle mixed content types', t => {
186
+ const chatHistory = [
187
+ {
188
+ role: 'user',
189
+ content: [
190
+ 'Hello',
191
+ { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' }, gcs: 'gs://bucket/image.jpg' },
192
+ { type: 'text', text: 'Some text' }
193
+ ]
194
+ }
195
+ ];
196
+
197
+ const files = extractFilesFromChatHistory(chatHistory);
198
+ t.is(files.length, 1);
199
+ t.is(files[0].url, 'https://example.com/image.jpg');
200
+ });
201
+
202
+ test('extractFilesFromChatHistory should extract files with hash', t => {
203
+ const chatHistory = [
204
+ {
205
+ role: 'user',
206
+ content: {
207
+ type: 'image_url',
208
+ image_url: { url: 'https://example.com/image.jpg' },
209
+ hash: 'abc123def456'
210
+ }
211
+ }
212
+ ];
213
+
214
+ const files = extractFilesFromChatHistory(chatHistory);
215
+ t.is(files.length, 1);
216
+ t.is(files[0].hash, 'abc123def456');
217
+ });
218
+
219
+ test('extractFilesFromChatHistory should handle files without gcsUrl', t => {
220
+ const chatHistory = [
221
+ {
222
+ role: 'user',
223
+ content: {
224
+ type: 'image_url',
225
+ image_url: { url: 'https://example.com/image.jpg' }
226
+ }
227
+ }
228
+ ];
229
+
230
+ const files = extractFilesFromChatHistory(chatHistory);
231
+ t.is(files.length, 1);
232
+ t.is(files[0].gcs, null);
233
+ });
234
+
235
+ test('extractFilesFromChatHistory should extract filename from various fields', t => {
236
+ const testCases = [
237
+ { originalFilename: 'file1.jpg', expected: 'file1.jpg' },
238
+ { name: 'file2.jpg', expected: 'file2.jpg' },
239
+ { filename: 'file3.jpg', expected: 'file3.jpg' },
240
+ { url: 'https://example.com/file4.jpg', expected: null } // Will extract from URL
241
+ ];
242
+
243
+ testCases.forEach((testCase, index) => {
244
+ const chatHistory = [{
245
+ role: 'user',
246
+ content: {
247
+ type: 'image_url',
248
+ image_url: { url: testCase.url || 'https://example.com/test.jpg' },
249
+ ...testCase
250
+ }
251
+ }];
252
+
253
+ const files = extractFilesFromChatHistory(chatHistory);
254
+ if (testCase.expected) {
255
+ t.is(files[0].filename, testCase.expected, `Test case ${index} failed`);
256
+ }
257
+ });
258
+ });
259
+