@aj-archipelago/cortex 1.4.29 → 1.4.31
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/RELEASE_NOTES_20251231_103631.md +15 -0
- package/RELEASE_NOTES_20251231_110946.md +5 -0
- package/lib/fileUtils.js +195 -188
- package/package.json +1 -1
- package/pathways/system/entity/files/sys_read_file_collection.js +3 -3
- package/pathways/system/entity/tools/sys_tool_analyzefile.js +48 -19
- package/pathways/system/entity/tools/sys_tool_editfile.js +4 -4
- package/pathways/system/entity/tools/sys_tool_file_collection.js +25 -16
- package/pathways/system/entity/tools/sys_tool_view_image.js +3 -3
- package/tests/integration/features/tools/fileCollection.test.js +697 -64
- package/tests/integration/features/tools/writefile.test.js +4 -4
- package/tests/integration/graphql/async/stream/file_operations_agent.test.js +839 -0
- package/tests/unit/core/fileCollection.test.js +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import test from 'ava';
|
|
5
5
|
import serverFactory from '../../../../index.js';
|
|
6
6
|
import { callPathway } from '../../../../lib/pathwayTools.js';
|
|
7
|
-
import { generateFileMessageContent, resolveFileParameter, loadFileCollection, syncAndStripFilesFromChatHistory
|
|
7
|
+
import { generateFileMessageContent, resolveFileParameter, loadFileCollection, syncAndStripFilesFromChatHistory } from '../../../../lib/fileUtils.js';
|
|
8
8
|
|
|
9
9
|
// Helper to create agentContext from contextId/contextKey
|
|
10
10
|
const createAgentContext = (contextId, contextKey = null) => [{ contextId, contextKey, default: true }];
|
|
@@ -65,7 +65,7 @@ test('File collection: Add file to collection', async t => {
|
|
|
65
65
|
t.true(parsed.message.includes('test.jpg'));
|
|
66
66
|
|
|
67
67
|
// Verify it was saved to Redis hash map
|
|
68
|
-
const collection = await loadFileCollection(contextId,
|
|
68
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
69
69
|
t.is(collection.length, 1);
|
|
70
70
|
t.is(collection[0].displayFilename, 'test.jpg');
|
|
71
71
|
t.is(collection[0].url, 'https://example.com/test.jpg');
|
|
@@ -489,7 +489,7 @@ test('generateFileMessageContent should find file by ID', async t => {
|
|
|
489
489
|
});
|
|
490
490
|
|
|
491
491
|
// Get the file ID from the collection
|
|
492
|
-
const collection = await loadFileCollection(contextId,
|
|
492
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
493
493
|
const fileId = collection[0].id;
|
|
494
494
|
|
|
495
495
|
// Normalize by ID
|
|
@@ -580,7 +580,7 @@ test('generateFileMessageContent should detect image type', async t => {
|
|
|
580
580
|
userMessage: 'Add image'
|
|
581
581
|
});
|
|
582
582
|
|
|
583
|
-
const collection = await loadFileCollection(contextId,
|
|
583
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
584
584
|
const fileId = collection[0].id;
|
|
585
585
|
|
|
586
586
|
const result = await generateFileMessageContent(fileId, createAgentContext(contextId));
|
|
@@ -880,7 +880,7 @@ test('File collection: Update file metadata', async t => {
|
|
|
880
880
|
const fileId = addParsed.fileId;
|
|
881
881
|
|
|
882
882
|
// Get the hash from the collection
|
|
883
|
-
const collection = await loadFileCollection(contextId,
|
|
883
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
884
884
|
const file = collection.find(f => f.id === fileId);
|
|
885
885
|
t.truthy(file);
|
|
886
886
|
const hash = file.hash;
|
|
@@ -897,7 +897,7 @@ test('File collection: Update file metadata', async t => {
|
|
|
897
897
|
t.is(success, true);
|
|
898
898
|
|
|
899
899
|
// Verify metadata was updated
|
|
900
|
-
const updatedCollection = await loadFileCollection(contextId,
|
|
900
|
+
const updatedCollection = await loadFileCollection(contextId, { useCache: false });
|
|
901
901
|
const updatedFile = updatedCollection.find(f => f.id === fileId);
|
|
902
902
|
t.truthy(updatedFile);
|
|
903
903
|
t.is(updatedFile.displayFilename, 'renamed.pdf');
|
|
@@ -930,7 +930,7 @@ test('updateFileMetadata should allow updating inCollection', async (t) => {
|
|
|
930
930
|
const fileId = addParsed.fileId;
|
|
931
931
|
|
|
932
932
|
// Get the hash from the collection
|
|
933
|
-
const collection = await loadFileCollection(contextId,
|
|
933
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
934
934
|
const file = collection.find(f => f.id === fileId);
|
|
935
935
|
t.truthy(file);
|
|
936
936
|
const hash = file.hash;
|
|
@@ -946,13 +946,16 @@ test('updateFileMetadata should allow updating inCollection', async (t) => {
|
|
|
946
946
|
t.is(success1, true);
|
|
947
947
|
|
|
948
948
|
// Verify file is now only in chat-123 (not global)
|
|
949
|
-
|
|
949
|
+
// With new unified loadFileCollection, loading without chatIds returns ALL files
|
|
950
|
+
// So we need to check that the file's inCollection is ['chat-123'] not ['*']
|
|
951
|
+
const collection1 = await loadFileCollection(contextId, { useCache: false });
|
|
950
952
|
const file1 = collection1.find(f => f.id === fileId);
|
|
951
|
-
//
|
|
952
|
-
t.
|
|
953
|
+
// File should still appear (all files are returned), but inCollection should be ['chat-123']
|
|
954
|
+
t.truthy(file1);
|
|
955
|
+
t.deepEqual(file1.inCollection, ['chat-123'], 'File should be in chat-123 collection, not global');
|
|
953
956
|
|
|
954
957
|
// Should appear when filtering by chat-123
|
|
955
|
-
const collection2 = await loadFileCollection(contextId,
|
|
958
|
+
const collection2 = await loadFileCollection(contextId, { chatIds: ['chat-123'], useCache: false });
|
|
956
959
|
const file2 = collection2.find(f => f.id === fileId);
|
|
957
960
|
t.truthy(file2);
|
|
958
961
|
|
|
@@ -963,25 +966,34 @@ test('updateFileMetadata should allow updating inCollection', async (t) => {
|
|
|
963
966
|
t.is(success2, true);
|
|
964
967
|
|
|
965
968
|
// Verify file is back in global collection
|
|
966
|
-
const collection3 = await loadFileCollection(contextId,
|
|
969
|
+
const collection3 = await loadFileCollection(contextId, { useCache: false });
|
|
967
970
|
const file3 = collection3.find(f => f.id === fileId);
|
|
968
971
|
t.truthy(file3);
|
|
972
|
+
// Verify it's global
|
|
973
|
+
t.true(
|
|
974
|
+
file3.inCollection === true ||
|
|
975
|
+
(Array.isArray(file3.inCollection) && file3.inCollection.includes('*')),
|
|
976
|
+
'File should be global'
|
|
977
|
+
);
|
|
969
978
|
|
|
970
979
|
// Update inCollection to false (remove from collection)
|
|
980
|
+
// false is normalized to undefined, which means "not in collection"
|
|
971
981
|
const success3 = await updateFileMetadata(contextId, hash, {
|
|
972
982
|
inCollection: false
|
|
973
983
|
});
|
|
974
984
|
t.is(success3, true);
|
|
975
985
|
|
|
976
|
-
// Verify file is
|
|
977
|
-
|
|
986
|
+
// Verify file still exists but inCollection is undefined (not in collection)
|
|
987
|
+
// When loading without chatIds, undefined files are included (Labeeb uploads, etc.)
|
|
988
|
+
const collection4 = await loadFileCollection(contextId, { useCache: false });
|
|
978
989
|
const file4 = collection4.find(f => f.id === fileId);
|
|
979
|
-
t.
|
|
990
|
+
t.truthy(file4, 'File should still exist (false → undefined, treated same as Labeeb uploads)');
|
|
991
|
+
t.falsy(file4.inCollection, 'File should have undefined inCollection (not in collection)');
|
|
980
992
|
|
|
981
|
-
//
|
|
982
|
-
const collection5 = await loadFileCollection(contextId,
|
|
993
|
+
// Not in chat-specific collection (undefined files don't match any chatId)
|
|
994
|
+
const collection5 = await loadFileCollection(contextId, { chatIds: ['chat-123'], useCache: false });
|
|
983
995
|
const file5 = collection5.find(f => f.id === fileId);
|
|
984
|
-
t.falsy(file5);
|
|
996
|
+
t.falsy(file5, 'File should not appear when filtering by chatId (undefined = not in collection)');
|
|
985
997
|
} finally {
|
|
986
998
|
await cleanup(contextId);
|
|
987
999
|
}
|
|
@@ -1004,7 +1016,7 @@ test('File collection: Permanent files not deleted on remove', async t => {
|
|
|
1004
1016
|
const fileId = addParsed.fileId;
|
|
1005
1017
|
|
|
1006
1018
|
// Mark as permanent
|
|
1007
|
-
const collection = await loadFileCollection(contextId,
|
|
1019
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1008
1020
|
const file = collection.find(f => f.id === fileId);
|
|
1009
1021
|
const { updateFileMetadata } = await import('../../../../lib/fileUtils.js');
|
|
1010
1022
|
await updateFileMetadata(contextId, file.hash, { permanent: true });
|
|
@@ -1074,7 +1086,7 @@ test('File collection: syncAndStripFilesFromChatHistory only strips collection f
|
|
|
1074
1086
|
];
|
|
1075
1087
|
|
|
1076
1088
|
// Process chat history
|
|
1077
|
-
const { chatHistory: processed
|
|
1089
|
+
const { chatHistory: processed } = await syncAndStripFilesFromChatHistory(chatHistory, createAgentContext(contextId));
|
|
1078
1090
|
|
|
1079
1091
|
// Verify only collection file was stripped
|
|
1080
1092
|
const content = processed[0].content;
|
|
@@ -1090,7 +1102,7 @@ test('File collection: syncAndStripFilesFromChatHistory only strips collection f
|
|
|
1090
1102
|
t.is(content[1].url, 'https://example.com/external.pdf');
|
|
1091
1103
|
|
|
1092
1104
|
// Collection should still have only 1 file (no auto-syncing)
|
|
1093
|
-
const collection = await loadFileCollection(contextId,
|
|
1105
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1094
1106
|
t.is(collection.length, 1);
|
|
1095
1107
|
|
|
1096
1108
|
// Available files should list the collection file
|
|
@@ -1100,6 +1112,176 @@ test('File collection: syncAndStripFilesFromChatHistory only strips collection f
|
|
|
1100
1112
|
}
|
|
1101
1113
|
});
|
|
1102
1114
|
|
|
1115
|
+
test('File collection: syncAndStripFilesFromChatHistory finds and syncs files without inCollection (Labeeb uploads)', async t => {
|
|
1116
|
+
const contextId = createTestContext();
|
|
1117
|
+
const chatId = `test-chat-${Date.now()}`;
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
const { syncAndStripFilesFromChatHistory, writeFileDataToRedis, getRedisClient, loadFileCollection } = await import('../../../../lib/fileUtils.js');
|
|
1121
|
+
|
|
1122
|
+
// Create 3 files directly in Redis without inCollection set (simulating Labeeb uploads)
|
|
1123
|
+
const redisClient = await getRedisClient();
|
|
1124
|
+
t.truthy(redisClient, 'Redis client should be available');
|
|
1125
|
+
|
|
1126
|
+
const contextMapKey = `FileStoreMap:ctx:${contextId}`;
|
|
1127
|
+
|
|
1128
|
+
const file1 = {
|
|
1129
|
+
url: 'https://example.com/labeeb-file-1.txt',
|
|
1130
|
+
gcs: 'gs://bucket/labeeb-file-1.txt',
|
|
1131
|
+
hash: 'hash-labeeb-1',
|
|
1132
|
+
filename: 'labeeb-file-1.txt',
|
|
1133
|
+
displayFilename: 'labeeb-file-1.txt',
|
|
1134
|
+
mimeType: 'text/plain',
|
|
1135
|
+
addedDate: new Date().toISOString(),
|
|
1136
|
+
lastAccessed: new Date().toISOString(),
|
|
1137
|
+
permanent: false,
|
|
1138
|
+
tags: [],
|
|
1139
|
+
notes: ''
|
|
1140
|
+
// Note: inCollection is NOT set
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
const file2 = {
|
|
1144
|
+
url: 'https://example.com/labeeb-file-2.txt',
|
|
1145
|
+
gcs: 'gs://bucket/labeeb-file-2.txt',
|
|
1146
|
+
hash: 'hash-labeeb-2',
|
|
1147
|
+
filename: 'labeeb-file-2.txt',
|
|
1148
|
+
displayFilename: 'labeeb-file-2.txt',
|
|
1149
|
+
mimeType: 'text/plain',
|
|
1150
|
+
addedDate: new Date().toISOString(),
|
|
1151
|
+
lastAccessed: new Date().toISOString(),
|
|
1152
|
+
permanent: false,
|
|
1153
|
+
tags: [],
|
|
1154
|
+
notes: ''
|
|
1155
|
+
// Note: inCollection is NOT set
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
const file3 = {
|
|
1159
|
+
url: 'https://example.com/labeeb-file-3.txt',
|
|
1160
|
+
gcs: 'gs://bucket/labeeb-file-3.txt',
|
|
1161
|
+
hash: 'hash-labeeb-3',
|
|
1162
|
+
filename: 'labeeb-file-3.txt',
|
|
1163
|
+
displayFilename: 'labeeb-file-3.txt',
|
|
1164
|
+
mimeType: 'text/plain',
|
|
1165
|
+
addedDate: new Date().toISOString(),
|
|
1166
|
+
lastAccessed: new Date().toISOString(),
|
|
1167
|
+
permanent: false,
|
|
1168
|
+
tags: [],
|
|
1169
|
+
notes: ''
|
|
1170
|
+
// Note: inCollection is NOT set
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
// Write files directly to Redis (without inCollection)
|
|
1174
|
+
await writeFileDataToRedis(redisClient, contextMapKey, file1.hash, file1, null);
|
|
1175
|
+
await writeFileDataToRedis(redisClient, contextMapKey, file2.hash, file2, null);
|
|
1176
|
+
await writeFileDataToRedis(redisClient, contextMapKey, file3.hash, file3, null);
|
|
1177
|
+
|
|
1178
|
+
// Verify files exist but don't have inCollection set
|
|
1179
|
+
const allFilesBefore = await loadFileCollection(contextId);
|
|
1180
|
+
const file1Before = allFilesBefore.find(f => f.hash === file1.hash);
|
|
1181
|
+
const file2Before = allFilesBefore.find(f => f.hash === file2.hash);
|
|
1182
|
+
const file3Before = allFilesBefore.find(f => f.hash === file3.hash);
|
|
1183
|
+
|
|
1184
|
+
t.truthy(file1Before, 'File 1 should exist in Redis');
|
|
1185
|
+
t.truthy(file2Before, 'File 2 should exist in Redis');
|
|
1186
|
+
t.truthy(file3Before, 'File 3 should exist in Redis');
|
|
1187
|
+
|
|
1188
|
+
t.falsy(file1Before.inCollection, 'File 1 should not have inCollection set');
|
|
1189
|
+
t.falsy(file2Before.inCollection, 'File 2 should not have inCollection set');
|
|
1190
|
+
t.falsy(file3Before.inCollection, 'File 3 should not have inCollection set');
|
|
1191
|
+
|
|
1192
|
+
// Create chat history with all 3 files
|
|
1193
|
+
const chatHistory = [
|
|
1194
|
+
{
|
|
1195
|
+
role: 'user',
|
|
1196
|
+
content: [
|
|
1197
|
+
{
|
|
1198
|
+
type: 'file',
|
|
1199
|
+
url: file1.url,
|
|
1200
|
+
gcs: file1.gcs,
|
|
1201
|
+
hash: file1.hash
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
type: 'file',
|
|
1205
|
+
url: file2.url,
|
|
1206
|
+
gcs: file2.gcs,
|
|
1207
|
+
hash: file2.hash
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
type: 'file',
|
|
1211
|
+
url: file3.url,
|
|
1212
|
+
gcs: file3.gcs,
|
|
1213
|
+
hash: file3.hash
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
type: 'text',
|
|
1217
|
+
text: 'Test message'
|
|
1218
|
+
}
|
|
1219
|
+
]
|
|
1220
|
+
}
|
|
1221
|
+
];
|
|
1222
|
+
|
|
1223
|
+
// Process chat history - should find all 3 files and sync them
|
|
1224
|
+
const { chatHistory: processed } = await syncAndStripFilesFromChatHistory(
|
|
1225
|
+
chatHistory,
|
|
1226
|
+
createAgentContext(contextId),
|
|
1227
|
+
chatId
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
// Verify all 3 files were stripped (found and synced)
|
|
1231
|
+
const content = processed[0].content;
|
|
1232
|
+
t.true(Array.isArray(content), 'Content should be an array');
|
|
1233
|
+
|
|
1234
|
+
// All 3 files should be stripped to placeholders
|
|
1235
|
+
const filePlaceholders = content.filter(item =>
|
|
1236
|
+
item.type === 'text' && item.text && item.text.includes('[File:') && item.text.includes('available via file tools')
|
|
1237
|
+
);
|
|
1238
|
+
t.is(filePlaceholders.length, 3, 'All 3 files should be stripped to placeholders');
|
|
1239
|
+
|
|
1240
|
+
// Text message should remain
|
|
1241
|
+
const textMessages = content.filter(item => item.type === 'text' && !item.text.includes('[File:'));
|
|
1242
|
+
t.is(textMessages.length, 1, 'Text message should remain');
|
|
1243
|
+
|
|
1244
|
+
// Wait a bit for async updates to complete
|
|
1245
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1246
|
+
|
|
1247
|
+
// Verify files now have inCollection set
|
|
1248
|
+
const allFilesAfter = await loadFileCollection(contextId);
|
|
1249
|
+
const file1After = allFilesAfter.find(f => f.hash === file1.hash);
|
|
1250
|
+
const file2After = allFilesAfter.find(f => f.hash === file2.hash);
|
|
1251
|
+
const file3After = allFilesAfter.find(f => f.hash === file3.hash);
|
|
1252
|
+
|
|
1253
|
+
t.truthy(file1After, 'File 1 should still exist after sync');
|
|
1254
|
+
t.truthy(file2After, 'File 2 should still exist after sync');
|
|
1255
|
+
t.truthy(file3After, 'File 3 should still exist after sync');
|
|
1256
|
+
|
|
1257
|
+
// All files should now have inCollection set
|
|
1258
|
+
t.truthy(file1After.inCollection, 'File 1 should have inCollection set after sync');
|
|
1259
|
+
t.truthy(file2After.inCollection, 'File 2 should have inCollection set after sync');
|
|
1260
|
+
t.truthy(file3After.inCollection, 'File 3 should have inCollection set after sync');
|
|
1261
|
+
|
|
1262
|
+
// Verify inCollection includes chatId or is global
|
|
1263
|
+
const hasChatId = (inCollection) => {
|
|
1264
|
+
if (inCollection === true) return true; // Global
|
|
1265
|
+
if (Array.isArray(inCollection)) {
|
|
1266
|
+
return inCollection.includes('*') || inCollection.includes(chatId);
|
|
1267
|
+
}
|
|
1268
|
+
return false;
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
t.true(hasChatId(file1After.inCollection), 'File 1 inCollection should include chatId or be global');
|
|
1272
|
+
t.true(hasChatId(file2After.inCollection), 'File 2 inCollection should include chatId or be global');
|
|
1273
|
+
t.true(hasChatId(file3After.inCollection), 'File 3 inCollection should include chatId or be global');
|
|
1274
|
+
|
|
1275
|
+
// Verify lastAccessed was updated
|
|
1276
|
+
t.truthy(file1After.lastAccessed, 'File 1 should have lastAccessed updated');
|
|
1277
|
+
t.truthy(file2After.lastAccessed, 'File 2 should have lastAccessed updated');
|
|
1278
|
+
t.truthy(file3After.lastAccessed, 'File 3 should have lastAccessed updated');
|
|
1279
|
+
|
|
1280
|
+
} finally {
|
|
1281
|
+
await cleanup(contextId);
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1103
1285
|
// ============================================
|
|
1104
1286
|
// UpdateFileMetadata Tool Tests
|
|
1105
1287
|
// ============================================
|
|
@@ -1135,7 +1317,7 @@ test('File collection: UpdateFileMetadata tool - Rename file', async t => {
|
|
|
1135
1317
|
t.true(updateParsed.message.includes('renamed to "new-name.pdf"'));
|
|
1136
1318
|
|
|
1137
1319
|
// Verify rename persisted
|
|
1138
|
-
const collection = await loadFileCollection(contextId,
|
|
1320
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1139
1321
|
const updatedFile = collection.find(f => f.id === originalFileId);
|
|
1140
1322
|
t.truthy(updatedFile);
|
|
1141
1323
|
t.is(updatedFile.displayFilename, 'new-name.pdf');
|
|
@@ -1174,7 +1356,7 @@ test('File collection: UpdateFileMetadata tool - Replace all tags', async t => {
|
|
|
1174
1356
|
t.is(updateParsed.success, true);
|
|
1175
1357
|
|
|
1176
1358
|
// Verify tags were replaced
|
|
1177
|
-
const collection = await loadFileCollection(contextId,
|
|
1359
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1178
1360
|
const file = collection.find(f => f.id === addParsed.fileId);
|
|
1179
1361
|
t.deepEqual(file.tags, ['new', 'replaced', 'tags']);
|
|
1180
1362
|
} finally {
|
|
@@ -1210,7 +1392,7 @@ test('File collection: UpdateFileMetadata tool - Add tags', async t => {
|
|
|
1210
1392
|
t.is(updateParsed.success, true);
|
|
1211
1393
|
|
|
1212
1394
|
// Verify tags were added (should contain both old and new)
|
|
1213
|
-
const collection = await loadFileCollection(contextId,
|
|
1395
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1214
1396
|
const file = collection.find(f => f.id === addParsed.fileId);
|
|
1215
1397
|
t.is(file.tags.length, 4);
|
|
1216
1398
|
t.true(file.tags.includes('existing'));
|
|
@@ -1250,7 +1432,7 @@ test('File collection: UpdateFileMetadata tool - Remove tags', async t => {
|
|
|
1250
1432
|
t.is(updateParsed.success, true);
|
|
1251
1433
|
|
|
1252
1434
|
// Verify tags were removed
|
|
1253
|
-
const collection = await loadFileCollection(contextId,
|
|
1435
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1254
1436
|
const file = collection.find(f => f.id === addParsed.fileId);
|
|
1255
1437
|
t.is(file.tags.length, 2);
|
|
1256
1438
|
t.true(file.tags.includes('keep'));
|
|
@@ -1291,7 +1473,7 @@ test('File collection: UpdateFileMetadata tool - Add and remove tags together',
|
|
|
1291
1473
|
t.is(updateParsed.success, true);
|
|
1292
1474
|
|
|
1293
1475
|
// Verify tags were updated correctly
|
|
1294
|
-
const collection = await loadFileCollection(contextId,
|
|
1476
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1295
1477
|
const file = collection.find(f => f.id === addParsed.fileId);
|
|
1296
1478
|
t.is(file.tags.length, 4);
|
|
1297
1479
|
t.true(file.tags.includes('old1'));
|
|
@@ -1332,7 +1514,7 @@ test('File collection: UpdateFileMetadata tool - Update notes', async t => {
|
|
|
1332
1514
|
t.is(updateParsed.success, true);
|
|
1333
1515
|
|
|
1334
1516
|
// Verify notes were updated
|
|
1335
|
-
const collection = await loadFileCollection(contextId,
|
|
1517
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1336
1518
|
const file = collection.find(f => f.id === addParsed.fileId);
|
|
1337
1519
|
t.is(file.notes, 'Updated notes with more detail');
|
|
1338
1520
|
} finally {
|
|
@@ -1367,7 +1549,7 @@ test('File collection: UpdateFileMetadata tool - Update permanent flag', async t
|
|
|
1367
1549
|
t.is(updateParsed.success, true);
|
|
1368
1550
|
|
|
1369
1551
|
// Verify permanent flag was set
|
|
1370
|
-
const collection = await loadFileCollection(contextId,
|
|
1552
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1371
1553
|
const file = collection.find(f => f.id === addParsed.fileId);
|
|
1372
1554
|
t.is(file.permanent, true);
|
|
1373
1555
|
} finally {
|
|
@@ -1412,7 +1594,7 @@ test('File collection: UpdateFileMetadata tool - Combined updates', async t => {
|
|
|
1412
1594
|
t.true(updateParsed.message.includes('permanent'));
|
|
1413
1595
|
|
|
1414
1596
|
// Verify all updates persisted
|
|
1415
|
-
const collection = await loadFileCollection(contextId,
|
|
1597
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1416
1598
|
const file = collection.find(f => f.id === originalFileId);
|
|
1417
1599
|
t.is(file.displayFilename, 'renamed-and-tagged.pdf');
|
|
1418
1600
|
t.deepEqual(file.tags, ['new', 'tags']);
|
|
@@ -1472,7 +1654,7 @@ test('File collection: UpdateFileMetadata tool - Find file by ID', async t => {
|
|
|
1472
1654
|
t.is(updateParsed.success, true);
|
|
1473
1655
|
|
|
1474
1656
|
// Verify update worked
|
|
1475
|
-
const collection = await loadFileCollection(contextId,
|
|
1657
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1476
1658
|
const file = collection.find(f => f.id === fileId);
|
|
1477
1659
|
t.is(file.displayFilename, 'renamed-by-id.pdf');
|
|
1478
1660
|
} finally {
|
|
@@ -1513,7 +1695,7 @@ test('File collection: addFileToCollection returns correct ID for existing files
|
|
|
1513
1695
|
t.is(addParsed2.fileId, firstFileId, 'Second add should return same ID as first');
|
|
1514
1696
|
|
|
1515
1697
|
// Verify only one entry exists (not duplicated)
|
|
1516
|
-
const collection = await loadFileCollection(contextId,
|
|
1698
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1517
1699
|
t.is(collection.length, 1);
|
|
1518
1700
|
|
|
1519
1701
|
// Verify metadata was merged (tags from second add, but same ID)
|
|
@@ -1526,6 +1708,83 @@ test('File collection: addFileToCollection returns correct ID for existing files
|
|
|
1526
1708
|
}
|
|
1527
1709
|
});
|
|
1528
1710
|
|
|
1711
|
+
test('File collection: AddFileToCollection adds same file to multiple chats without creating duplicates', async t => {
|
|
1712
|
+
const contextId = createTestContext();
|
|
1713
|
+
const chatId1 = `test-chat-1-${Date.now()}`;
|
|
1714
|
+
const chatId2 = `test-chat-2-${Date.now()}`;
|
|
1715
|
+
|
|
1716
|
+
try {
|
|
1717
|
+
// Add file to chat-1
|
|
1718
|
+
const addResult1 = await callPathway('sys_tool_file_collection', {
|
|
1719
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
1720
|
+
url: 'https://example.com/shared-file.pdf',
|
|
1721
|
+
filename: 'shared-file.pdf',
|
|
1722
|
+
tags: ['shared'],
|
|
1723
|
+
userMessage: 'Add file to chat-1',
|
|
1724
|
+
chatId: chatId1
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
const addParsed1 = JSON.parse(addResult1);
|
|
1728
|
+
t.is(addParsed1.success, true);
|
|
1729
|
+
const fileId1 = addParsed1.fileId;
|
|
1730
|
+
|
|
1731
|
+
// Add same file (same URL = same hash) to chat-2
|
|
1732
|
+
const addResult2 = await callPathway('sys_tool_file_collection', {
|
|
1733
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
1734
|
+
url: 'https://example.com/shared-file.pdf',
|
|
1735
|
+
filename: 'shared-file.pdf',
|
|
1736
|
+
tags: ['shared'],
|
|
1737
|
+
userMessage: 'Add same file to chat-2',
|
|
1738
|
+
chatId: chatId2
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
const addParsed2 = JSON.parse(addResult2);
|
|
1742
|
+
t.is(addParsed2.success, true);
|
|
1743
|
+
|
|
1744
|
+
// Should return the same file ID (no duplicate created)
|
|
1745
|
+
t.is(addParsed2.fileId, fileId1, 'Second add should return same ID (no duplicate)');
|
|
1746
|
+
|
|
1747
|
+
// Verify only one file entry exists (match by URL since hash is computed from URL)
|
|
1748
|
+
const allFiles = await loadFileCollection(contextId, { useCache: false });
|
|
1749
|
+
const matchingFiles = allFiles.filter(f => f.url === 'https://example.com/shared-file.pdf');
|
|
1750
|
+
t.is(matchingFiles.length, 1, 'Should have only one file entry (no duplicate)');
|
|
1751
|
+
|
|
1752
|
+
const file = matchingFiles[0];
|
|
1753
|
+
t.is(file.id, fileId1, 'File ID should match');
|
|
1754
|
+
|
|
1755
|
+
// Verify inCollection contains both chatIds (reference counting)
|
|
1756
|
+
t.truthy(file.inCollection, 'File should have inCollection set');
|
|
1757
|
+
|
|
1758
|
+
if (Array.isArray(file.inCollection)) {
|
|
1759
|
+
// Both chatIds should be in the array
|
|
1760
|
+
t.true(
|
|
1761
|
+
file.inCollection.includes(chatId1) && file.inCollection.includes(chatId2),
|
|
1762
|
+
`inCollection should contain both chatIds: ${JSON.stringify(file.inCollection)}`
|
|
1763
|
+
);
|
|
1764
|
+
} else if (file.inCollection === true) {
|
|
1765
|
+
// If it's global (true), that's also valid (one chat might have been global)
|
|
1766
|
+
t.pass('File is global (inCollection=true), which is valid');
|
|
1767
|
+
} else {
|
|
1768
|
+
t.fail(`Unexpected inCollection value: ${JSON.stringify(file.inCollection)}`);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Verify file appears in both chats' collections when filtered
|
|
1772
|
+
const chat1Files = await loadFileCollection(contextId, { chatIds: [chatId1], useCache: false });
|
|
1773
|
+
const fileInChat1 = chat1Files.find(f => f.id === fileId1);
|
|
1774
|
+
t.truthy(fileInChat1, 'File should appear in chat-1 collection');
|
|
1775
|
+
|
|
1776
|
+
const chat2Files = await loadFileCollection(contextId, { chatIds: [chatId2], useCache: false });
|
|
1777
|
+
const fileInChat2 = chat2Files.find(f => f.id === fileId1);
|
|
1778
|
+
t.truthy(fileInChat2, 'File should appear in chat-2 collection');
|
|
1779
|
+
|
|
1780
|
+
// Verify it's the same file object (same ID)
|
|
1781
|
+
t.is(fileInChat1.id, fileInChat2.id, 'Both chats should reference the same file');
|
|
1782
|
+
|
|
1783
|
+
} finally {
|
|
1784
|
+
await cleanup(contextId);
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1529
1788
|
// ============================================
|
|
1530
1789
|
// File Collection Encryption Tests
|
|
1531
1790
|
// ============================================
|
|
@@ -1552,7 +1811,7 @@ test('File collection encryption: Encrypt tags and notes with contextKey', async
|
|
|
1552
1811
|
const { getRedisClient } = await import('../../../../lib/fileUtils.js');
|
|
1553
1812
|
const redisClient = await getRedisClient();
|
|
1554
1813
|
const contextMapKey = `FileStoreMap:ctx:${contextId}`;
|
|
1555
|
-
const collection = await loadFileCollection(contextId, contextKey, false);
|
|
1814
|
+
const collection = await loadFileCollection({ contextId, contextKey, default: true }, { useCache: false });
|
|
1556
1815
|
const file = collection.find(f => f.id === parsed.fileId);
|
|
1557
1816
|
t.truthy(file);
|
|
1558
1817
|
|
|
@@ -1601,7 +1860,7 @@ test('File collection encryption: Empty tags and notes are not encrypted', async
|
|
|
1601
1860
|
const { getRedisClient } = await import('../../../../lib/fileUtils.js');
|
|
1602
1861
|
const redisClient = await getRedisClient();
|
|
1603
1862
|
const contextMapKey = `FileStoreMap:ctx:${contextId}`;
|
|
1604
|
-
const collection = await loadFileCollection(contextId, contextKey, false);
|
|
1863
|
+
const collection = await loadFileCollection({ contextId, contextKey, default: true }, { useCache: false });
|
|
1605
1864
|
const file = collection.find(f => f.id === parsed.fileId);
|
|
1606
1865
|
t.truthy(file);
|
|
1607
1866
|
|
|
@@ -1641,7 +1900,7 @@ test('File collection encryption: Decryption fails with wrong contextKey', async
|
|
|
1641
1900
|
t.is(parsed.success, true);
|
|
1642
1901
|
|
|
1643
1902
|
// Try to load with wrong key
|
|
1644
|
-
const collection = await loadFileCollection(contextId, wrongKey, false);
|
|
1903
|
+
const collection = await loadFileCollection({ contextId, contextKey: wrongKey, default: true }, { useCache: false });
|
|
1645
1904
|
const file = collection.find(f => f.id === parsed.fileId);
|
|
1646
1905
|
t.truthy(file);
|
|
1647
1906
|
|
|
@@ -1691,7 +1950,7 @@ test('File collection encryption: Migration from unencrypted to encrypted', asyn
|
|
|
1691
1950
|
const { getRedisClient } = await import('../../../../lib/fileUtils.js');
|
|
1692
1951
|
const redisClient = await getRedisClient();
|
|
1693
1952
|
const contextMapKey = `FileStoreMap:ctx:${contextId}`;
|
|
1694
|
-
const collection1 = await loadFileCollection(contextId,
|
|
1953
|
+
const collection1 = await loadFileCollection(contextId, { useCache: false });
|
|
1695
1954
|
const file1 = collection1.find(f => f.id === parsed1.fileId);
|
|
1696
1955
|
t.truthy(file1);
|
|
1697
1956
|
|
|
@@ -1721,7 +1980,7 @@ test('File collection encryption: Migration from unencrypted to encrypted', asyn
|
|
|
1721
1980
|
t.true(rawData2.notes.includes(':'), 'Encrypted notes should contain IV separator');
|
|
1722
1981
|
|
|
1723
1982
|
// Verify decryption works
|
|
1724
|
-
const collection2 = await loadFileCollection(contextId, contextKey, false);
|
|
1983
|
+
const collection2 = await loadFileCollection({ contextId, contextKey, default: true }, { useCache: false });
|
|
1725
1984
|
const file2 = collection2.find(f => f.id === parsed1.fileId);
|
|
1726
1985
|
t.deepEqual(file2.tags, ['encrypted'], 'Tags should be decrypted correctly');
|
|
1727
1986
|
t.is(file2.notes, 'Encrypted notes', 'Notes should be decrypted correctly');
|
|
@@ -1753,7 +2012,7 @@ test('File collection encryption: Core fields are never encrypted', async t => {
|
|
|
1753
2012
|
const { getRedisClient } = await import('../../../../lib/fileUtils.js');
|
|
1754
2013
|
const redisClient = await getRedisClient();
|
|
1755
2014
|
const contextMapKey = `FileStoreMap:ctx:${contextId}`;
|
|
1756
|
-
const collection = await loadFileCollection(contextId, contextKey, false);
|
|
2015
|
+
const collection = await loadFileCollection({ contextId, contextKey, default: true }, { useCache: false });
|
|
1757
2016
|
const file = collection.find(f => f.id === parsed.fileId);
|
|
1758
2017
|
t.truthy(file);
|
|
1759
2018
|
|
|
@@ -1795,7 +2054,7 @@ test('File collection encryption: Works without contextKey (no encryption)', asy
|
|
|
1795
2054
|
const { getRedisClient } = await import('../../../../lib/fileUtils.js');
|
|
1796
2055
|
const redisClient = await getRedisClient();
|
|
1797
2056
|
const contextMapKey = `FileStoreMap:ctx:${contextId}`;
|
|
1798
|
-
const collection = await loadFileCollection(contextId,
|
|
2057
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1799
2058
|
const file = collection.find(f => f.id === parsed.fileId);
|
|
1800
2059
|
t.truthy(file);
|
|
1801
2060
|
|
|
@@ -1849,7 +2108,7 @@ test('File collection: YouTube URLs are rejected (cannot be added to collection)
|
|
|
1849
2108
|
}
|
|
1850
2109
|
|
|
1851
2110
|
// Verify it was NOT added to collection
|
|
1852
|
-
const collection = await loadFileCollection(contextId,
|
|
2111
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1853
2112
|
t.is(collection.length, 0);
|
|
1854
2113
|
} catch (error) {
|
|
1855
2114
|
// If callPathway throws, verify the error message
|
|
@@ -1859,7 +2118,7 @@ test('File collection: YouTube URLs are rejected (cannot be added to collection)
|
|
|
1859
2118
|
);
|
|
1860
2119
|
|
|
1861
2120
|
// Verify it was NOT added to collection
|
|
1862
|
-
const collection = await loadFileCollection(contextId,
|
|
2121
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1863
2122
|
t.is(collection.length, 0);
|
|
1864
2123
|
} finally {
|
|
1865
2124
|
await cleanup(contextId);
|
|
@@ -1886,11 +2145,11 @@ test('File collection: YouTube Shorts URLs are rejected', async t => {
|
|
|
1886
2145
|
t.true(result.includes('YouTube URLs cannot be added'));
|
|
1887
2146
|
}
|
|
1888
2147
|
|
|
1889
|
-
const collection = await loadFileCollection(contextId,
|
|
2148
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1890
2149
|
t.is(collection.length, 0);
|
|
1891
2150
|
} catch (error) {
|
|
1892
2151
|
t.true(error.message.includes('YouTube URLs cannot be added'));
|
|
1893
|
-
const collection = await loadFileCollection(contextId,
|
|
2152
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1894
2153
|
t.is(collection.length, 0);
|
|
1895
2154
|
} finally {
|
|
1896
2155
|
await cleanup(contextId);
|
|
@@ -1917,11 +2176,11 @@ test('File collection: youtu.be URLs are rejected', async t => {
|
|
|
1917
2176
|
t.true(result.includes('YouTube URLs cannot be added'));
|
|
1918
2177
|
}
|
|
1919
2178
|
|
|
1920
|
-
const collection = await loadFileCollection(contextId,
|
|
2179
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1921
2180
|
t.is(collection.length, 0);
|
|
1922
2181
|
} catch (error) {
|
|
1923
2182
|
t.true(error.message.includes('YouTube URLs cannot be added'));
|
|
1924
|
-
const collection = await loadFileCollection(contextId,
|
|
2183
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1925
2184
|
t.is(collection.length, 0);
|
|
1926
2185
|
} finally {
|
|
1927
2186
|
await cleanup(contextId);
|
|
@@ -1943,7 +2202,7 @@ test('generateFileMessageContent: Accepts direct YouTube URL without collection'
|
|
|
1943
2202
|
t.falsy(fileContent.hash);
|
|
1944
2203
|
|
|
1945
2204
|
// Verify it's not in the collection
|
|
1946
|
-
const collection = await loadFileCollection(contextId,
|
|
2205
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
1947
2206
|
t.is(collection.length, 0);
|
|
1948
2207
|
} finally {
|
|
1949
2208
|
await cleanup(contextId);
|
|
@@ -1970,7 +2229,7 @@ test('Analyzer tool: Returns error JSON format when file not found', async t =>
|
|
|
1970
2229
|
try {
|
|
1971
2230
|
const result = await callPathway('sys_tool_analyzefile', {
|
|
1972
2231
|
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
1973
|
-
|
|
2232
|
+
files: ['non-existent-file.jpg'],
|
|
1974
2233
|
detailedInstructions: 'Analyze this file',
|
|
1975
2234
|
userMessage: 'Testing error handling'
|
|
1976
2235
|
});
|
|
@@ -2009,7 +2268,7 @@ test('Analyzer tool: Works with legacy contextId/contextKey parameters (backward
|
|
|
2009
2268
|
});
|
|
2010
2269
|
|
|
2011
2270
|
// Get the file ID from the collection
|
|
2012
|
-
const collection = await loadFileCollection(contextId,
|
|
2271
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
2013
2272
|
const fileId = collection[0].id;
|
|
2014
2273
|
|
|
2015
2274
|
// Test analyzer tool with legacy contextId/contextKey (without agentContext)
|
|
@@ -2017,7 +2276,7 @@ test('Analyzer tool: Works with legacy contextId/contextKey parameters (backward
|
|
|
2017
2276
|
const result = await callPathway('sys_tool_analyzefile', {
|
|
2018
2277
|
contextId, // Legacy format - no agentContext
|
|
2019
2278
|
contextKey: null,
|
|
2020
|
-
|
|
2279
|
+
files: [fileId],
|
|
2021
2280
|
detailedInstructions: 'What is this file?',
|
|
2022
2281
|
userMessage: 'Testing backward compatibility'
|
|
2023
2282
|
});
|
|
@@ -2055,13 +2314,13 @@ test('Analyzer tool: File resolution works with agentContext', async t => {
|
|
|
2055
2314
|
});
|
|
2056
2315
|
|
|
2057
2316
|
// Get the file ID from the collection
|
|
2058
|
-
const collection = await loadFileCollection(contextId,
|
|
2317
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
2059
2318
|
const fileId = collection[0].id;
|
|
2060
2319
|
|
|
2061
2320
|
// Test analyzer tool with agentContext (modern format)
|
|
2062
2321
|
const result = await callPathway('sys_tool_analyzefile', {
|
|
2063
2322
|
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2064
|
-
|
|
2323
|
+
files: [fileId],
|
|
2065
2324
|
detailedInstructions: 'What is this file?',
|
|
2066
2325
|
userMessage: 'Testing with agentContext'
|
|
2067
2326
|
});
|
|
@@ -2106,7 +2365,7 @@ test('Converted files: displayFilename .docx but URL .md - MIME type from URL',
|
|
|
2106
2365
|
t.is(addParsed.success, true);
|
|
2107
2366
|
|
|
2108
2367
|
// Verify file was stored correctly
|
|
2109
|
-
const collection = await loadFileCollection(contextId,
|
|
2368
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
2110
2369
|
t.is(collection.length, 1);
|
|
2111
2370
|
|
|
2112
2371
|
const file = collection[0];
|
|
@@ -2137,7 +2396,7 @@ test('Converted files: EditFile should use URL MIME type, not displayFilename',
|
|
|
2137
2396
|
t.is(addParsed.success, true);
|
|
2138
2397
|
|
|
2139
2398
|
// Verify the file has correct MIME type from URL
|
|
2140
|
-
const collection = await loadFileCollection(contextId,
|
|
2399
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
2141
2400
|
const file = collection[0];
|
|
2142
2401
|
t.is(file.mimeType, 'text/markdown', 'MIME type should be from URL');
|
|
2143
2402
|
t.is(file.displayFilename, 'report.docx', 'displayFilename should be original');
|
|
@@ -2170,7 +2429,7 @@ test('Converted files: ReadFile should accept text files based on URL, not displ
|
|
|
2170
2429
|
t.is(addParsed.success, true);
|
|
2171
2430
|
|
|
2172
2431
|
// Verify file setup
|
|
2173
|
-
const collection = await loadFileCollection(contextId,
|
|
2432
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
2174
2433
|
const file = collection[0];
|
|
2175
2434
|
t.is(file.displayFilename, 'document.docx');
|
|
2176
2435
|
t.is(file.mimeType, 'text/markdown');
|
|
@@ -2219,7 +2478,7 @@ test('Converted files: Multiple converted files with different extensions', asyn
|
|
|
2219
2478
|
});
|
|
2220
2479
|
|
|
2221
2480
|
// Verify all files have correct MIME types from URLs
|
|
2222
|
-
const collection = await loadFileCollection(contextId,
|
|
2481
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
2223
2482
|
t.is(collection.length, 3);
|
|
2224
2483
|
|
|
2225
2484
|
const doc1 = collection.find(f => f.displayFilename === 'document1.docx');
|
|
@@ -2277,7 +2536,7 @@ test('Converted files: loadFileCollection should use converted values as primary
|
|
|
2277
2536
|
await writeFileDataToRedis(redisClient, contextMapKey, hash, fileData, null);
|
|
2278
2537
|
|
|
2279
2538
|
// Load the collection
|
|
2280
|
-
const collection = await loadFileCollection(contextId,
|
|
2539
|
+
const collection = await loadFileCollection(contextId, { useCache: false });
|
|
2281
2540
|
t.is(collection.length, 1);
|
|
2282
2541
|
|
|
2283
2542
|
const file = collection[0];
|
|
@@ -2324,8 +2583,8 @@ test('Converted files: loadFileCollection should use converted values as primary
|
|
|
2324
2583
|
}
|
|
2325
2584
|
});
|
|
2326
2585
|
|
|
2327
|
-
test('
|
|
2328
|
-
const {
|
|
2586
|
+
test('loadFileCollection should merge collections from multiple contexts', async t => {
|
|
2587
|
+
const { loadFileCollection, addFileToCollection, getRedisClient } = await import('../../../../lib/fileUtils.js');
|
|
2329
2588
|
|
|
2330
2589
|
const contextId = `test-primary-${Date.now()}`;
|
|
2331
2590
|
const altContextId = `test-alt-${Date.now()}`;
|
|
@@ -2338,12 +2597,12 @@ test('loadMergedFileCollection should merge collections from contextId and altCo
|
|
|
2338
2597
|
await addFileToCollection(altContextId, null, 'https://example.com/alt.jpg', null, 'alt.jpg', [], '', 'hash-alt');
|
|
2339
2598
|
|
|
2340
2599
|
// Load just primary - should have 1 file
|
|
2341
|
-
const primaryOnly = await
|
|
2600
|
+
const primaryOnly = await loadFileCollection([{ contextId, contextKey: null, default: true }]);
|
|
2342
2601
|
t.is(primaryOnly.length, 1);
|
|
2343
2602
|
t.is(primaryOnly[0].hash, 'hash-primary');
|
|
2344
2603
|
|
|
2345
|
-
// Load
|
|
2346
|
-
const merged = await
|
|
2604
|
+
// Load from both contexts - should have 2 files (both contexts unencrypted)
|
|
2605
|
+
const merged = await loadFileCollection([
|
|
2347
2606
|
{ contextId, contextKey: null, default: true },
|
|
2348
2607
|
{ contextId: altContextId, contextKey: null, default: false }
|
|
2349
2608
|
]);
|
|
@@ -2359,8 +2618,8 @@ test('loadMergedFileCollection should merge collections from contextId and altCo
|
|
|
2359
2618
|
}
|
|
2360
2619
|
});
|
|
2361
2620
|
|
|
2362
|
-
test('
|
|
2363
|
-
const {
|
|
2621
|
+
test('loadFileCollection should dedupe files present in both contexts', async t => {
|
|
2622
|
+
const { loadFileCollection, addFileToCollection, getRedisClient } = await import('../../../../lib/fileUtils.js');
|
|
2364
2623
|
|
|
2365
2624
|
const contextId = `test-primary-dupe-${Date.now()}`;
|
|
2366
2625
|
const altContextId = `test-alt-dupe-${Date.now()}`;
|
|
@@ -2373,8 +2632,8 @@ test('loadMergedFileCollection should dedupe files present in both contexts', as
|
|
|
2373
2632
|
// Add unique file to alt context
|
|
2374
2633
|
await addFileToCollection(altContextId, null, 'https://example.com/alt-only.jpg', null, 'alt-only.jpg', [], '', 'hash-alt-only');
|
|
2375
2634
|
|
|
2376
|
-
// Load
|
|
2377
|
-
const merged = await
|
|
2635
|
+
// Load from both contexts - should have 2 files (deduped shared file, both contexts unencrypted)
|
|
2636
|
+
const merged = await loadFileCollection([
|
|
2378
2637
|
{ contextId, contextKey: null, default: true },
|
|
2379
2638
|
{ contextId: altContextId, contextKey: null, default: false }
|
|
2380
2639
|
]);
|
|
@@ -2710,3 +2969,377 @@ test('File collection: SearchFileCollection normalizes separators (space/dash/un
|
|
|
2710
2969
|
await cleanup(contextId);
|
|
2711
2970
|
}
|
|
2712
2971
|
});
|
|
2972
|
+
|
|
2973
|
+
test('File collection: SearchFileCollection comprehensive test - all search permutations', async t => {
|
|
2974
|
+
const contextId = createTestContext();
|
|
2975
|
+
const chatId1 = `test-chat-1-${Date.now()}`;
|
|
2976
|
+
const chatId2 = `test-chat-2-${Date.now()}`;
|
|
2977
|
+
|
|
2978
|
+
try {
|
|
2979
|
+
const { addFileToCollection } = await import('../../../../lib/fileUtils.js');
|
|
2980
|
+
|
|
2981
|
+
// Setup: Create files in different chats with various metadata
|
|
2982
|
+
// Chat-1 files
|
|
2983
|
+
await addFileToCollection(contextId, null, 'https://example.com/report-2024.pdf', null, 'Annual Report 2024.pdf', ['finance', 'annual'], 'Year-end financial report', 'hash-report-2024', null, null, false, chatId1);
|
|
2984
|
+
await addFileToCollection(contextId, null, 'https://example.com/budget.xlsx', null, 'Budget Q1.xlsx', ['finance', 'budget'], 'First quarter budget', 'hash-budget-q1', null, null, false, chatId1);
|
|
2985
|
+
await addFileToCollection(contextId, null, 'https://example.com/meeting-notes.txt', null, 'Meeting Notes.txt', ['meetings'], 'Team meeting notes', 'hash-meeting-notes', null, null, false, chatId1);
|
|
2986
|
+
|
|
2987
|
+
// Chat-2 files
|
|
2988
|
+
await addFileToCollection(contextId, null, 'https://example.com/presentation.pdf', null, 'Product Presentation.pdf', ['product', 'sales'], 'Product launch presentation', 'hash-presentation', null, null, false, chatId2);
|
|
2989
|
+
await addFileToCollection(contextId, null, 'https://example.com/roadmap.xlsx', null, 'Product Roadmap.xlsx', ['product', 'planning'], 'Product roadmap for 2024', 'hash-roadmap', null, null, false, chatId2);
|
|
2990
|
+
|
|
2991
|
+
// Global files (no chatId)
|
|
2992
|
+
await addFileToCollection(contextId, null, 'https://example.com/company-policy.pdf', null, 'Company Policy.pdf', ['policy', 'hr'], 'Company-wide policies', 'hash-policy', null, null, false, null);
|
|
2993
|
+
await addFileToCollection(contextId, null, 'https://example.com/employee-handbook.pdf', null, 'Employee Handbook.pdf', ['hr', 'handbook'], 'Employee handbook', 'hash-handbook', null, null, false, null);
|
|
2994
|
+
|
|
2995
|
+
// Shared file (same file in both chats)
|
|
2996
|
+
await addFileToCollection(contextId, null, 'https://example.com/shared-doc.pdf', null, 'Shared Document.pdf', ['shared'], 'Document shared across chats', 'hash-shared', null, null, false, chatId1);
|
|
2997
|
+
await addFileToCollection(contextId, null, 'https://example.com/shared-doc.pdf', null, 'Shared Document.pdf', ['shared'], 'Document shared across chats', 'hash-shared', null, null, false, chatId2);
|
|
2998
|
+
|
|
2999
|
+
// Test 1: Search by filename (partial match) within chat-1
|
|
3000
|
+
const result1 = await callPathway('sys_tool_file_collection', {
|
|
3001
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3002
|
+
chatId: chatId1,
|
|
3003
|
+
query: 'Report',
|
|
3004
|
+
userMessage: 'Search for Report in chat-1'
|
|
3005
|
+
});
|
|
3006
|
+
const parsed1 = JSON.parse(result1);
|
|
3007
|
+
t.is(parsed1.success, true);
|
|
3008
|
+
t.is(parsed1.count, 1, 'Should find 1 file in chat-1');
|
|
3009
|
+
t.is(parsed1.files[0].displayFilename, 'Annual Report 2024.pdf');
|
|
3010
|
+
|
|
3011
|
+
// Test 2: Search by filename (partial match) across all chats
|
|
3012
|
+
const result2 = await callPathway('sys_tool_file_collection', {
|
|
3013
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3014
|
+
chatId: chatId1,
|
|
3015
|
+
query: 'Report',
|
|
3016
|
+
includeAllChats: true,
|
|
3017
|
+
userMessage: 'Search for Report across all chats'
|
|
3018
|
+
});
|
|
3019
|
+
const parsed2 = JSON.parse(result2);
|
|
3020
|
+
t.is(parsed2.success, true);
|
|
3021
|
+
t.is(parsed2.count, 1, 'Should still find 1 file (only one has "Report" in name)');
|
|
3022
|
+
|
|
3023
|
+
// Test 3: Search by tag within chat-1
|
|
3024
|
+
const result3 = await callPathway('sys_tool_file_collection', {
|
|
3025
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3026
|
+
chatId: chatId1,
|
|
3027
|
+
query: 'finance',
|
|
3028
|
+
userMessage: 'Search by tag finance in chat-1'
|
|
3029
|
+
});
|
|
3030
|
+
const parsed3 = JSON.parse(result3);
|
|
3031
|
+
t.is(parsed3.success, true);
|
|
3032
|
+
t.is(parsed3.count, 2, 'Should find 2 files with finance tag in chat-1');
|
|
3033
|
+
const filenames3 = parsed3.files.map(f => f.displayFilename);
|
|
3034
|
+
t.true(filenames3.includes('Annual Report 2024.pdf'));
|
|
3035
|
+
t.true(filenames3.includes('Budget Q1.xlsx'));
|
|
3036
|
+
|
|
3037
|
+
// Test 4: Search by tag across all chats
|
|
3038
|
+
const result4 = await callPathway('sys_tool_file_collection', {
|
|
3039
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3040
|
+
chatId: chatId1,
|
|
3041
|
+
query: 'product',
|
|
3042
|
+
includeAllChats: true,
|
|
3043
|
+
userMessage: 'Search by tag product across all chats'
|
|
3044
|
+
});
|
|
3045
|
+
const parsed4 = JSON.parse(result4);
|
|
3046
|
+
t.is(parsed4.success, true);
|
|
3047
|
+
t.is(parsed4.count, 2, 'Should find 2 files with product tag from chat-2');
|
|
3048
|
+
const filenames4 = parsed4.files.map(f => f.displayFilename);
|
|
3049
|
+
t.true(filenames4.includes('Product Presentation.pdf'));
|
|
3050
|
+
t.true(filenames4.includes('Product Roadmap.xlsx'));
|
|
3051
|
+
|
|
3052
|
+
// Test 5: Search by notes (partial match)
|
|
3053
|
+
const result5 = await callPathway('sys_tool_file_collection', {
|
|
3054
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3055
|
+
chatId: chatId1,
|
|
3056
|
+
query: 'financial',
|
|
3057
|
+
userMessage: 'Search by notes containing financial'
|
|
3058
|
+
});
|
|
3059
|
+
const parsed5 = JSON.parse(result5);
|
|
3060
|
+
t.is(parsed5.success, true);
|
|
3061
|
+
t.is(parsed5.count, 1, 'Should find file with "financial" in notes');
|
|
3062
|
+
t.is(parsed5.files[0].displayFilename, 'Annual Report 2024.pdf');
|
|
3063
|
+
|
|
3064
|
+
// Test 6: Search with tag filter
|
|
3065
|
+
const result6 = await callPathway('sys_tool_file_collection', {
|
|
3066
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3067
|
+
chatId: chatId1,
|
|
3068
|
+
query: '2024',
|
|
3069
|
+
tags: ['finance'],
|
|
3070
|
+
userMessage: 'Search for 2024 with finance tag filter'
|
|
3071
|
+
});
|
|
3072
|
+
const parsed6 = JSON.parse(result6);
|
|
3073
|
+
t.is(parsed6.success, true);
|
|
3074
|
+
t.is(parsed6.count, 1, 'Should find 1 file matching both query and tag filter');
|
|
3075
|
+
t.is(parsed6.files[0].displayFilename, 'Annual Report 2024.pdf');
|
|
3076
|
+
|
|
3077
|
+
// Test 7: Search with tag filter (no matches)
|
|
3078
|
+
const result7 = await callPathway('sys_tool_file_collection', {
|
|
3079
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3080
|
+
chatId: chatId1,
|
|
3081
|
+
query: 'Report',
|
|
3082
|
+
tags: ['product'],
|
|
3083
|
+
userMessage: 'Search for Report with product tag (should find nothing)'
|
|
3084
|
+
});
|
|
3085
|
+
const parsed7 = JSON.parse(result7);
|
|
3086
|
+
t.is(parsed7.success, true);
|
|
3087
|
+
t.is(parsed7.count, 0, 'Should find no files (Report is in chat-1, product tag is in chat-2)');
|
|
3088
|
+
|
|
3089
|
+
// Test 8: Search with tag filter across all chats
|
|
3090
|
+
const result8 = await callPathway('sys_tool_file_collection', {
|
|
3091
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3092
|
+
chatId: chatId1,
|
|
3093
|
+
query: 'Report',
|
|
3094
|
+
tags: ['product'],
|
|
3095
|
+
includeAllChats: true,
|
|
3096
|
+
userMessage: 'Search for Report with product tag across all chats (should find nothing)'
|
|
3097
|
+
});
|
|
3098
|
+
const parsed8 = JSON.parse(result8);
|
|
3099
|
+
t.is(parsed8.success, true);
|
|
3100
|
+
t.is(parsed8.count, 0, 'Should find no files (Report file has finance tag, not product)');
|
|
3101
|
+
|
|
3102
|
+
// Test 9: Search for shared file from chat-1
|
|
3103
|
+
const result9 = await callPathway('sys_tool_file_collection', {
|
|
3104
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3105
|
+
chatId: chatId1,
|
|
3106
|
+
query: 'Shared',
|
|
3107
|
+
userMessage: 'Search for Shared file in chat-1'
|
|
3108
|
+
});
|
|
3109
|
+
const parsed9 = JSON.parse(result9);
|
|
3110
|
+
t.is(parsed9.success, true);
|
|
3111
|
+
t.is(parsed9.count, 1, 'Should find shared file in chat-1');
|
|
3112
|
+
t.is(parsed9.files[0].displayFilename, 'Shared Document.pdf');
|
|
3113
|
+
|
|
3114
|
+
// Test 10: Search for shared file from chat-2
|
|
3115
|
+
const result10 = await callPathway('sys_tool_file_collection', {
|
|
3116
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3117
|
+
chatId: chatId2,
|
|
3118
|
+
query: 'Shared',
|
|
3119
|
+
userMessage: 'Search for Shared file in chat-2'
|
|
3120
|
+
});
|
|
3121
|
+
const parsed10 = JSON.parse(result10);
|
|
3122
|
+
t.is(parsed10.success, true);
|
|
3123
|
+
t.is(parsed10.count, 1, 'Should find shared file in chat-2');
|
|
3124
|
+
t.is(parsed10.files[0].displayFilename, 'Shared Document.pdf');
|
|
3125
|
+
|
|
3126
|
+
// Test 11: Search for shared file across all chats (should still be 1 result, not 2)
|
|
3127
|
+
const result11 = await callPathway('sys_tool_file_collection', {
|
|
3128
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3129
|
+
chatId: chatId1,
|
|
3130
|
+
query: 'Shared',
|
|
3131
|
+
includeAllChats: true,
|
|
3132
|
+
userMessage: 'Search for Shared file across all chats'
|
|
3133
|
+
});
|
|
3134
|
+
const parsed11 = JSON.parse(result11);
|
|
3135
|
+
t.is(parsed11.success, true);
|
|
3136
|
+
t.is(parsed11.count, 1, 'Should find 1 shared file (not duplicated)');
|
|
3137
|
+
t.is(parsed11.files[0].displayFilename, 'Shared Document.pdf');
|
|
3138
|
+
|
|
3139
|
+
// Test 12: Search with limit
|
|
3140
|
+
const result12 = await callPathway('sys_tool_file_collection', {
|
|
3141
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3142
|
+
chatId: chatId1,
|
|
3143
|
+
query: 'pdf',
|
|
3144
|
+
limit: 2,
|
|
3145
|
+
userMessage: 'Search for pdf with limit 2'
|
|
3146
|
+
});
|
|
3147
|
+
const parsed12 = JSON.parse(result12);
|
|
3148
|
+
t.is(parsed12.success, true);
|
|
3149
|
+
t.true(parsed12.count <= 2, 'Should respect limit');
|
|
3150
|
+
t.true(parsed12.files.length <= 2, 'Results array should respect limit');
|
|
3151
|
+
|
|
3152
|
+
// Test 13: Search for global files from chat-1
|
|
3153
|
+
const result13 = await callPathway('sys_tool_file_collection', {
|
|
3154
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3155
|
+
chatId: chatId1,
|
|
3156
|
+
query: 'Policy',
|
|
3157
|
+
userMessage: 'Search for Policy (global file) from chat-1'
|
|
3158
|
+
});
|
|
3159
|
+
const parsed13 = JSON.parse(result13);
|
|
3160
|
+
t.is(parsed13.success, true);
|
|
3161
|
+
t.is(parsed13.count, 1, 'Should find global file from chat-1');
|
|
3162
|
+
t.is(parsed13.files[0].displayFilename, 'Company Policy.pdf');
|
|
3163
|
+
|
|
3164
|
+
// Test 14: Search with normalized matching (space/dash/underscore)
|
|
3165
|
+
await addFileToCollection(contextId, null, 'https://example.com/test-file.pdf', null, 'Test File Name.pdf', [], '', 'hash-test-normalized', null, null, false, chatId1);
|
|
3166
|
+
const result14 = await callPathway('sys_tool_file_collection', {
|
|
3167
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3168
|
+
chatId: chatId1,
|
|
3169
|
+
query: 'Test-File',
|
|
3170
|
+
userMessage: 'Search with dash should match space-separated'
|
|
3171
|
+
});
|
|
3172
|
+
const parsed14 = JSON.parse(result14);
|
|
3173
|
+
t.is(parsed14.success, true);
|
|
3174
|
+
t.is(parsed14.count, 1, 'Should find file with normalized matching');
|
|
3175
|
+
t.is(parsed14.files[0].displayFilename, 'Test File Name.pdf');
|
|
3176
|
+
|
|
3177
|
+
// Test 15: Search with multiple tag filters
|
|
3178
|
+
const result15 = await callPathway('sys_tool_file_collection', {
|
|
3179
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3180
|
+
chatId: chatId1,
|
|
3181
|
+
query: 'Report',
|
|
3182
|
+
tags: ['finance', 'annual'],
|
|
3183
|
+
userMessage: 'Search with multiple tag filters'
|
|
3184
|
+
});
|
|
3185
|
+
const parsed15 = JSON.parse(result15);
|
|
3186
|
+
t.is(parsed15.success, true);
|
|
3187
|
+
t.is(parsed15.count, 1, 'Should find file matching all tags');
|
|
3188
|
+
t.is(parsed15.files[0].displayFilename, 'Annual Report 2024.pdf');
|
|
3189
|
+
|
|
3190
|
+
// Test 16: Search with no matches
|
|
3191
|
+
const result16 = await callPathway('sys_tool_file_collection', {
|
|
3192
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3193
|
+
chatId: chatId1,
|
|
3194
|
+
query: 'NonexistentFile12345',
|
|
3195
|
+
userMessage: 'Search for nonexistent file'
|
|
3196
|
+
});
|
|
3197
|
+
const parsed16 = JSON.parse(result16);
|
|
3198
|
+
t.is(parsed16.success, true);
|
|
3199
|
+
t.is(parsed16.count, 0, 'Should find no files');
|
|
3200
|
+
t.true(parsed16.message.includes('No files found'), 'Should have helpful message');
|
|
3201
|
+
|
|
3202
|
+
// Test 17: Search case-insensitive
|
|
3203
|
+
const result17 = await callPathway('sys_tool_file_collection', {
|
|
3204
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3205
|
+
chatId: chatId1,
|
|
3206
|
+
query: 'ANNUAL REPORT',
|
|
3207
|
+
userMessage: 'Search case-insensitive'
|
|
3208
|
+
});
|
|
3209
|
+
const parsed17 = JSON.parse(result17);
|
|
3210
|
+
t.is(parsed17.success, true);
|
|
3211
|
+
t.is(parsed17.count, 1, 'Should find file with case-insensitive match');
|
|
3212
|
+
t.is(parsed17.files[0].displayFilename, 'Annual Report 2024.pdf');
|
|
3213
|
+
|
|
3214
|
+
// Test 18: Search from chat-2 (different chat)
|
|
3215
|
+
const result18 = await callPathway('sys_tool_file_collection', {
|
|
3216
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3217
|
+
chatId: chatId2,
|
|
3218
|
+
query: 'Product',
|
|
3219
|
+
userMessage: 'Search for Product in chat-2'
|
|
3220
|
+
});
|
|
3221
|
+
const parsed18 = JSON.parse(result18);
|
|
3222
|
+
t.is(parsed18.success, true);
|
|
3223
|
+
t.is(parsed18.count, 2, 'Should find 2 product files in chat-2');
|
|
3224
|
+
const filenames18 = parsed18.files.map(f => f.displayFilename);
|
|
3225
|
+
t.true(filenames18.includes('Product Presentation.pdf'));
|
|
3226
|
+
t.true(filenames18.includes('Product Roadmap.xlsx'));
|
|
3227
|
+
|
|
3228
|
+
// Test 19: Search from chat-2 should NOT see chat-1 files (without includeAllChats)
|
|
3229
|
+
const result19 = await callPathway('sys_tool_file_collection', {
|
|
3230
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3231
|
+
chatId: chatId2,
|
|
3232
|
+
query: 'Report',
|
|
3233
|
+
userMessage: 'Search for Report in chat-2 (should not find chat-1 file)'
|
|
3234
|
+
});
|
|
3235
|
+
const parsed19 = JSON.parse(result19);
|
|
3236
|
+
t.is(parsed19.success, true);
|
|
3237
|
+
// Should find global files with "Report" but not chat-1 specific files
|
|
3238
|
+
// Actually, there are no global files with "Report", so should be 0
|
|
3239
|
+
t.is(parsed19.count, 0, 'Should not find chat-1 files from chat-2');
|
|
3240
|
+
|
|
3241
|
+
// Test 20: Search for files matching multiple criteria (query + tags + notes)
|
|
3242
|
+
const result20 = await callPathway('sys_tool_file_collection', {
|
|
3243
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
3244
|
+
chatId: chatId1,
|
|
3245
|
+
query: 'budget',
|
|
3246
|
+
tags: ['finance'],
|
|
3247
|
+
userMessage: 'Search for budget with finance tag'
|
|
3248
|
+
});
|
|
3249
|
+
const parsed20 = JSON.parse(result20);
|
|
3250
|
+
t.is(parsed20.success, true);
|
|
3251
|
+
t.is(parsed20.count, 1, 'Should find file matching query and tag');
|
|
3252
|
+
t.is(parsed20.files[0].displayFilename, 'Budget Q1.xlsx');
|
|
3253
|
+
|
|
3254
|
+
} finally {
|
|
3255
|
+
await cleanup(contextId);
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
|
|
3259
|
+
test('File collection: SearchFileCollection with compound context (user + workspace)', async t => {
|
|
3260
|
+
const userContextId = `test-user-search-${Date.now()}`;
|
|
3261
|
+
const workspaceContextId = `test-workspace-search-${Date.now()}`;
|
|
3262
|
+
const chatId = `test-chat-${Date.now()}`;
|
|
3263
|
+
|
|
3264
|
+
try {
|
|
3265
|
+
const { addFileToCollection } = await import('../../../../lib/fileUtils.js');
|
|
3266
|
+
|
|
3267
|
+
// Add files to user context
|
|
3268
|
+
await addFileToCollection(userContextId, null, 'https://example.com/user-doc.pdf', null, 'User Document.pdf', ['personal'], 'Personal document', 'hash-user-doc', null, null, false, chatId);
|
|
3269
|
+
await addFileToCollection(userContextId, null, 'https://example.com/user-notes.txt', null, 'User Notes.txt', ['personal', 'notes'], 'Personal notes', 'hash-user-notes', null, null, false, chatId);
|
|
3270
|
+
|
|
3271
|
+
// Add files to workspace context
|
|
3272
|
+
await addFileToCollection(workspaceContextId, null, 'https://example.com/workspace-doc.pdf', null, 'Workspace Document.pdf', ['workspace'], 'Workspace document', 'hash-workspace-doc', null, null, false, chatId);
|
|
3273
|
+
await addFileToCollection(workspaceContextId, null, 'https://example.com/workspace-data.xlsx', null, 'Workspace Data.xlsx', ['workspace', 'data'], 'Workspace data', 'hash-workspace-data', null, null, false, chatId);
|
|
3274
|
+
|
|
3275
|
+
// Define compound agentContext
|
|
3276
|
+
const agentContext = [
|
|
3277
|
+
{ contextId: userContextId, contextKey: null, default: true },
|
|
3278
|
+
{ contextId: workspaceContextId, contextKey: null, default: false }
|
|
3279
|
+
];
|
|
3280
|
+
|
|
3281
|
+
// Test 1: Search across both contexts
|
|
3282
|
+
const result1 = await callPathway('sys_tool_file_collection', {
|
|
3283
|
+
agentContext: agentContext,
|
|
3284
|
+
chatId: chatId,
|
|
3285
|
+
query: 'Document',
|
|
3286
|
+
userMessage: 'Search for Document across user and workspace contexts'
|
|
3287
|
+
});
|
|
3288
|
+
const parsed1 = JSON.parse(result1);
|
|
3289
|
+
t.is(parsed1.success, true);
|
|
3290
|
+
t.is(parsed1.count, 2, 'Should find documents from both contexts');
|
|
3291
|
+
const filenames1 = parsed1.files.map(f => f.displayFilename);
|
|
3292
|
+
t.true(filenames1.includes('User Document.pdf'));
|
|
3293
|
+
t.true(filenames1.includes('Workspace Document.pdf'));
|
|
3294
|
+
|
|
3295
|
+
// Test 2: Search by tag across both contexts
|
|
3296
|
+
const result2 = await callPathway('sys_tool_file_collection', {
|
|
3297
|
+
agentContext: agentContext,
|
|
3298
|
+
chatId: chatId,
|
|
3299
|
+
query: 'personal',
|
|
3300
|
+
userMessage: 'Search by personal tag across contexts'
|
|
3301
|
+
});
|
|
3302
|
+
const parsed2 = JSON.parse(result2);
|
|
3303
|
+
t.is(parsed2.success, true);
|
|
3304
|
+
t.is(parsed2.count, 2, 'Should find 2 files with personal tag from user context');
|
|
3305
|
+
const filenames2 = parsed2.files.map(f => f.displayFilename);
|
|
3306
|
+
t.true(filenames2.includes('User Document.pdf'));
|
|
3307
|
+
t.true(filenames2.includes('User Notes.txt'));
|
|
3308
|
+
|
|
3309
|
+
// Test 3: Search by tag with includeAllChats (should still work with compound context)
|
|
3310
|
+
const result3 = await callPathway('sys_tool_file_collection', {
|
|
3311
|
+
agentContext: agentContext,
|
|
3312
|
+
chatId: chatId,
|
|
3313
|
+
query: 'workspace',
|
|
3314
|
+
includeAllChats: true,
|
|
3315
|
+
userMessage: 'Search by workspace tag across all chats'
|
|
3316
|
+
});
|
|
3317
|
+
const parsed3 = JSON.parse(result3);
|
|
3318
|
+
t.is(parsed3.success, true);
|
|
3319
|
+
t.is(parsed3.count, 2, 'Should find 2 workspace files');
|
|
3320
|
+
const filenames3 = parsed3.files.map(f => f.displayFilename);
|
|
3321
|
+
t.true(filenames3.includes('Workspace Document.pdf'));
|
|
3322
|
+
t.true(filenames3.includes('Workspace Data.xlsx'));
|
|
3323
|
+
|
|
3324
|
+
// Test 4: Search with tag filter across compound context
|
|
3325
|
+
const result4 = await callPathway('sys_tool_file_collection', {
|
|
3326
|
+
agentContext: agentContext,
|
|
3327
|
+
chatId: chatId,
|
|
3328
|
+
query: 'Notes',
|
|
3329
|
+
tags: ['personal'],
|
|
3330
|
+
userMessage: 'Search for Notes with personal tag filter'
|
|
3331
|
+
});
|
|
3332
|
+
const parsed4 = JSON.parse(result4);
|
|
3333
|
+
t.is(parsed4.success, true);
|
|
3334
|
+
t.is(parsed4.count, 1, 'Should find 1 file matching query and tag');
|
|
3335
|
+
t.is(parsed4.files[0].displayFilename, 'User Notes.txt');
|
|
3336
|
+
|
|
3337
|
+
} finally {
|
|
3338
|
+
const { getRedisClient } = await import('../../../../lib/fileUtils.js');
|
|
3339
|
+
const redisClient = await getRedisClient();
|
|
3340
|
+
if (redisClient) {
|
|
3341
|
+
await redisClient.del(`FileStoreMap:ctx:${userContextId}`);
|
|
3342
|
+
await redisClient.del(`FileStoreMap:ctx:${workspaceContextId}`);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
});
|