@aj-archipelago/cortex 1.3.57 → 1.3.59

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 (47) hide show
  1. package/README.md +6 -0
  2. package/config.js +22 -0
  3. package/helper-apps/cortex-file-handler/INTERFACE.md +20 -9
  4. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  5. package/helper-apps/cortex-file-handler/package.json +1 -1
  6. package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +17 -17
  7. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +35 -35
  8. package/helper-apps/cortex-file-handler/src/blobHandler.js +1010 -909
  9. package/helper-apps/cortex-file-handler/src/constants.js +98 -98
  10. package/helper-apps/cortex-file-handler/src/docHelper.js +27 -27
  11. package/helper-apps/cortex-file-handler/src/fileChunker.js +224 -214
  12. package/helper-apps/cortex-file-handler/src/helper.js +93 -93
  13. package/helper-apps/cortex-file-handler/src/index.js +584 -550
  14. package/helper-apps/cortex-file-handler/src/localFileHandler.js +86 -86
  15. package/helper-apps/cortex-file-handler/src/redis.js +186 -90
  16. package/helper-apps/cortex-file-handler/src/services/ConversionService.js +301 -273
  17. package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +55 -55
  18. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +174 -154
  19. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +239 -223
  20. package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +161 -159
  21. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +73 -71
  22. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +46 -45
  23. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +256 -213
  24. package/helper-apps/cortex-file-handler/src/start.js +4 -1
  25. package/helper-apps/cortex-file-handler/src/utils/filenameUtils.js +59 -25
  26. package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +119 -116
  27. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +257 -257
  28. package/helper-apps/cortex-file-handler/tests/cleanup.test.js +676 -0
  29. package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +124 -124
  30. package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +249 -208
  31. package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +439 -380
  32. package/helper-apps/cortex-file-handler/tests/getOperations.test.js +299 -263
  33. package/helper-apps/cortex-file-handler/tests/postOperations.test.js +265 -239
  34. package/helper-apps/cortex-file-handler/tests/start.test.js +1230 -1201
  35. package/helper-apps/cortex-file-handler/tests/storage/AzureStorageProvider.test.js +110 -105
  36. package/helper-apps/cortex-file-handler/tests/storage/GCSStorageProvider.test.js +201 -175
  37. package/helper-apps/cortex-file-handler/tests/storage/LocalStorageProvider.test.js +128 -125
  38. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +78 -73
  39. package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +99 -99
  40. package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -70
  41. package/package.json +1 -1
  42. package/pathways/translate_apptek.js +33 -0
  43. package/pathways/translate_subtitle.js +15 -8
  44. package/server/plugins/apptekTranslatePlugin.js +46 -91
  45. package/tests/apptekTranslatePlugin.test.js +0 -2
  46. package/tests/integration/apptekTranslatePlugin.integration.test.js +159 -93
  47. package/tests/translate_apptek.test.js +16 -0
@@ -0,0 +1,676 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ import test from "ava";
6
+ import axios from "axios";
7
+
8
+ import { uploadBlob } from "../src/blobHandler.js";
9
+ import { urlExists } from "../src/helper.js";
10
+ import {
11
+ setFileStoreMap,
12
+ getFileStoreMap,
13
+ removeFromFileStoreMap,
14
+ cleanupRedisFileStoreMapAge,
15
+ } from "../src/redis.js";
16
+ import { StorageService } from "../src/services/storage/StorageService.js";
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+
20
+ // Helper function to determine if we should use local storage
21
+ function shouldUseLocalStorage() {
22
+ // Use local storage if Azure is not configured
23
+ const useLocal = !process.env.AZURE_STORAGE_CONNECTION_STRING;
24
+ console.log(
25
+ `Debug - AZURE_STORAGE_CONNECTION_STRING: ${process.env.AZURE_STORAGE_CONNECTION_STRING ? "SET" : "NOT SET"}`,
26
+ );
27
+ console.log(`Debug - shouldUseLocalStorage(): ${useLocal}`);
28
+ return useLocal;
29
+ }
30
+ const __dirname = path.dirname(__filename);
31
+
32
+ const baseUrl = "http://localhost:7072/api/CortexFileHandler";
33
+
34
+ // Helper function to create a test file
35
+ async function createTestFile(content, extension = "txt") {
36
+ const tempDir = path.join(__dirname, "temp");
37
+ if (!fs.existsSync(tempDir)) {
38
+ fs.mkdirSync(tempDir, { recursive: true });
39
+ }
40
+ const filename = `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${extension}`;
41
+ const filePath = path.join(tempDir, filename);
42
+ fs.writeFileSync(filePath, content);
43
+ return filePath;
44
+ }
45
+
46
+ // Helper function to clean up test files
47
+ function cleanupTestFile(filePath) {
48
+ try {
49
+ if (filePath && fs.existsSync(filePath)) {
50
+ fs.unlinkSync(filePath);
51
+ }
52
+ } catch (error) {
53
+ // Ignore cleanup errors
54
+ }
55
+ }
56
+
57
+ // Helper function to create an old timestamp
58
+ function createOldTimestamp(daysOld = 8) {
59
+ const oldDate = new Date();
60
+ oldDate.setDate(oldDate.getDate() - daysOld);
61
+ return oldDate.toISOString();
62
+ }
63
+
64
+ // Helper function to get requestId from upload result
65
+ function getRequestIdFromUploadResult(uploadResult) {
66
+ // Extract requestId from the URL or use a fallback
67
+ if (uploadResult.url) {
68
+ const urlParts = uploadResult.url.split("/");
69
+ const filename = urlParts[urlParts.length - 1];
70
+ // The requestId is the full filename without extension
71
+ const requestId = filename.replace(/\.[^/.]+$/, "");
72
+ console.log(`Extracted requestId: ${requestId} from filename: ${filename}`);
73
+ return requestId;
74
+ }
75
+ return uploadResult.hash || "test-request-id";
76
+ }
77
+
78
+ test.before(async () => {
79
+ // Ensure Redis is connected
80
+ const { connectClient } = await import("../src/redis.js");
81
+ await connectClient();
82
+ });
83
+
84
+ test.after(async () => {
85
+ // Clean up any remaining test entries
86
+ const testKeys = [
87
+ "test-lazy-cleanup",
88
+ "test-age-cleanup",
89
+ "test-old-entry",
90
+ "test-missing-file",
91
+ "test-gcs-backup",
92
+ "test-recent-entry",
93
+ "test-skip-lazy-cleanup",
94
+ "test-no-timestamp",
95
+ "test-malformed",
96
+ "test-checkhash-error",
97
+ ];
98
+ for (const key of testKeys) {
99
+ try {
100
+ await removeFromFileStoreMap(key);
101
+ } catch (error) {
102
+ // Ignore errors
103
+ }
104
+ }
105
+
106
+ // Clean up any remaining test files in src/files
107
+ try {
108
+ const fs = await import("fs");
109
+ const path = await import("path");
110
+ const publicFolder = path.join(process.cwd(), "src", "files");
111
+
112
+ if (fs.existsSync(publicFolder)) {
113
+ const entries = fs.readdirSync(publicFolder);
114
+ for (const entry of entries) {
115
+ const entryPath = path.join(publicFolder, entry);
116
+ const stat = fs.statSync(entryPath);
117
+
118
+ // Only clean up directories that look like test files (LLM-friendly IDs)
119
+ if (stat.isDirectory() && /^[a-z0-9]+-[a-z0-9]+$/.test(entry)) {
120
+ console.log(`Cleaning up test directory: ${entry}`);
121
+ fs.rmSync(entryPath, { recursive: true, force: true });
122
+ }
123
+ }
124
+ }
125
+ } catch (error) {
126
+ console.error("Error cleaning up test files:", error);
127
+ }
128
+ });
129
+
130
+ test("lazy cleanup should remove cache entry when file is missing", async (t) => {
131
+ // Create a test file and upload it
132
+ const testFile = await createTestFile("Test content for lazy cleanup");
133
+
134
+ try {
135
+ const context = { log: console.log };
136
+ const uploadResult = await uploadBlob(
137
+ context,
138
+ null,
139
+ shouldUseLocalStorage(),
140
+ testFile,
141
+ ); // Use appropriate storage
142
+
143
+ // Store the hash in Redis
144
+ const hash = "test-lazy-cleanup";
145
+ await setFileStoreMap(hash, uploadResult);
146
+
147
+ // Verify the entry exists (with skipLazyCleanup to avoid interference)
148
+ const initialResult = await getFileStoreMap(hash, true);
149
+ t.truthy(initialResult, "Cache entry should exist initially");
150
+ t.is(
151
+ initialResult.url,
152
+ uploadResult.url,
153
+ "Cache entry should have correct URL",
154
+ );
155
+
156
+ // Delete the actual file from storage using the correct requestId
157
+ const requestId = getRequestIdFromUploadResult(uploadResult);
158
+ console.log(`Attempting to delete file with requestId: ${requestId}`);
159
+
160
+ // First verify the file exists
161
+ const fileExistsBeforeDelete = await urlExists(uploadResult.url);
162
+ t.true(fileExistsBeforeDelete.valid, "File should exist before deletion");
163
+
164
+ const deleteResponse = await axios.delete(
165
+ `${baseUrl}?operation=delete&requestId=${requestId}`,
166
+ { validateStatus: () => true },
167
+ );
168
+ console.log(
169
+ `Delete response status: ${deleteResponse.status}, body:`,
170
+ deleteResponse.data,
171
+ );
172
+ t.is(deleteResponse.status, 200, "File deletion should succeed");
173
+
174
+ // Wait a moment for deletion to complete
175
+ await new Promise((resolve) => setTimeout(resolve, 1000));
176
+
177
+ // After deletion, check all storages using StorageService
178
+ const storageService = new StorageService();
179
+ const azureExists = uploadResult.url
180
+ ? await storageService.fileExists(uploadResult.url)
181
+ : false;
182
+ const azureGone = !azureExists;
183
+ const gcsExists = uploadResult.gcs
184
+ ? await storageService.fileExists(uploadResult.gcs)
185
+ : false;
186
+ const gcsGone = !gcsExists;
187
+ console.log(`Debug - uploadResult.url: ${uploadResult.url}`);
188
+ console.log(`Debug - uploadResult.gcs: ${uploadResult.gcs}`);
189
+ console.log(`Debug - azureExists: ${azureExists}, azureGone: ${azureGone}`);
190
+ console.log(`Debug - gcsExists: ${gcsExists}, gcsGone: ${gcsGone}`);
191
+ t.true(azureGone && gcsGone, "File should be deleted from all storages");
192
+
193
+ // Now call getFileStoreMap - lazy cleanup should remove the entry
194
+ const resultAfterCleanup = await getFileStoreMap(hash);
195
+ t.is(
196
+ resultAfterCleanup,
197
+ null,
198
+ "Lazy cleanup should remove cache entry for missing file",
199
+ );
200
+ } finally {
201
+ cleanupTestFile(testFile);
202
+ }
203
+ });
204
+
205
+ test("lazy cleanup should keep cache entry when GCS backup exists", async (t) => {
206
+ // This test requires GCS to be configured
207
+ if (!process.env.GOOGLE_CLOUD_STORAGE_BUCKET) {
208
+ t.pass("Skipping test - GCS not configured");
209
+ return;
210
+ }
211
+
212
+ const testFile = await createTestFile("Test content for GCS backup test");
213
+
214
+ try {
215
+ const context = { log: console.log };
216
+ const uploadResult = await uploadBlob(
217
+ context,
218
+ null,
219
+ shouldUseLocalStorage(),
220
+ testFile,
221
+ ); // Use appropriate storage
222
+
223
+ // Verify GCS backup exists
224
+ t.truthy(uploadResult.gcs, "Should have GCS backup URL");
225
+
226
+ // Store the hash in Redis
227
+ const hash = "test-gcs-backup";
228
+ await setFileStoreMap(hash, uploadResult);
229
+
230
+ // Verify the entry exists initially
231
+ const initialResult = await getFileStoreMap(hash, true);
232
+ t.truthy(initialResult, "Cache entry should exist initially");
233
+
234
+ // Delete the primary file but keep GCS backup
235
+ const requestId = getRequestIdFromUploadResult(uploadResult);
236
+ const deleteResponse = await axios.delete(
237
+ `${baseUrl}?operation=delete&requestId=${requestId}`,
238
+ { validateStatus: () => true },
239
+ );
240
+ t.is(deleteResponse.status, 200, "File deletion should succeed");
241
+
242
+ // Wait a moment for deletion to complete
243
+ await new Promise((resolve) => setTimeout(resolve, 1000));
244
+
245
+ // After deletion, check all storages using StorageService
246
+ const storageService = new StorageService();
247
+ const azureExists = uploadResult.url
248
+ ? await storageService.fileExists(uploadResult.url)
249
+ : false;
250
+ const azureGone = !azureExists;
251
+ const gcsExists = uploadResult.gcs
252
+ ? await storageService.fileExists(uploadResult.gcs)
253
+ : false;
254
+ const gcsGone = !gcsExists;
255
+ console.log(`Debug - uploadResult.url: ${uploadResult.url}`);
256
+ console.log(`Debug - uploadResult.gcs: ${uploadResult.gcs}`);
257
+ console.log(`Debug - azureExists: ${azureExists}, azureGone: ${azureGone}`);
258
+ console.log(`Debug - gcsExists: ${gcsExists}, gcsGone: ${gcsGone}`);
259
+ t.true(
260
+ azureGone && gcsGone,
261
+ "Primary file should be deleted from all storages",
262
+ );
263
+
264
+ // Now call getFileStoreMap - lazy cleanup should keep the entry because GCS backup exists
265
+ const resultAfterCleanup = await getFileStoreMap(hash);
266
+ t.truthy(
267
+ resultAfterCleanup,
268
+ "Lazy cleanup should keep cache entry when GCS backup exists",
269
+ );
270
+ t.is(
271
+ resultAfterCleanup.gcs,
272
+ uploadResult.gcs,
273
+ "GCS backup URL should be preserved",
274
+ );
275
+ } finally {
276
+ cleanupTestFile(testFile);
277
+ }
278
+ });
279
+
280
+ test("age-based cleanup should remove old entries", async (t) => {
281
+ // Create a test file and upload it
282
+ const testFile = await createTestFile("Test content for age cleanup");
283
+
284
+ try {
285
+ const context = { log: console.log };
286
+ const uploadResult = await uploadBlob(context, null, true, testFile); // Use local storage
287
+
288
+ // Store the hash in Redis with an old timestamp
289
+ const hash = "test-old-entry";
290
+ const oldEntry = {
291
+ ...uploadResult,
292
+ timestamp: createOldTimestamp(8), // 8 days old
293
+ };
294
+ console.log(`Storing old entry with timestamp: ${oldEntry.timestamp}`);
295
+ await setFileStoreMap(hash, oldEntry);
296
+
297
+ // Verify it exists initially (with skipLazyCleanup to avoid interference)
298
+ const initialResult = await getFileStoreMap(hash, true);
299
+ t.truthy(initialResult, "Old entry should exist initially");
300
+
301
+ // Run age-based cleanup with 7-day threshold
302
+ const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
303
+ console.log(
304
+ `Age cleanup returned ${cleaned.length} entries:`,
305
+ cleaned.map((c) => c.hash),
306
+ );
307
+
308
+ // Verify the old entry was cleaned up
309
+ t.true(cleaned.length > 0, "Should have cleaned up some entries");
310
+ const cleanedHash = cleaned.find(
311
+ (entry) => entry.hash === "test-old-entry",
312
+ );
313
+ t.truthy(cleanedHash, "Old entry should be in cleaned list");
314
+
315
+ // Verify the entry is gone from cache (with skipLazyCleanup to avoid interference)
316
+ const resultAfterCleanup = await getFileStoreMap(hash, true);
317
+ t.is(resultAfterCleanup, null, "Old entry should be removed from cache");
318
+ } finally {
319
+ cleanupTestFile(testFile);
320
+ }
321
+ });
322
+
323
+ test("age-based cleanup should keep recent entries", async (t) => {
324
+ // Create a test file and upload it
325
+ const testFile = await createTestFile("Test content for recent entry test");
326
+
327
+ try {
328
+ const context = { log: console.log };
329
+ const uploadResult = await uploadBlob(
330
+ context,
331
+ null,
332
+ shouldUseLocalStorage(),
333
+ testFile,
334
+ ); // Use appropriate storage
335
+
336
+ // Store the hash in Redis with a recent timestamp
337
+ const hash = "test-recent-entry";
338
+ const recentEntry = {
339
+ ...uploadResult,
340
+ timestamp: new Date().toISOString(), // Current timestamp
341
+ };
342
+ await setFileStoreMap(hash, recentEntry);
343
+
344
+ // Verify it exists initially (with skipLazyCleanup to avoid interference)
345
+ const initialResult = await getFileStoreMap(hash, true);
346
+ t.truthy(initialResult, "Recent entry should exist initially");
347
+
348
+ // Run age-based cleanup with 7-day threshold
349
+ const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
350
+
351
+ // Verify the recent entry was NOT cleaned up
352
+ const cleanedHash = cleaned.find(
353
+ (entry) => entry.hash === "test-recent-entry",
354
+ );
355
+ t.falsy(cleanedHash, "Recent entry should not be in cleaned list");
356
+
357
+ // Verify the entry still exists in cache
358
+ const resultAfterCleanup = await getFileStoreMap(hash);
359
+ t.truthy(resultAfterCleanup, "Recent entry should still exist in cache");
360
+
361
+ // Clean up
362
+ await removeFromFileStoreMap("test-recent-entry");
363
+ } finally {
364
+ cleanupTestFile(testFile);
365
+ }
366
+ });
367
+
368
+ test("age-based cleanup should respect maxEntriesToCheck limit", async (t) => {
369
+ // Create multiple test files and upload them
370
+ const testFiles = [];
371
+ const oldEntries = [];
372
+
373
+ try {
374
+ // Create 15 test files
375
+ for (let i = 0; i < 15; i++) {
376
+ const testFile = await createTestFile(
377
+ `Test content for age cleanup ${i}`,
378
+ );
379
+ testFiles.push(testFile);
380
+
381
+ const context = { log: console.log };
382
+ const uploadResult = await uploadBlob(
383
+ context,
384
+ null,
385
+ shouldUseLocalStorage(),
386
+ testFile,
387
+ ); // Use appropriate storage
388
+
389
+ // Store with old timestamp
390
+ const hash = `test-old-entry-${i}`;
391
+ const oldEntry = {
392
+ ...uploadResult,
393
+ timestamp: createOldTimestamp(8), // 8 days old
394
+ };
395
+ oldEntries.push(oldEntry);
396
+ await setFileStoreMap(hash, oldEntry);
397
+ }
398
+
399
+ // Run age-based cleanup with limit of 5 entries
400
+ const cleaned = await cleanupRedisFileStoreMapAge(7, 5);
401
+ console.log(
402
+ `Age cleanup with limit returned ${cleaned.length} entries:`,
403
+ cleaned.map((c) => c.hash),
404
+ );
405
+
406
+ // Should only clean up 5 entries due to the limit
407
+ t.is(cleaned.length, 5, "Should only clean up 5 entries due to limit");
408
+
409
+ // Verify some entries are still there (with skipLazyCleanup to avoid interference)
410
+ const remainingEntry = await getFileStoreMap("test-old-entry-5", true);
411
+ t.truthy(remainingEntry, "Some old entries should still exist");
412
+ } finally {
413
+ // Clean up test files
414
+ for (const testFile of testFiles) {
415
+ cleanupTestFile(testFile);
416
+ }
417
+
418
+ // Clean up remaining entries
419
+ for (let i = 0; i < 15; i++) {
420
+ await removeFromFileStoreMap(`test-old-entry-${i}`);
421
+ }
422
+ }
423
+ });
424
+
425
+ test("getFileStoreMap with skipLazyCleanup should not perform cleanup", async (t) => {
426
+ // Create a test file and upload it
427
+ const testFile = await createTestFile(
428
+ "Test content for skipLazyCleanup test",
429
+ );
430
+
431
+ try {
432
+ const context = { log: console.log };
433
+ const uploadResult = await uploadBlob(
434
+ context,
435
+ null,
436
+ shouldUseLocalStorage(),
437
+ testFile,
438
+ ); // Use appropriate storage
439
+
440
+ // Store the hash in Redis
441
+ const hash = "test-skip-lazy-cleanup";
442
+ await setFileStoreMap(hash, uploadResult);
443
+
444
+ // Verify the entry exists initially
445
+ const initialResult = await getFileStoreMap(hash, true);
446
+ t.truthy(initialResult, "Cache entry should exist initially");
447
+
448
+ // Delete the actual file from storage
449
+ const requestId = getRequestIdFromUploadResult(uploadResult);
450
+ const deleteResponse = await axios.delete(
451
+ `${baseUrl}?operation=delete&requestId=${requestId}`,
452
+ { validateStatus: () => true },
453
+ );
454
+ t.is(deleteResponse.status, 200, "File deletion should succeed");
455
+
456
+ // Wait a moment for deletion to complete
457
+ await new Promise((resolve) => setTimeout(resolve, 1000));
458
+
459
+ // After deletion, check all storages using StorageService
460
+ const storageService = new StorageService();
461
+ const azureExists = uploadResult.url
462
+ ? await storageService.fileExists(uploadResult.url)
463
+ : false;
464
+ const azureGone = !azureExists;
465
+ const gcsExists = uploadResult.gcs
466
+ ? await storageService.fileExists(uploadResult.gcs)
467
+ : false;
468
+ const gcsGone = !gcsExists;
469
+ console.log(`Debug - uploadResult.url: ${uploadResult.url}`);
470
+ console.log(`Debug - uploadResult.gcs: ${uploadResult.gcs}`);
471
+ console.log(`Debug - azureExists: ${azureExists}, azureGone: ${azureGone}`);
472
+ console.log(`Debug - gcsExists: ${gcsExists}, gcsGone: ${gcsGone}`);
473
+ t.true(azureGone && gcsGone, "File should be deleted from all storages");
474
+
475
+ // Call getFileStoreMap with skipLazyCleanup=true - should NOT remove the entry
476
+ const resultWithSkip = await getFileStoreMap(hash, true);
477
+ t.truthy(
478
+ resultWithSkip,
479
+ "Entry should still exist when skipLazyCleanup=true",
480
+ );
481
+
482
+ // Call getFileStoreMap without skipLazyCleanup - should remove the entry
483
+ const resultWithoutSkip = await getFileStoreMap(hash, false);
484
+ t.is(
485
+ resultWithoutSkip,
486
+ null,
487
+ "Entry should be removed when skipLazyCleanup=false",
488
+ );
489
+ } finally {
490
+ cleanupTestFile(testFile);
491
+ }
492
+ });
493
+
494
+ test("cleanup should handle entries without timestamps gracefully", async (t) => {
495
+ // Create a test file and upload it
496
+ const testFile = await createTestFile("Test content for no timestamp test");
497
+
498
+ try {
499
+ const context = { log: console.log };
500
+ const uploadResult = await uploadBlob(
501
+ context,
502
+ null,
503
+ shouldUseLocalStorage(),
504
+ testFile,
505
+ ); // Use appropriate storage
506
+
507
+ // Store the hash in Redis without timestamp
508
+ const hash = "test-no-timestamp";
509
+ const { timestamp, ...entryWithoutTimestamp } = uploadResult;
510
+ console.log(`Storing entry without timestamp:`, entryWithoutTimestamp);
511
+
512
+ // Store directly in Redis to avoid timestamp being added
513
+ const redis = await import("ioredis");
514
+ const connectionString = process.env["REDIS_CONNECTION_STRING"];
515
+ const client = redis.default.createClient(connectionString);
516
+
517
+ try {
518
+ if (client.status !== "ready" && client.status !== "connecting") {
519
+ await client.connect();
520
+ }
521
+ await client.hset(
522
+ "FileStoreMap",
523
+ hash,
524
+ JSON.stringify(entryWithoutTimestamp),
525
+ );
526
+ } finally {
527
+ await client.disconnect();
528
+ }
529
+
530
+ // Verify it exists initially
531
+ const initialResult = await getFileStoreMap(hash, true);
532
+ t.truthy(initialResult, "Entry without timestamp should exist initially");
533
+ t.falsy(initialResult.timestamp, "Entry should not have timestamp");
534
+
535
+ // Run age-based cleanup - should not crash
536
+ const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
537
+
538
+ // Entry without timestamp should not be cleaned up
539
+ const cleanedHash = cleaned.find(
540
+ (entry) => entry.hash === "test-no-timestamp",
541
+ );
542
+ t.falsy(cleanedHash, "Entry without timestamp should not be cleaned up");
543
+
544
+ // Verify the entry still exists
545
+ const resultAfterCleanup = await getFileStoreMap(hash, true);
546
+ t.truthy(resultAfterCleanup, "Entry without timestamp should still exist");
547
+ } finally {
548
+ cleanupTestFile(testFile);
549
+ }
550
+ });
551
+
552
+ test("cleanup should handle malformed entries gracefully", async (t) => {
553
+ // Create a test file and upload it
554
+ const testFile = await createTestFile(
555
+ "Test content for malformed entry test",
556
+ );
557
+
558
+ try {
559
+ const context = { log: console.log };
560
+ const uploadResult = await uploadBlob(
561
+ context,
562
+ null,
563
+ shouldUseLocalStorage(),
564
+ testFile,
565
+ ); // Use appropriate storage
566
+
567
+ // Store the hash in Redis with malformed data
568
+ const malformedKey = "test-malformed";
569
+ const redis = await import("ioredis");
570
+ const connectionString = process.env["REDIS_CONNECTION_STRING"];
571
+ const client = redis.default.createClient(connectionString);
572
+
573
+ try {
574
+ // Don't try to connect if already connected
575
+ if (client.status !== "ready" && client.status !== "connecting") {
576
+ await client.connect();
577
+ }
578
+ await client.hset("FileStoreMap", malformedKey, "this is not json");
579
+
580
+ // Verify malformed entry exists
581
+ const initialResult = await getFileStoreMap(malformedKey, true);
582
+ t.truthy(initialResult, "Malformed entry should exist initially");
583
+
584
+ // Run age-based cleanup - should not crash
585
+ const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
586
+
587
+ // Malformed entry should not be cleaned up (no timestamp)
588
+ const cleanedHash = cleaned.find(
589
+ (entry) => entry.hash === "test-malformed",
590
+ );
591
+ t.falsy(cleanedHash, "Malformed entry should not be cleaned up");
592
+
593
+ // Verify the entry still exists
594
+ const resultAfterCleanup = await getFileStoreMap(malformedKey, true);
595
+ t.truthy(resultAfterCleanup, "Malformed entry should still exist");
596
+ } finally {
597
+ await removeFromFileStoreMap(malformedKey);
598
+ await client.disconnect();
599
+ }
600
+ } finally {
601
+ cleanupTestFile(testFile);
602
+ }
603
+ });
604
+
605
+ test("checkHash operation should provide correct error message when files are missing", async (t) => {
606
+ // Create a test file and upload it
607
+ const testFile = await createTestFile(
608
+ "Test content for checkHash error test",
609
+ );
610
+
611
+ try {
612
+ const context = { log: console.log };
613
+ const uploadResult = await uploadBlob(
614
+ context,
615
+ null,
616
+ shouldUseLocalStorage(),
617
+ testFile,
618
+ ); // Use appropriate storage
619
+
620
+ // Store the hash in Redis
621
+ const hash = "test-checkhash-error";
622
+ await setFileStoreMap(hash, uploadResult);
623
+
624
+ // Verify the entry exists initially
625
+ const initialResult = await getFileStoreMap(hash, true);
626
+ t.truthy(initialResult, "Cache entry should exist initially");
627
+
628
+ // Delete the actual file from storage
629
+ const requestId = getRequestIdFromUploadResult(uploadResult);
630
+ const deleteResponse = await axios.delete(
631
+ `${baseUrl}?operation=delete&requestId=${requestId}`,
632
+ { validateStatus: () => true },
633
+ );
634
+ t.is(deleteResponse.status, 200, "File deletion should succeed");
635
+
636
+ // Wait a moment for deletion to complete
637
+ await new Promise((resolve) => setTimeout(resolve, 1000));
638
+
639
+ // After deletion, check all storages using StorageService
640
+ const storageService = new StorageService();
641
+ const azureExists = uploadResult.url
642
+ ? await storageService.fileExists(uploadResult.url)
643
+ : false;
644
+ const azureGone = !azureExists;
645
+ const gcsExists = uploadResult.gcs
646
+ ? await storageService.fileExists(uploadResult.gcs)
647
+ : false;
648
+ const gcsGone = !gcsExists;
649
+ console.log(`Debug - uploadResult.url: ${uploadResult.url}`);
650
+ console.log(`Debug - uploadResult.gcs: ${uploadResult.gcs}`);
651
+ console.log(`Debug - azureExists: ${azureExists}, azureGone: ${azureGone}`);
652
+ console.log(`Debug - gcsExists: ${gcsExists}, gcsGone: ${gcsGone}`);
653
+ t.true(azureGone && gcsGone, "File should be deleted from all storages");
654
+
655
+ // Now test checkHash operation - should return 404 with appropriate message
656
+ const checkHashResponse = await axios.get(
657
+ `${baseUrl}?hash=${hash}&checkHash=true`,
658
+ { validateStatus: () => true },
659
+ );
660
+
661
+ t.is(
662
+ checkHashResponse.status,
663
+ 404,
664
+ "checkHash should return 404 for missing file",
665
+ );
666
+ t.truthy(checkHashResponse.data, "checkHash should return error message");
667
+ t.true(
668
+ checkHashResponse.data.includes("not found") ||
669
+ checkHashResponse.data.includes("Hash") ||
670
+ checkHashResponse.data.includes("404"),
671
+ "Error message should indicate file not found",
672
+ );
673
+ } finally {
674
+ cleanupTestFile(testFile);
675
+ }
676
+ });