@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,858 @@
1
+ // fileCollection.test.js
2
+ // Integration tests for file collection tool
3
+
4
+ import test from 'ava';
5
+ import serverFactory from '../../../../index.js';
6
+ import { callPathway } from '../../../../lib/pathwayTools.js';
7
+ import { generateFileMessageContent, resolveFileParameter } from '../../../../lib/fileUtils.js';
8
+
9
+ let testServer;
10
+
11
+ test.before(async () => {
12
+ const { server, startServer } = await serverFactory();
13
+ if (startServer) {
14
+ await startServer();
15
+ }
16
+ testServer = server;
17
+ });
18
+
19
+ test.after.always('cleanup', async () => {
20
+ if (testServer) {
21
+ await testServer.stop();
22
+ }
23
+ });
24
+
25
+ // Helper to create a test context
26
+ const createTestContext = () => {
27
+ const contextId = `test-file-collection-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
28
+ return contextId;
29
+ };
30
+
31
+ // Helper to extract files array from stored format (handles both old array format and new {version, files} format)
32
+ const extractFilesFromStored = (stored) => {
33
+ if (!stored) return [];
34
+ const parsed = typeof stored === 'string' ? JSON.parse(stored) : stored;
35
+ // Handle new format: { version, files }
36
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.files) {
37
+ return Array.isArray(parsed.files) ? parsed.files : [];
38
+ }
39
+ // Handle old format: just an array
40
+ if (Array.isArray(parsed)) {
41
+ return parsed;
42
+ }
43
+ return [];
44
+ };
45
+
46
+ // Helper to clean up test data
47
+ const cleanup = async (contextId, contextKey = null) => {
48
+ try {
49
+ const { keyValueStorageClient } = await import('../../../../lib/keyValueStorageClient.js');
50
+ // Delete the key entirely instead of setting to empty array
51
+ await keyValueStorageClient.delete(`${contextId}-memoryFiles`);
52
+ } catch (e) {
53
+ // Ignore cleanup errors
54
+ }
55
+ };
56
+
57
+ test('File collection: Add file to collection', async t => {
58
+ const contextId = createTestContext();
59
+
60
+ try {
61
+ const result = await callPathway('sys_tool_file_collection', {
62
+ contextId,
63
+ url: 'https://example.com/test.jpg',
64
+ gcs: 'gs://bucket/test.jpg',
65
+ filename: 'test.jpg',
66
+ tags: ['photo', 'test'],
67
+ notes: 'Test file',
68
+ userMessage: 'Adding test file'
69
+ });
70
+
71
+ const parsed = JSON.parse(result);
72
+ t.is(parsed.success, true);
73
+ t.truthy(parsed.fileId);
74
+ t.true(parsed.message.includes('test.jpg'));
75
+
76
+ // Verify it was saved
77
+ const saved = await callPathway('sys_read_memory', {
78
+ contextId,
79
+ section: 'memoryFiles'
80
+ });
81
+ const collection = extractFilesFromStored(saved);
82
+ t.is(collection.length, 1);
83
+ t.is(collection[0].filename, 'test.jpg');
84
+ t.is(collection[0].url, 'https://example.com/test.jpg');
85
+ t.is(collection[0].gcs, 'gs://bucket/test.jpg');
86
+ t.deepEqual(collection[0].tags, ['photo', 'test']);
87
+ t.is(collection[0].notes, 'Test file');
88
+ } finally {
89
+ await cleanup(contextId);
90
+ }
91
+ });
92
+
93
+ test('File collection: List files', async t => {
94
+ const contextId = createTestContext();
95
+
96
+ try {
97
+ // Add a few files first
98
+ await callPathway('sys_tool_file_collection', {
99
+ contextId,
100
+ url: 'https://example.com/file1.jpg',
101
+ filename: 'file1.jpg',
102
+ userMessage: 'Add file 1'
103
+ });
104
+
105
+ await callPathway('sys_tool_file_collection', {
106
+ contextId,
107
+ url: 'https://example.com/file2.pdf',
108
+ filename: 'file2.pdf',
109
+ tags: ['document'],
110
+ userMessage: 'Add file 2'
111
+ });
112
+
113
+ // List files
114
+ const result = await callPathway('sys_tool_file_collection', {
115
+ contextId,
116
+ userMessage: 'List files'
117
+ });
118
+
119
+ const parsed = JSON.parse(result);
120
+ t.is(parsed.success, true);
121
+ t.is(parsed.count, 2);
122
+ t.is(parsed.totalFiles, 2);
123
+ t.is(parsed.files.length, 2);
124
+ t.true(parsed.files.some(f => f.filename === 'file1.jpg'));
125
+ t.true(parsed.files.some(f => f.filename === 'file2.pdf'));
126
+ } finally {
127
+ await cleanup(contextId);
128
+ }
129
+ });
130
+
131
+ test('File collection: Search files', async t => {
132
+ const contextId = createTestContext();
133
+
134
+ try {
135
+ // Add files with different metadata
136
+ await callPathway('sys_tool_file_collection', {
137
+ contextId,
138
+ url: 'https://example.com/report.pdf',
139
+ filename: 'report.pdf',
140
+ tags: ['document', 'report'],
141
+ notes: 'Monthly report',
142
+ userMessage: 'Add report'
143
+ });
144
+
145
+ await callPathway('sys_tool_file_collection', {
146
+ contextId,
147
+ url: 'https://example.com/image.jpg',
148
+ filename: 'image.jpg',
149
+ tags: ['photo'],
150
+ notes: 'Photo of office',
151
+ userMessage: 'Add image'
152
+ });
153
+
154
+ // Search by filename
155
+ const result1 = await callPathway('sys_tool_file_collection', {
156
+ contextId,
157
+ query: 'report',
158
+ userMessage: 'Search for report'
159
+ });
160
+
161
+ const parsed1 = JSON.parse(result1);
162
+ t.is(parsed1.success, true);
163
+ t.is(parsed1.count, 1);
164
+ t.is(parsed1.files[0].filename, 'report.pdf');
165
+
166
+ // Search by tag
167
+ const result2 = await callPathway('sys_tool_file_collection', {
168
+ contextId,
169
+ query: 'photo',
170
+ userMessage: 'Search for photo'
171
+ });
172
+
173
+ const parsed2 = JSON.parse(result2);
174
+ t.is(parsed2.success, true);
175
+ t.is(parsed2.count, 1);
176
+ t.is(parsed2.files[0].filename, 'image.jpg');
177
+
178
+ // Search by notes
179
+ const result3 = await callPathway('sys_tool_file_collection', {
180
+ contextId,
181
+ query: 'office',
182
+ userMessage: 'Search for office'
183
+ });
184
+
185
+ const parsed3 = JSON.parse(result3);
186
+ t.is(parsed3.success, true);
187
+ t.is(parsed3.count, 1);
188
+ t.is(parsed3.files[0].filename, 'image.jpg');
189
+ } finally {
190
+ await cleanup(contextId);
191
+ }
192
+ });
193
+
194
+ test('File collection: Remove single file', async t => {
195
+ const contextId = createTestContext();
196
+
197
+ try {
198
+ // Add files
199
+ const addResult1 = await callPathway('sys_tool_file_collection', {
200
+ contextId,
201
+ url: 'https://example.com/file1.jpg',
202
+ filename: 'file1.jpg',
203
+ userMessage: 'Add file 1'
204
+ });
205
+ const file1Id = JSON.parse(addResult1).fileId;
206
+
207
+ await callPathway('sys_tool_file_collection', {
208
+ contextId,
209
+ url: 'https://example.com/file2.pdf',
210
+ filename: 'file2.pdf',
211
+ userMessage: 'Add file 2'
212
+ });
213
+
214
+ // Remove file1
215
+ const result = await callPathway('sys_tool_file_collection', {
216
+ contextId,
217
+ fileId: file1Id,
218
+ userMessage: 'Remove file 1'
219
+ });
220
+
221
+ const parsed = JSON.parse(result);
222
+ t.is(parsed.success, true);
223
+ t.is(parsed.removedCount, 1);
224
+ t.is(parsed.remainingFiles, 1);
225
+ t.is(parsed.removedFiles.length, 1);
226
+ t.is(parsed.removedFiles[0].filename, 'file1.jpg');
227
+ // Note: deletedFromCloud may be 0 if file has no hash or deletion fails (which is OK)
228
+ t.true(typeof parsed.deletedFromCloud === 'number');
229
+
230
+ // Verify it was removed
231
+ const listResult = await callPathway('sys_tool_file_collection', {
232
+ contextId,
233
+ userMessage: 'List files'
234
+ });
235
+ const listParsed = JSON.parse(listResult);
236
+ t.is(listParsed.totalFiles, 1);
237
+ t.false(listParsed.files.some(f => f.filename === 'file1.jpg'));
238
+ } finally {
239
+ await cleanup(contextId);
240
+ }
241
+ });
242
+
243
+ test('File collection: Remove all files', async t => {
244
+ const contextId = createTestContext();
245
+
246
+ try {
247
+ // Add files
248
+ await callPathway('sys_tool_file_collection', {
249
+ contextId,
250
+ url: 'https://example.com/file1.jpg',
251
+ filename: 'file1.jpg',
252
+ userMessage: 'Add file 1'
253
+ });
254
+
255
+ await callPathway('sys_tool_file_collection', {
256
+ contextId,
257
+ url: 'https://example.com/file2.pdf',
258
+ filename: 'file2.pdf',
259
+ userMessage: 'Add file 2'
260
+ });
261
+
262
+ // Remove all
263
+ const result = await callPathway('sys_tool_file_collection', {
264
+ contextId,
265
+ fileId: '*',
266
+ userMessage: 'Remove all files'
267
+ });
268
+
269
+ const parsed = JSON.parse(result);
270
+ t.is(parsed.success, true);
271
+ t.is(parsed.removedCount, 2);
272
+ t.is(parsed.remainingFiles, 0);
273
+ t.is(parsed.removedFiles.length, 2);
274
+ t.true(parsed.message.includes('All 2 file(s)'));
275
+ // Note: deletedFromCloud may be 0 if files have no hash or deletion fails (which is OK)
276
+ t.true(typeof parsed.deletedFromCloud === 'number');
277
+
278
+ // Verify collection is empty
279
+ const listResult = await callPathway('sys_tool_file_collection', {
280
+ contextId,
281
+ userMessage: 'List files'
282
+ });
283
+ const listParsed = JSON.parse(listResult);
284
+ t.is(listParsed.totalFiles, 0);
285
+ } finally {
286
+ await cleanup(contextId);
287
+ }
288
+ });
289
+
290
+ test('File collection: Error handling - missing contextId', async t => {
291
+ const result = await callPathway('sys_tool_file_collection', {
292
+ url: 'https://example.com/test.jpg',
293
+ filename: 'test.jpg',
294
+ userMessage: 'Test'
295
+ });
296
+
297
+ const parsed = JSON.parse(result);
298
+ t.is(parsed.success, false);
299
+ t.true(parsed.error.includes('contextId is required'));
300
+ });
301
+
302
+ test('File collection: Error handling - remove non-existent file', async t => {
303
+ const contextId = createTestContext();
304
+
305
+ try {
306
+ const result = await callPathway('sys_tool_file_collection', {
307
+ contextId,
308
+ fileId: 'non-existent-id',
309
+ userMessage: 'Remove file'
310
+ });
311
+
312
+ const parsed = JSON.parse(result);
313
+ t.is(parsed.success, false);
314
+ t.true(parsed.error.includes('not found in collection'));
315
+ } finally {
316
+ await cleanup(contextId);
317
+ }
318
+ });
319
+
320
+ test('File collection: List with filters and sorting', async t => {
321
+ const contextId = createTestContext();
322
+
323
+ try {
324
+ // Add files with different tags and dates
325
+ await callPathway('sys_tool_file_collection', {
326
+ contextId,
327
+ url: 'https://example.com/file1.jpg',
328
+ filename: 'a_file.jpg',
329
+ tags: ['photo'],
330
+ userMessage: 'Add file 1'
331
+ });
332
+
333
+ // Wait a bit to ensure different timestamps
334
+ await new Promise(resolve => setTimeout(resolve, 10));
335
+
336
+ await callPathway('sys_tool_file_collection', {
337
+ contextId,
338
+ url: 'https://example.com/file2.pdf',
339
+ filename: 'z_file.pdf',
340
+ tags: ['document'],
341
+ userMessage: 'Add file 2'
342
+ });
343
+
344
+ // List sorted by filename
345
+ const result1 = await callPathway('sys_tool_file_collection', {
346
+ contextId,
347
+ sortBy: 'filename',
348
+ userMessage: 'List sorted by filename'
349
+ });
350
+
351
+ const parsed1 = JSON.parse(result1);
352
+ t.is(parsed1.files[0].filename, 'a_file.jpg');
353
+ t.is(parsed1.files[1].filename, 'z_file.pdf');
354
+
355
+ // List filtered by tag
356
+ const result2 = await callPathway('sys_tool_file_collection', {
357
+ contextId,
358
+ tags: ['photo'],
359
+ userMessage: 'List photos'
360
+ });
361
+
362
+ const parsed2 = JSON.parse(result2);
363
+ t.is(parsed2.count, 1);
364
+ t.is(parsed2.files[0].filename, 'a_file.jpg');
365
+ } finally {
366
+ await cleanup(contextId);
367
+ }
368
+ });
369
+
370
+ test('Memory system: memoryFiles excluded from memoryAll', async t => {
371
+ const contextId = createTestContext();
372
+
373
+ try {
374
+ // Save a file collection
375
+ await callPathway('sys_save_memory', {
376
+ contextId,
377
+ section: 'memoryFiles',
378
+ aiMemory: JSON.stringify([{
379
+ id: 'test-1',
380
+ url: 'https://example.com/test.jpg',
381
+ filename: 'test.jpg'
382
+ }])
383
+ });
384
+
385
+ // Save other memory
386
+ await callPathway('sys_save_memory', {
387
+ contextId,
388
+ section: 'memorySelf',
389
+ aiMemory: 'Test memory content'
390
+ });
391
+
392
+ // Read all memory - should not include memoryFiles
393
+ const allMemory = await callPathway('sys_read_memory', {
394
+ contextId,
395
+ section: 'memoryAll'
396
+ });
397
+
398
+ const parsed = JSON.parse(allMemory);
399
+ t.truthy(parsed.memorySelf);
400
+ t.falsy(parsed.memoryFiles);
401
+
402
+ // But should be accessible explicitly
403
+ const files = await callPathway('sys_read_memory', {
404
+ contextId,
405
+ section: 'memoryFiles'
406
+ });
407
+
408
+ const filesParsed = JSON.parse(files);
409
+ t.is(filesParsed.length, 1);
410
+ t.is(filesParsed[0].filename, 'test.jpg');
411
+ } finally {
412
+ await cleanup(contextId);
413
+ }
414
+ });
415
+
416
+ test('Memory system: memoryFiles not cleared by memoryAll clear', async t => {
417
+ const contextId = createTestContext();
418
+
419
+ try {
420
+ // Save file collection
421
+ await callPathway('sys_save_memory', {
422
+ contextId,
423
+ section: 'memoryFiles',
424
+ aiMemory: JSON.stringify([{
425
+ id: 'test-1',
426
+ url: 'https://example.com/test.jpg',
427
+ filename: 'test.jpg'
428
+ }])
429
+ });
430
+
431
+ // Clear all memory
432
+ await callPathway('sys_save_memory', {
433
+ contextId,
434
+ section: 'memoryAll',
435
+ aiMemory: ''
436
+ });
437
+
438
+ // Verify files are still there
439
+ const files = await callPathway('sys_read_memory', {
440
+ contextId,
441
+ section: 'memoryFiles'
442
+ });
443
+
444
+ const filesParsed = JSON.parse(files);
445
+ t.is(filesParsed.length, 1);
446
+ t.is(filesParsed[0].filename, 'test.jpg');
447
+ } finally {
448
+ await cleanup(contextId);
449
+ }
450
+ });
451
+
452
+ test('Memory system: memoryFiles ignored in memoryAll save', async t => {
453
+ const contextId = createTestContext();
454
+
455
+ try {
456
+ // Save file collection first
457
+ await callPathway('sys_save_memory', {
458
+ contextId,
459
+ section: 'memoryFiles',
460
+ aiMemory: JSON.stringify([{
461
+ id: 'original',
462
+ cloudUrl: 'https://example.com/original.jpg',
463
+ filename: 'original.jpg'
464
+ }])
465
+ });
466
+
467
+ // Try to save all memory with memoryFiles included
468
+ await callPathway('sys_save_memory', {
469
+ contextId,
470
+ section: 'memoryAll',
471
+ aiMemory: JSON.stringify({
472
+ memorySelf: 'Test content',
473
+ memoryFiles: JSON.stringify([{
474
+ id: 'new',
475
+ url: 'https://example.com/new.jpg',
476
+ filename: 'new.jpg'
477
+ }])
478
+ })
479
+ });
480
+
481
+ // Verify original files are still there (not overwritten)
482
+ const files = await callPathway('sys_read_memory', {
483
+ contextId,
484
+ section: 'memoryFiles'
485
+ });
486
+
487
+ const filesParsed = JSON.parse(files);
488
+ t.is(filesParsed.length, 1);
489
+ t.is(filesParsed[0].filename, 'original.jpg');
490
+ } finally {
491
+ await cleanup(contextId);
492
+ }
493
+ });
494
+
495
+ // Test generateFileMessageContent function (integration tests)
496
+ test('generateFileMessageContent should find file by ID', async t => {
497
+ const contextId = createTestContext();
498
+
499
+ try {
500
+ // Add a file to collection
501
+ await callPathway('sys_tool_file_collection', {
502
+ contextId,
503
+ url: 'https://example.com/test.pdf',
504
+ gcs: 'gs://bucket/test.pdf',
505
+ filename: 'test.pdf',
506
+ userMessage: 'Add test file'
507
+ });
508
+
509
+ // Get the file ID from the collection
510
+ const saved = await callPathway('sys_read_memory', {
511
+ contextId,
512
+ section: 'memoryFiles'
513
+ });
514
+ const collection = extractFilesFromStored(saved);
515
+ const fileId = collection[0].id;
516
+
517
+ // Normalize by ID
518
+ const result = await generateFileMessageContent(fileId, contextId);
519
+
520
+ t.truthy(result);
521
+ t.is(result.type, 'file');
522
+ t.is(result.url, 'https://example.com/test.pdf');
523
+ t.is(result.gcs, 'gs://bucket/test.pdf');
524
+ t.is(result.originalFilename, 'test.pdf');
525
+ } finally {
526
+ await cleanup(contextId);
527
+ }
528
+ });
529
+
530
+ test('generateFileMessageContent should find file by URL', async t => {
531
+ const contextId = createTestContext();
532
+
533
+ try {
534
+ // Add a file to collection
535
+ await callPathway('sys_tool_file_collection', {
536
+ contextId,
537
+ url: 'https://example.com/test.pdf',
538
+ gcs: 'gs://bucket/test.pdf',
539
+ filename: 'test.pdf',
540
+ userMessage: 'Add test file'
541
+ });
542
+
543
+ // Normalize by URL
544
+ const result = await generateFileMessageContent('https://example.com/test.pdf', contextId);
545
+
546
+ t.truthy(result);
547
+ t.is(result.url, 'https://example.com/test.pdf');
548
+ t.is(result.gcs, 'gs://bucket/test.pdf');
549
+ } finally {
550
+ await cleanup(contextId);
551
+ }
552
+ });
553
+
554
+ test('generateFileMessageContent should find file by fuzzy filename match', async t => {
555
+ const contextId = createTestContext();
556
+
557
+ try {
558
+ // Add files to collection
559
+ await callPathway('sys_tool_file_collection', {
560
+ contextId,
561
+ url: 'https://example.com/document.pdf',
562
+ filename: 'document.pdf',
563
+ userMessage: 'Add document'
564
+ });
565
+
566
+ await callPathway('sys_tool_file_collection', {
567
+ contextId,
568
+ url: 'https://example.com/image.jpg',
569
+ filename: 'image.jpg',
570
+ userMessage: 'Add image'
571
+ });
572
+
573
+ // Normalize by partial filename
574
+ const result1 = await generateFileMessageContent('document', contextId);
575
+ t.truthy(result1);
576
+ t.is(result1.originalFilename, 'document.pdf');
577
+
578
+ // Normalize by full filename
579
+ const result2 = await generateFileMessageContent('image.jpg', contextId);
580
+ t.truthy(result2);
581
+ t.is(result2.originalFilename, 'image.jpg');
582
+ } finally {
583
+ await cleanup(contextId);
584
+ }
585
+ });
586
+
587
+ test('generateFileMessageContent should detect image type', async t => {
588
+ const contextId = createTestContext();
589
+
590
+ try {
591
+ // Add an image file
592
+ await callPathway('sys_tool_file_collection', {
593
+ contextId,
594
+ url: 'https://example.com/image.jpg',
595
+ filename: 'image.jpg',
596
+ userMessage: 'Add image'
597
+ });
598
+
599
+ const saved = await callPathway('sys_read_memory', {
600
+ contextId,
601
+ section: 'memoryFiles'
602
+ });
603
+ const collection = extractFilesFromStored(saved);
604
+ const fileId = collection[0].id;
605
+
606
+ const result = await generateFileMessageContent(fileId, contextId);
607
+
608
+ t.truthy(result);
609
+ t.is(result.type, 'image_url');
610
+ t.truthy(result.image_url);
611
+ t.is(result.image_url.url, 'https://example.com/image.jpg');
612
+ } finally {
613
+ await cleanup(contextId);
614
+ }
615
+ });
616
+
617
+ // Tests for resolveFileParameter
618
+ test('resolveFileParameter: Resolve by file ID', async t => {
619
+ const contextId = createTestContext();
620
+
621
+ try {
622
+ // Add a file to collection
623
+ const addResult = await callPathway('sys_tool_file_collection', {
624
+ contextId,
625
+ url: 'https://example.com/test-doc.pdf',
626
+ gcs: 'gs://bucket/test-doc.pdf',
627
+ filename: 'test-doc.pdf',
628
+ userMessage: 'Adding test file'
629
+ });
630
+
631
+ const addParsed = JSON.parse(addResult);
632
+ const fileId = addParsed.fileId;
633
+
634
+ // Resolve by file ID
635
+ const resolved = await resolveFileParameter(fileId, contextId);
636
+ t.is(resolved, 'https://example.com/test-doc.pdf');
637
+ } finally {
638
+ await cleanup(contextId);
639
+ }
640
+ });
641
+
642
+ test('resolveFileParameter: Resolve by filename', async t => {
643
+ const contextId = createTestContext();
644
+
645
+ try {
646
+ // Add a file to collection
647
+ await callPathway('sys_tool_file_collection', {
648
+ contextId,
649
+ url: 'https://example.com/my-file.txt',
650
+ gcs: 'gs://bucket/my-file.txt',
651
+ filename: 'my-file.txt',
652
+ userMessage: 'Adding test file'
653
+ });
654
+
655
+ // Resolve by filename
656
+ const resolved = await resolveFileParameter('my-file.txt', contextId);
657
+ t.is(resolved, 'https://example.com/my-file.txt');
658
+ } finally {
659
+ await cleanup(contextId);
660
+ }
661
+ });
662
+
663
+ test('resolveFileParameter: Resolve by hash', async t => {
664
+ const contextId = createTestContext();
665
+ const testHash = 'abc123def456';
666
+
667
+ try {
668
+ // Add a file to collection with hash
669
+ await callPathway('sys_tool_file_collection', {
670
+ contextId,
671
+ url: 'https://example.com/hashed-file.jpg',
672
+ gcs: 'gs://bucket/hashed-file.jpg',
673
+ filename: 'hashed-file.jpg',
674
+ hash: testHash,
675
+ userMessage: 'Adding test file'
676
+ });
677
+
678
+ // Resolve by hash
679
+ const resolved = await resolveFileParameter(testHash, contextId);
680
+ t.is(resolved, 'https://example.com/hashed-file.jpg');
681
+ } finally {
682
+ await cleanup(contextId);
683
+ }
684
+ });
685
+
686
+ test('resolveFileParameter: Resolve by Azure URL', async t => {
687
+ const contextId = createTestContext();
688
+ const testUrl = 'https://example.com/existing-file.pdf';
689
+
690
+ try {
691
+ // Add a file to collection
692
+ await callPathway('sys_tool_file_collection', {
693
+ contextId,
694
+ url: testUrl,
695
+ gcs: 'gs://bucket/existing-file.pdf',
696
+ filename: 'existing-file.pdf',
697
+ userMessage: 'Adding test file'
698
+ });
699
+
700
+ // Resolve by Azure URL
701
+ const resolved = await resolveFileParameter(testUrl, contextId);
702
+ t.is(resolved, testUrl);
703
+ } finally {
704
+ await cleanup(contextId);
705
+ }
706
+ });
707
+
708
+ test('resolveFileParameter: Resolve by GCS URL', async t => {
709
+ const contextId = createTestContext();
710
+ const testGcsUrl = 'gs://bucket/gcs-file.pdf';
711
+
712
+ try {
713
+ // Add a file to collection
714
+ await callPathway('sys_tool_file_collection', {
715
+ contextId,
716
+ url: 'https://example.com/gcs-file.pdf',
717
+ gcs: testGcsUrl,
718
+ filename: 'gcs-file.pdf',
719
+ userMessage: 'Adding test file'
720
+ });
721
+
722
+ // Resolve by GCS URL
723
+ const resolved = await resolveFileParameter(testGcsUrl, contextId);
724
+ t.is(resolved, 'https://example.com/gcs-file.pdf');
725
+ } finally {
726
+ await cleanup(contextId);
727
+ }
728
+ });
729
+
730
+ test('resolveFileParameter: Prefer GCS URL when preferGcs is true', async t => {
731
+ const contextId = createTestContext();
732
+ const testGcsUrl = 'gs://bucket/prefer-gcs-file.pdf';
733
+ const testAzureUrl = 'https://example.com/prefer-gcs-file.pdf';
734
+
735
+ try {
736
+ // Add a file to collection with both URLs
737
+ await callPathway('sys_tool_file_collection', {
738
+ contextId,
739
+ url: testAzureUrl,
740
+ gcs: testGcsUrl,
741
+ filename: 'prefer-gcs-file.pdf',
742
+ userMessage: 'Adding test file'
743
+ });
744
+
745
+ // Resolve by filename without preferGcs (should return Azure URL)
746
+ const resolvedDefault = await resolveFileParameter('prefer-gcs-file.pdf', contextId);
747
+ t.is(resolvedDefault, testAzureUrl);
748
+
749
+ // Resolve by filename with preferGcs (should return GCS URL)
750
+ const resolvedGcs = await resolveFileParameter('prefer-gcs-file.pdf', contextId, null, { preferGcs: true });
751
+ t.is(resolvedGcs, testGcsUrl);
752
+ } finally {
753
+ await cleanup(contextId);
754
+ }
755
+ });
756
+
757
+ test('resolveFileParameter: Return null when file not found', async t => {
758
+ const contextId = createTestContext();
759
+
760
+ try {
761
+ // Try to resolve a non-existent file
762
+ const resolved = await resolveFileParameter('non-existent-file.txt', contextId);
763
+ t.is(resolved, null);
764
+ } finally {
765
+ await cleanup(contextId);
766
+ }
767
+ });
768
+
769
+ test('resolveFileParameter: Return null when contextId is missing', async t => {
770
+ // Try to resolve without contextId
771
+ const resolved = await resolveFileParameter('some-file.txt', null);
772
+ t.is(resolved, null);
773
+ });
774
+
775
+ test('resolveFileParameter: Return null when fileParam is empty', async t => {
776
+ const contextId = createTestContext();
777
+
778
+ try {
779
+ // Try with empty string
780
+ const resolved1 = await resolveFileParameter('', contextId);
781
+ t.is(resolved1, null);
782
+
783
+ // Try with null
784
+ const resolved2 = await resolveFileParameter(null, contextId);
785
+ t.is(resolved2, null);
786
+
787
+ // Try with undefined
788
+ const resolved3 = await resolveFileParameter(undefined, contextId);
789
+ t.is(resolved3, null);
790
+ } finally {
791
+ await cleanup(contextId);
792
+ }
793
+ });
794
+
795
+ test('resolveFileParameter: Fuzzy filename matching', async t => {
796
+ const contextId = createTestContext();
797
+
798
+ try {
799
+ // Add a file with a specific filename
800
+ await callPathway('sys_tool_file_collection', {
801
+ contextId,
802
+ url: 'https://example.com/my-document.pdf',
803
+ gcs: 'gs://bucket/my-document.pdf',
804
+ filename: 'my-document.pdf',
805
+ userMessage: 'Adding test file'
806
+ });
807
+
808
+ // Resolve by partial filename (fuzzy match)
809
+ const resolved = await resolveFileParameter('document.pdf', contextId);
810
+ t.is(resolved, 'https://example.com/my-document.pdf');
811
+ } finally {
812
+ await cleanup(contextId);
813
+ }
814
+ });
815
+
816
+ test('resolveFileParameter: Fallback to Azure URL when GCS not available and preferGcs is true', async t => {
817
+ const contextId = createTestContext();
818
+ const testAzureUrl = 'https://example.com/no-gcs-file.pdf';
819
+
820
+ try {
821
+ // Add a file without GCS URL
822
+ await callPathway('sys_tool_file_collection', {
823
+ contextId,
824
+ url: testAzureUrl,
825
+ filename: 'no-gcs-file.pdf',
826
+ userMessage: 'Adding test file'
827
+ });
828
+
829
+ // Resolve with preferGcs=true, but no GCS available (should fallback to Azure URL)
830
+ const resolved = await resolveFileParameter('no-gcs-file.pdf', contextId, null, { preferGcs: true });
831
+ t.is(resolved, testAzureUrl);
832
+ } finally {
833
+ await cleanup(contextId);
834
+ }
835
+ });
836
+
837
+ test('resolveFileParameter: Handle contextKey for encrypted collections', async t => {
838
+ const contextId = createTestContext();
839
+ const contextKey = 'test-encryption-key';
840
+
841
+ try {
842
+ // Add a file to collection with contextKey
843
+ await callPathway('sys_tool_file_collection', {
844
+ contextId,
845
+ contextKey,
846
+ url: 'https://example.com/encrypted-file.pdf',
847
+ gcs: 'gs://bucket/encrypted-file.pdf',
848
+ filename: 'encrypted-file.pdf',
849
+ userMessage: 'Adding test file'
850
+ });
851
+
852
+ // Resolve with contextKey
853
+ const resolved = await resolveFileParameter('encrypted-file.pdf', contextId, contextKey);
854
+ t.is(resolved, 'https://example.com/encrypted-file.pdf');
855
+ } finally {
856
+ await cleanup(contextId);
857
+ }
858
+ });