@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.
- package/helper-apps/cortex-file-handler/package-lock.json +2 -2
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/src/index.js +27 -4
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +74 -10
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +23 -2
- package/helper-apps/cortex-file-handler/src/start.js +2 -0
- package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +287 -0
- package/helper-apps/cortex-file-handler/tests/start.test.js +1 -1
- package/lib/entityConstants.js +1 -1
- package/lib/fileUtils.js +1481 -0
- package/lib/pathwayTools.js +7 -1
- package/lib/util.js +2 -313
- package/package.json +4 -3
- package/pathways/image_qwen.js +1 -1
- package/pathways/system/entity/memory/sys_read_memory.js +17 -3
- package/pathways/system/entity/memory/sys_save_memory.js +22 -6
- package/pathways/system/entity/sys_entity_agent.js +21 -4
- package/pathways/system/entity/tools/sys_tool_analyzefile.js +171 -0
- package/pathways/system/entity/tools/sys_tool_codingagent.js +38 -4
- package/pathways/system/entity/tools/sys_tool_editfile.js +403 -0
- package/pathways/system/entity/tools/sys_tool_file_collection.js +433 -0
- package/pathways/system/entity/tools/sys_tool_image.js +172 -10
- package/pathways/system/entity/tools/sys_tool_image_gemini.js +123 -10
- package/pathways/system/entity/tools/sys_tool_readfile.js +217 -124
- package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
- package/pathways/system/entity/tools/sys_tool_writefile.js +211 -0
- package/pathways/system/workspaces/run_workspace_prompt.js +4 -3
- package/pathways/transcribe_gemini.js +2 -1
- package/server/executeWorkspace.js +1 -1
- package/server/plugins/neuralSpacePlugin.js +2 -6
- package/server/plugins/openAiWhisperPlugin.js +2 -1
- package/server/plugins/replicateApiPlugin.js +4 -14
- package/server/typeDef.js +10 -1
- package/tests/integration/features/tools/fileCollection.test.js +858 -0
- package/tests/integration/features/tools/fileOperations.test.js +851 -0
- package/tests/integration/features/tools/writefile.test.js +350 -0
- package/tests/unit/core/fileCollection.test.js +259 -0
- 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
|
+
|