@aj-archipelago/cortex 1.3.62 → 1.3.63

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 (211) hide show
  1. package/.github/workflows/cortex-file-handler-test.yml +61 -0
  2. package/README.md +31 -7
  3. package/config/default.example.json +15 -0
  4. package/config.js +133 -12
  5. package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +22 -0
  6. package/helper-apps/cortex-autogen2/Dockerfile +31 -0
  7. package/helper-apps/cortex-autogen2/Dockerfile.worker +41 -0
  8. package/helper-apps/cortex-autogen2/README.md +183 -0
  9. package/helper-apps/cortex-autogen2/__init__.py +1 -0
  10. package/helper-apps/cortex-autogen2/agents.py +131 -0
  11. package/helper-apps/cortex-autogen2/docker-compose.yml +20 -0
  12. package/helper-apps/cortex-autogen2/function_app.py +55 -0
  13. package/helper-apps/cortex-autogen2/host.json +15 -0
  14. package/helper-apps/cortex-autogen2/main.py +126 -0
  15. package/helper-apps/cortex-autogen2/poetry.lock +3652 -0
  16. package/helper-apps/cortex-autogen2/pyproject.toml +36 -0
  17. package/helper-apps/cortex-autogen2/requirements.txt +20 -0
  18. package/helper-apps/cortex-autogen2/send_task.py +105 -0
  19. package/helper-apps/cortex-autogen2/services/__init__.py +1 -0
  20. package/helper-apps/cortex-autogen2/services/azure_queue.py +85 -0
  21. package/helper-apps/cortex-autogen2/services/redis_publisher.py +153 -0
  22. package/helper-apps/cortex-autogen2/task_processor.py +488 -0
  23. package/helper-apps/cortex-autogen2/tools/__init__.py +24 -0
  24. package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +175 -0
  25. package/helper-apps/cortex-autogen2/tools/azure_foundry_agents.py +601 -0
  26. package/helper-apps/cortex-autogen2/tools/coding_tools.py +72 -0
  27. package/helper-apps/cortex-autogen2/tools/download_tools.py +48 -0
  28. package/helper-apps/cortex-autogen2/tools/file_tools.py +545 -0
  29. package/helper-apps/cortex-autogen2/tools/search_tools.py +646 -0
  30. package/helper-apps/cortex-azure-cleaner/README.md +36 -0
  31. package/helper-apps/cortex-file-converter/README.md +93 -0
  32. package/helper-apps/cortex-file-converter/key_to_pdf.py +104 -0
  33. package/helper-apps/cortex-file-converter/list_blob_extensions.py +89 -0
  34. package/helper-apps/cortex-file-converter/process_azure_keynotes.py +181 -0
  35. package/helper-apps/cortex-file-converter/requirements.txt +1 -0
  36. package/helper-apps/cortex-file-handler/.env.test.azure.ci +7 -0
  37. package/helper-apps/cortex-file-handler/.env.test.azure.sample +1 -1
  38. package/helper-apps/cortex-file-handler/.env.test.gcs.ci +10 -0
  39. package/helper-apps/cortex-file-handler/.env.test.gcs.sample +2 -2
  40. package/helper-apps/cortex-file-handler/INTERFACE.md +41 -0
  41. package/helper-apps/cortex-file-handler/package.json +1 -1
  42. package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +41 -17
  43. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +30 -15
  44. package/helper-apps/cortex-file-handler/scripts/test-azure.sh +32 -6
  45. package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +24 -2
  46. package/helper-apps/cortex-file-handler/scripts/validate-env.js +128 -0
  47. package/helper-apps/cortex-file-handler/src/blobHandler.js +161 -51
  48. package/helper-apps/cortex-file-handler/src/constants.js +3 -0
  49. package/helper-apps/cortex-file-handler/src/fileChunker.js +10 -8
  50. package/helper-apps/cortex-file-handler/src/index.js +116 -9
  51. package/helper-apps/cortex-file-handler/src/redis.js +61 -1
  52. package/helper-apps/cortex-file-handler/src/services/ConversionService.js +11 -8
  53. package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +2 -2
  54. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +88 -6
  55. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +58 -0
  56. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +25 -5
  57. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +9 -0
  58. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +120 -16
  59. package/helper-apps/cortex-file-handler/src/start.js +27 -17
  60. package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +52 -1
  61. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +40 -0
  62. package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +553 -0
  63. package/helper-apps/cortex-file-handler/tests/cleanup.test.js +46 -52
  64. package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +451 -0
  65. package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +229 -0
  66. package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +392 -0
  67. package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +7 -2
  68. package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +348 -0
  69. package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +23 -2
  70. package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +11 -5
  71. package/helper-apps/cortex-file-handler/tests/getOperations.test.js +58 -24
  72. package/helper-apps/cortex-file-handler/tests/postOperations.test.js +11 -4
  73. package/helper-apps/cortex-file-handler/tests/shortLivedUrlConversion.test.js +225 -0
  74. package/helper-apps/cortex-file-handler/tests/start.test.js +8 -12
  75. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +80 -0
  76. package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +388 -22
  77. package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -0
  78. package/lib/cortexResponse.js +153 -0
  79. package/lib/entityConstants.js +21 -3
  80. package/lib/logger.js +21 -4
  81. package/lib/pathwayTools.js +28 -9
  82. package/lib/util.js +49 -0
  83. package/package.json +1 -1
  84. package/pathways/basePathway.js +1 -0
  85. package/pathways/bing_afagent.js +54 -1
  86. package/pathways/call_tools.js +2 -3
  87. package/pathways/chat_jarvis.js +1 -1
  88. package/pathways/google_cse.js +27 -0
  89. package/pathways/grok_live_search.js +18 -0
  90. package/pathways/system/entity/memory/sys_memory_lookup_required.js +1 -0
  91. package/pathways/system/entity/memory/sys_memory_required.js +1 -0
  92. package/pathways/system/entity/memory/sys_search_memory.js +1 -0
  93. package/pathways/system/entity/sys_entity_agent.js +56 -4
  94. package/pathways/system/entity/sys_generator_quick.js +1 -0
  95. package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +26 -0
  96. package/pathways/system/entity/tools/sys_tool_google_search.js +141 -0
  97. package/pathways/system/entity/tools/sys_tool_grok_x_search.js +237 -0
  98. package/pathways/system/entity/tools/sys_tool_image.js +1 -1
  99. package/pathways/system/rest_streaming/sys_claude_37_sonnet.js +21 -0
  100. package/pathways/system/rest_streaming/sys_claude_41_opus.js +21 -0
  101. package/pathways/system/rest_streaming/sys_claude_4_sonnet.js +21 -0
  102. package/pathways/system/rest_streaming/sys_google_gemini_25_flash.js +25 -0
  103. package/pathways/system/rest_streaming/{sys_google_gemini_chat.js → sys_google_gemini_25_pro.js} +6 -4
  104. package/pathways/system/rest_streaming/sys_grok_4.js +23 -0
  105. package/pathways/system/rest_streaming/sys_grok_4_fast_non_reasoning.js +23 -0
  106. package/pathways/system/rest_streaming/sys_grok_4_fast_reasoning.js +23 -0
  107. package/pathways/system/rest_streaming/sys_openai_chat.js +3 -0
  108. package/pathways/system/rest_streaming/sys_openai_chat_gpt41.js +22 -0
  109. package/pathways/system/rest_streaming/sys_openai_chat_gpt41_mini.js +21 -0
  110. package/pathways/system/rest_streaming/sys_openai_chat_gpt41_nano.js +21 -0
  111. package/pathways/system/rest_streaming/{sys_claude_35_sonnet.js → sys_openai_chat_gpt4_omni.js} +6 -4
  112. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_omni_mini.js +21 -0
  113. package/pathways/system/rest_streaming/{sys_claude_3_haiku.js → sys_openai_chat_gpt5.js} +7 -5
  114. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_chat.js +21 -0
  115. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_mini.js +21 -0
  116. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_nano.js +21 -0
  117. package/pathways/system/rest_streaming/{sys_openai_chat_o1.js → sys_openai_chat_o3.js} +6 -3
  118. package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +3 -0
  119. package/pathways/system/workspaces/run_workspace_prompt.js +99 -0
  120. package/pathways/vision.js +1 -1
  121. package/server/graphql.js +1 -1
  122. package/server/modelExecutor.js +8 -0
  123. package/server/pathwayResolver.js +166 -16
  124. package/server/pathwayResponseParser.js +16 -8
  125. package/server/plugins/azureFoundryAgentsPlugin.js +1 -1
  126. package/server/plugins/claude3VertexPlugin.js +193 -45
  127. package/server/plugins/gemini15ChatPlugin.js +21 -0
  128. package/server/plugins/gemini15VisionPlugin.js +360 -0
  129. package/server/plugins/googleCsePlugin.js +94 -0
  130. package/server/plugins/grokVisionPlugin.js +365 -0
  131. package/server/plugins/modelPlugin.js +3 -1
  132. package/server/plugins/openAiChatPlugin.js +106 -13
  133. package/server/plugins/openAiVisionPlugin.js +42 -30
  134. package/server/resolver.js +28 -4
  135. package/server/rest.js +270 -53
  136. package/server/typeDef.js +1 -0
  137. package/tests/{mocks.js → helpers/mocks.js} +5 -2
  138. package/tests/{server.js → helpers/server.js} +2 -2
  139. package/tests/helpers/sseAssert.js +23 -0
  140. package/tests/helpers/sseClient.js +73 -0
  141. package/tests/helpers/subscriptionAssert.js +11 -0
  142. package/tests/helpers/subscriptions.js +113 -0
  143. package/tests/{sublong.srt → integration/features/translate/sublong.srt} +4543 -4543
  144. package/tests/integration/features/translate/translate_chunking_stream.test.js +100 -0
  145. package/tests/{translate_srt.test.js → integration/features/translate/translate_srt.test.js} +2 -2
  146. package/tests/integration/graphql/async/stream/agentic.test.js +477 -0
  147. package/tests/integration/graphql/async/stream/subscription_streaming.test.js +62 -0
  148. package/tests/integration/graphql/async/stream/sys_entity_start_streaming.test.js +71 -0
  149. package/tests/integration/graphql/async/stream/vendors/claude_streaming.test.js +56 -0
  150. package/tests/integration/graphql/async/stream/vendors/gemini_streaming.test.js +66 -0
  151. package/tests/integration/graphql/async/stream/vendors/grok_streaming.test.js +56 -0
  152. package/tests/integration/graphql/async/stream/vendors/openai_streaming.test.js +72 -0
  153. package/tests/integration/graphql/features/google/sysToolGoogleSearch.test.js +96 -0
  154. package/tests/integration/graphql/features/grok/grok.test.js +688 -0
  155. package/tests/integration/graphql/features/grok/grok_x_search_tool.test.js +354 -0
  156. package/tests/{main.test.js → integration/graphql/features/main.test.js} +1 -1
  157. package/tests/{call_tools.test.js → integration/graphql/features/tools/call_tools.test.js} +2 -2
  158. package/tests/{vision.test.js → integration/graphql/features/vision/vision.test.js} +1 -1
  159. package/tests/integration/graphql/subscriptions/connection.test.js +26 -0
  160. package/tests/{openai_api.test.js → integration/rest/oai/openai_api.test.js} +63 -238
  161. package/tests/integration/rest/oai/tool_calling_api.test.js +343 -0
  162. package/tests/integration/rest/oai/tool_calling_streaming.test.js +85 -0
  163. package/tests/integration/rest/vendors/claude_streaming.test.js +47 -0
  164. package/tests/integration/rest/vendors/claude_tool_calling_streaming.test.js +75 -0
  165. package/tests/integration/rest/vendors/gemini_streaming.test.js +47 -0
  166. package/tests/integration/rest/vendors/gemini_tool_calling_streaming.test.js +75 -0
  167. package/tests/integration/rest/vendors/grok_streaming.test.js +55 -0
  168. package/tests/integration/rest/vendors/grok_tool_calling_streaming.test.js +75 -0
  169. package/tests/{azureAuthTokenHelper.test.js → unit/core/azureAuthTokenHelper.test.js} +1 -1
  170. package/tests/{chunkfunction.test.js → unit/core/chunkfunction.test.js} +2 -2
  171. package/tests/{config.test.js → unit/core/config.test.js} +3 -3
  172. package/tests/{encodeCache.test.js → unit/core/encodeCache.test.js} +1 -1
  173. package/tests/{fastLruCache.test.js → unit/core/fastLruCache.test.js} +1 -1
  174. package/tests/{handleBars.test.js → unit/core/handleBars.test.js} +1 -1
  175. package/tests/{memoryfunction.test.js → unit/core/memoryfunction.test.js} +2 -2
  176. package/tests/unit/core/mergeResolver.test.js +952 -0
  177. package/tests/{parser.test.js → unit/core/parser.test.js} +3 -3
  178. package/tests/unit/core/pathwayResolver.test.js +187 -0
  179. package/tests/{requestMonitor.test.js → unit/core/requestMonitor.test.js} +1 -1
  180. package/tests/{requestMonitorDurationEstimator.test.js → unit/core/requestMonitorDurationEstimator.test.js} +1 -1
  181. package/tests/{truncateMessages.test.js → unit/core/truncateMessages.test.js} +3 -3
  182. package/tests/{util.test.js → unit/core/util.test.js} +1 -1
  183. package/tests/{apptekTranslatePlugin.test.js → unit/plugins/apptekTranslatePlugin.test.js} +3 -3
  184. package/tests/{azureFoundryAgents.test.js → unit/plugins/azureFoundryAgents.test.js} +136 -1
  185. package/tests/{claude3VertexPlugin.test.js → unit/plugins/claude3VertexPlugin.test.js} +32 -10
  186. package/tests/{claude3VertexToolConversion.test.js → unit/plugins/claude3VertexToolConversion.test.js} +3 -3
  187. package/tests/unit/plugins/googleCsePlugin.test.js +111 -0
  188. package/tests/unit/plugins/grokVisionPlugin.test.js +1392 -0
  189. package/tests/{modelPlugin.test.js → unit/plugins/modelPlugin.test.js} +3 -3
  190. package/tests/{multimodal_conversion.test.js → unit/plugins/multimodal_conversion.test.js} +4 -4
  191. package/tests/{openAiChatPlugin.test.js → unit/plugins/openAiChatPlugin.test.js} +13 -4
  192. package/tests/{openAiToolPlugin.test.js → unit/plugins/openAiToolPlugin.test.js} +35 -27
  193. package/tests/{tokenHandlingTests.test.js → unit/plugins/tokenHandlingTests.test.js} +5 -5
  194. package/tests/{translate_apptek.test.js → unit/plugins/translate_apptek.test.js} +3 -3
  195. package/tests/{streaming.test.js → unit/plugins.streaming/plugin_stream_events.test.js} +19 -58
  196. package/helper-apps/mogrt-handler/tests/test-files/test.gif +0 -1
  197. package/helper-apps/mogrt-handler/tests/test-files/test.mogrt +0 -1
  198. package/helper-apps/mogrt-handler/tests/test-files/test.mp4 +0 -1
  199. package/pathways/system/rest_streaming/sys_openai_chat_gpt4.js +0 -19
  200. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_32.js +0 -19
  201. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_turbo.js +0 -19
  202. package/pathways/system/workspaces/run_claude35_sonnet.js +0 -21
  203. package/pathways/system/workspaces/run_claude3_haiku.js +0 -20
  204. package/pathways/system/workspaces/run_gpt35turbo.js +0 -20
  205. package/pathways/system/workspaces/run_gpt4.js +0 -20
  206. package/pathways/system/workspaces/run_gpt4_32.js +0 -20
  207. package/tests/agentic.test.js +0 -256
  208. package/tests/pathwayResolver.test.js +0 -78
  209. package/tests/subscription.test.js +0 -387
  210. /package/tests/{subchunk.srt → integration/features/translate/subchunk.srt} +0 -0
  211. /package/tests/{subhorizontal.srt → integration/features/translate/subhorizontal.srt} +0 -0
@@ -7,6 +7,7 @@ import axios from "axios";
7
7
 
8
8
  import { uploadBlob } from "../src/blobHandler.js";
9
9
  import { urlExists } from "../src/helper.js";
10
+ import { port } from "../src/start.js";
10
11
  import {
11
12
  setFileStoreMap,
12
13
  getFileStoreMap,
@@ -14,6 +15,7 @@ import {
14
15
  cleanupRedisFileStoreMapAge,
15
16
  } from "../src/redis.js";
16
17
  import { StorageService } from "../src/services/storage/StorageService.js";
18
+ import { startTestServer, stopTestServer } from "./testUtils.helper.js";
17
19
 
18
20
  const __filename = fileURLToPath(import.meta.url);
19
21
 
@@ -29,7 +31,7 @@ function shouldUseLocalStorage() {
29
31
  }
30
32
  const __dirname = path.dirname(__filename);
31
33
 
32
- const baseUrl = "http://localhost:7072/api/CortexFileHandler";
34
+ const baseUrl = `http://localhost:${port}/api/CortexFileHandler`;
33
35
 
34
36
  // Helper function to create a test file
35
37
  async function createTestFile(content, extension = "txt") {
@@ -75,18 +77,26 @@ function getRequestIdFromUploadResult(uploadResult) {
75
77
  return uploadResult.hash || "test-request-id";
76
78
  }
77
79
 
80
+ // Ensure server is ready before tests
78
81
  test.before(async () => {
79
- // Ensure Redis is connected
80
- const { connectClient } = await import("../src/redis.js");
81
- await connectClient();
82
+ // Start the server with Redis connection setup
83
+ await startTestServer({
84
+ beforeReady: async () => {
85
+ // Ensure Redis is connected
86
+ const { connectClient } = await import("../src/redis.js");
87
+ await connectClient();
88
+ }
89
+ });
82
90
  });
83
91
 
84
92
  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",
93
+ // Clean up server with cleanup logic
94
+ await stopTestServer(async () => {
95
+ // Clean up any remaining test entries
96
+ const testKeys = [
97
+ "test-lazy-cleanup",
98
+ "test-age-cleanup",
99
+ "test-old-entry",
90
100
  "test-missing-file",
91
101
  "test-gcs-backup",
92
102
  "test-recent-entry",
@@ -125,6 +135,7 @@ test.after(async () => {
125
135
  } catch (error) {
126
136
  console.error("Error cleaning up test files:", error);
127
137
  }
138
+ });
128
139
  });
129
140
 
130
141
  test("lazy cleanup should remove cache entry when file is missing", async (t) => {
@@ -510,22 +521,13 @@ test("cleanup should handle entries without timestamps gracefully", async (t) =>
510
521
  console.log(`Storing entry without timestamp:`, entryWithoutTimestamp);
511
522
 
512
523
  // 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
- }
524
+ const { client } = await import("../src/redis.js");
525
+
526
+ await client.hset(
527
+ "FileStoreMap",
528
+ hash,
529
+ JSON.stringify(entryWithoutTimestamp),
530
+ );
529
531
 
530
532
  // Verify it exists initially
531
533
  const initialResult = await getFileStoreMap(hash, true);
@@ -566,37 +568,29 @@ test("cleanup should handle malformed entries gracefully", async (t) => {
566
568
 
567
569
  // Store the hash in Redis with malformed data
568
570
  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);
571
+ const { client } = await import("../src/redis.js");
572
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");
573
+ await client.hset("FileStoreMap", malformedKey, "this is not json");
579
574
 
580
- // Verify malformed entry exists
581
- const initialResult = await getFileStoreMap(malformedKey, true);
582
- t.truthy(initialResult, "Malformed entry should exist initially");
575
+ // Verify malformed entry exists
576
+ const initialResult = await getFileStoreMap(malformedKey, true);
577
+ t.truthy(initialResult, "Malformed entry should exist initially");
583
578
 
584
- // Run age-based cleanup - should not crash
585
- const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
579
+ // Run age-based cleanup - should not crash
580
+ const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
586
581
 
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
- }
582
+ // Malformed entry should not be cleaned up (no timestamp)
583
+ const cleanedHash = cleaned.find(
584
+ (entry) => entry.hash === "test-malformed",
585
+ );
586
+ t.falsy(cleanedHash, "Malformed entry should not be cleaned up");
587
+
588
+ // Verify the entry still exists
589
+ const resultAfterCleanup = await getFileStoreMap(malformedKey, true);
590
+ t.truthy(resultAfterCleanup, "Malformed entry should still exist");
591
+
592
+ // Cleanup
593
+ await removeFromFileStoreMap(malformedKey);
600
594
  } finally {
601
595
  cleanupTestFile(testFile);
602
596
  }
@@ -0,0 +1,451 @@
1
+ import test from "ava";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ import { fileURLToPath } from "url";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import axios from "axios";
8
+ import FormData from "form-data";
9
+ import XLSX from "xlsx";
10
+ import { port } from "../src/start.js";
11
+ import {
12
+ uploadBlob,
13
+ isValidContainerName,
14
+ AZURE_STORAGE_CONTAINER_NAMES,
15
+ saveFileToBlob,
16
+ } from "../src/blobHandler.js";
17
+ import { FileConversionService } from "../src/services/FileConversionService.js";
18
+ import CortexFileHandler from "../src/index.js";
19
+ import {
20
+ startTestServer,
21
+ stopTestServer,
22
+ setupTestDirectory,
23
+ cleanupHashAndFile,
24
+ getFolderNameFromUrl,
25
+ } from "./testUtils.helper.js";
26
+
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = path.dirname(__filename);
29
+ const baseUrl = `http://localhost:${port}/api/CortexFileHandler`;
30
+
31
+ // Mock context for testing
32
+ const mockContext = {
33
+ log: (message) => console.log(`[CONTAINER_CONVERSION_TEST] ${message}`),
34
+ res: null,
35
+ };
36
+
37
+ // Helper function to create test files
38
+ async function createTestFile(content, extension, filename = null) {
39
+ const testDir = path.join(__dirname, "test-conversion-files");
40
+ if (!fs.existsSync(testDir)) {
41
+ fs.mkdirSync(testDir, { recursive: true });
42
+ }
43
+ const testFilename = filename || `${uuidv4()}.${extension}`;
44
+ const filePath = path.join(testDir, testFilename);
45
+
46
+ if (extension === 'xlsx') {
47
+ // Create Excel file
48
+ const workbook = XLSX.utils.book_new();
49
+ const ws1 = XLSX.utils.aoa_to_sheet(content);
50
+ XLSX.utils.book_append_sheet(workbook, ws1, "Sheet1");
51
+ XLSX.writeFile(workbook, filePath);
52
+ } else {
53
+ fs.writeFileSync(filePath, content);
54
+ }
55
+
56
+ return filePath;
57
+ }
58
+
59
+ // Helper function to check if URL belongs to specific container
60
+ function getContainerFromUrl(url) {
61
+ try {
62
+ const urlObj = new URL(url);
63
+ const pathSegments = urlObj.pathname.split('/').filter(Boolean);
64
+
65
+ // For Azure URLs, container is typically the first segment after the account
66
+ // Format: https://account.blob.core.windows.net/container/blob...
67
+ // For Azurite (local emulator), format is: http://127.0.0.1:10000/devstoreaccount1/container/blob...
68
+ if (pathSegments.length > 0) {
69
+ // Check if this is an Azurite URL (localhost with devstoreaccount1)
70
+ if (urlObj.hostname === '127.0.0.1' && pathSegments[0] === 'devstoreaccount1') {
71
+ // For Azurite, container is the second segment
72
+ return pathSegments.length > 1 ? pathSegments[1] : null;
73
+ } else {
74
+ // For production Azure, container is the first segment
75
+ return pathSegments[0];
76
+ }
77
+ }
78
+ } catch (error) {
79
+ console.log("Error parsing container from URL:", error);
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // Setup: Create test directory and start server
85
+ test.before(async (t) => {
86
+ await startTestServer();
87
+ await setupTestDirectory(t);
88
+ });
89
+
90
+ // Cleanup
91
+ test.after.always(async (t) => {
92
+ await stopTestServer();
93
+
94
+ // Clean up test directory
95
+ if (t.context?.testDir) {
96
+ await fs.promises.rm(t.context.testDir, { recursive: true, force: true });
97
+ }
98
+
99
+ // Clean up any remaining files in the test-conversion-files directory
100
+ const testFilesDir = path.join(__dirname, "test-conversion-files");
101
+ if (fs.existsSync(testFilesDir)) {
102
+ try {
103
+ await fs.promises.rm(testFilesDir, { recursive: true, force: true });
104
+ } catch (error) {
105
+ console.log("Error cleaning test files:", error);
106
+ }
107
+ }
108
+ });
109
+
110
+ // Test that FileConversionService._saveConvertedFile respects container parameter
111
+ test("FileConversionService._saveConvertedFile should use specified container", async (t) => {
112
+ if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
113
+ t.pass("Skipping test - Azure not configured");
114
+ return;
115
+ }
116
+
117
+ const originalEnv = process.env.AZURE_STORAGE_CONTAINER_NAME;
118
+ process.env.AZURE_STORAGE_CONTAINER_NAME = "test1,test2,test3";
119
+
120
+ try {
121
+ const service = new FileConversionService(mockContext, true); // useAzure = true
122
+
123
+ // Create a test file to save
124
+ const testContent = "This is converted file content";
125
+ const testFile = await createTestFile(testContent, "txt", "converted-test.txt");
126
+ const requestId = uuidv4();
127
+ const targetContainer = "test2";
128
+
129
+ // Call _saveConvertedFile with container parameter
130
+ const result = await service._saveConvertedFile(
131
+ testFile,
132
+ requestId,
133
+ null, // filename
134
+ targetContainer
135
+ );
136
+
137
+ t.truthy(result);
138
+ t.truthy(result.url);
139
+
140
+ // Verify the URL indicates it was uploaded to the correct container
141
+ const containerFromUrl = getContainerFromUrl(result.url);
142
+ t.is(containerFromUrl, targetContainer,
143
+ `File should be uploaded to container ${targetContainer}, but was uploaded to ${containerFromUrl}`);
144
+
145
+ // Cleanup
146
+ await cleanupHashAndFile(null, result.url, baseUrl);
147
+ } finally {
148
+ // Restore environment
149
+ if (originalEnv) {
150
+ process.env.AZURE_STORAGE_CONTAINER_NAME = originalEnv;
151
+ } else {
152
+ delete process.env.AZURE_STORAGE_CONTAINER_NAME;
153
+ }
154
+ }
155
+ });
156
+
157
+ // Test that file upload with conversion respects container parameter
158
+ test("File upload with conversion should upload both original and converted files to specified container", async (t) => {
159
+ if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
160
+ t.pass("Skipping test - Azure not configured");
161
+ return;
162
+ }
163
+
164
+ const originalEnv = process.env.AZURE_STORAGE_CONTAINER_NAME;
165
+ process.env.AZURE_STORAGE_CONTAINER_NAME = "test1,test2,test3";
166
+
167
+ try {
168
+ // Create an Excel file that will need conversion
169
+ const excelData = [
170
+ ["Name", "Age", "City"],
171
+ ["John", 30, "New York"],
172
+ ["Jane", 25, "Boston"],
173
+ ];
174
+ const testFile = await createTestFile(excelData, "xlsx", "test-conversion.xlsx");
175
+ const targetContainer = "test3";
176
+
177
+ // Create form data with container parameter
178
+ const form = new FormData();
179
+ form.append("file", fs.createReadStream(testFile), "test-conversion.xlsx");
180
+ form.append("container", targetContainer);
181
+
182
+ const response = await axios.post(baseUrl, form, {
183
+ headers: {
184
+ ...form.getHeaders(),
185
+ "Content-Type": "multipart/form-data",
186
+ },
187
+ validateStatus: (status) => true,
188
+ timeout: 60000, // Longer timeout for conversion
189
+ });
190
+
191
+ t.is(response.status, 200);
192
+ t.truthy(response.data.url);
193
+
194
+ // Check that the main uploaded file is in the correct container
195
+ const mainContainerFromUrl = getContainerFromUrl(response.data.url);
196
+ t.is(mainContainerFromUrl, targetContainer,
197
+ `Original file should be in container ${targetContainer}, but was in ${mainContainerFromUrl}`);
198
+
199
+ // If there's a converted file mentioned in the response, check its container too
200
+ if (response.data.converted && response.data.converted.url) {
201
+ const convertedContainerFromUrl = getContainerFromUrl(response.data.converted.url);
202
+ t.is(convertedContainerFromUrl, targetContainer,
203
+ `Converted file should be in container ${targetContainer}, but was in ${convertedContainerFromUrl}`);
204
+ }
205
+
206
+ // Cleanup
207
+ await cleanupHashAndFile(null, response.data.url, baseUrl);
208
+ if (response.data.converted && response.data.converted.url) {
209
+ await cleanupHashAndFile(null, response.data.converted.url, baseUrl);
210
+ }
211
+ } finally {
212
+ // Restore environment
213
+ if (originalEnv) {
214
+ process.env.AZURE_STORAGE_CONTAINER_NAME = originalEnv;
215
+ } else {
216
+ delete process.env.AZURE_STORAGE_CONTAINER_NAME;
217
+ }
218
+ }
219
+ });
220
+
221
+ // Test document processing with save=true and container parameter
222
+ test("Document processing with save=true should save converted file to specified container", async (t) => {
223
+ if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
224
+ t.pass("Skipping test - Azure not configured");
225
+ return;
226
+ }
227
+
228
+ const originalEnv = process.env.AZURE_STORAGE_CONTAINER_NAME;
229
+ process.env.AZURE_STORAGE_CONTAINER_NAME = "test1,test2,test3";
230
+
231
+ try {
232
+ // First upload a document file to get a URI
233
+ const docContent = "This is a test document content for processing.";
234
+ const testFile = await createTestFile(docContent, "txt", "test-doc.txt");
235
+ const targetContainer = "test1";
236
+
237
+ // Upload the file first
238
+ const uploadForm = new FormData();
239
+ uploadForm.append("file", fs.createReadStream(testFile), "test-doc.txt");
240
+ uploadForm.append("container", targetContainer);
241
+
242
+ const uploadResponse = await axios.post(baseUrl, uploadForm, {
243
+ headers: {
244
+ ...uploadForm.getHeaders(),
245
+ "Content-Type": "multipart/form-data",
246
+ },
247
+ validateStatus: (status) => true,
248
+ timeout: 30000,
249
+ });
250
+
251
+ t.is(uploadResponse.status, 200);
252
+ const documentUri = uploadResponse.data.url;
253
+
254
+ // Now process the document with save=true and container parameter
255
+ const processResponse = await axios.get(baseUrl, {
256
+ params: {
257
+ uri: documentUri,
258
+ requestId: uuidv4(),
259
+ save: true,
260
+ container: targetContainer,
261
+ },
262
+ validateStatus: (status) => true,
263
+ timeout: 60000,
264
+ });
265
+
266
+ t.is(processResponse.status, 200);
267
+ t.truthy(processResponse.data.url);
268
+
269
+ // Check that the saved file is in the correct container
270
+ const savedContainerFromUrl = getContainerFromUrl(processResponse.data.url);
271
+ t.is(savedContainerFromUrl, targetContainer,
272
+ `Saved processed file should be in container ${targetContainer}, but was in ${savedContainerFromUrl}`);
273
+
274
+ // Cleanup
275
+ await cleanupHashAndFile(null, documentUri, baseUrl);
276
+ await cleanupHashAndFile(null, processResponse.data.url, baseUrl);
277
+ } finally {
278
+ // Restore environment
279
+ if (originalEnv) {
280
+ process.env.AZURE_STORAGE_CONTAINER_NAME = originalEnv;
281
+ } else {
282
+ delete process.env.AZURE_STORAGE_CONTAINER_NAME;
283
+ }
284
+ }
285
+ });
286
+
287
+ // Test checkHash operation preserves container for converted files
288
+ test("checkHash operation should respect container parameter for converted files", async (t) => {
289
+ if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
290
+ t.pass("Skipping test - Azure not configured");
291
+ return;
292
+ }
293
+
294
+ const originalEnv = process.env.AZURE_STORAGE_CONTAINER_NAME;
295
+ process.env.AZURE_STORAGE_CONTAINER_NAME = "test1,test2,test3";
296
+
297
+ try {
298
+ // Create an Excel file that will need conversion
299
+ const excelData = [
300
+ ["Product", "Price"],
301
+ ["Widget", 10.99],
302
+ ["Gadget", 15.50],
303
+ ];
304
+ const testFile = await createTestFile(excelData, "xlsx", "hash-test.xlsx");
305
+ const targetContainer = "test2";
306
+ const testHash = uuidv4();
307
+
308
+ // Upload the file with a hash and container parameter
309
+ const form = new FormData();
310
+ form.append("file", fs.createReadStream(testFile), "hash-test.xlsx");
311
+ form.append("hash", testHash);
312
+ form.append("container", targetContainer);
313
+
314
+ const uploadResponse = await axios.post(baseUrl, form, {
315
+ headers: {
316
+ ...form.getHeaders(),
317
+ "Content-Type": "multipart/form-data",
318
+ },
319
+ validateStatus: (status) => true,
320
+ timeout: 60000,
321
+ });
322
+
323
+ t.is(uploadResponse.status, 200);
324
+
325
+ // Now check the hash with container parameter
326
+ const checkResponse = await axios.get(baseUrl, {
327
+ params: {
328
+ hash: testHash,
329
+ checkHash: true,
330
+ container: targetContainer,
331
+ },
332
+ validateStatus: (status) => true,
333
+ timeout: 60000,
334
+ });
335
+
336
+ t.is(checkResponse.status, 200);
337
+ t.truthy(checkResponse.data.url);
338
+
339
+ // Check that the original file is in the correct container
340
+ const originalContainerFromUrl = getContainerFromUrl(checkResponse.data.url);
341
+ t.is(originalContainerFromUrl, targetContainer,
342
+ `Original file should be in container ${targetContainer}, but was in ${originalContainerFromUrl}`);
343
+
344
+ // If there's a converted file, check its container too
345
+ if (checkResponse.data.converted && checkResponse.data.converted.url) {
346
+ const convertedContainerFromUrl = getContainerFromUrl(checkResponse.data.converted.url);
347
+ t.is(convertedContainerFromUrl, targetContainer,
348
+ `Converted file should be in container ${targetContainer}, but was in ${convertedContainerFromUrl}`);
349
+ }
350
+
351
+ // Cleanup
352
+ await cleanupHashAndFile(testHash, checkResponse.data.url, baseUrl);
353
+ if (checkResponse.data.converted && checkResponse.data.converted.url) {
354
+ await cleanupHashAndFile(null, checkResponse.data.converted.url, baseUrl);
355
+ }
356
+ } finally {
357
+ // Restore environment
358
+ if (originalEnv) {
359
+ process.env.AZURE_STORAGE_CONTAINER_NAME = originalEnv;
360
+ } else {
361
+ delete process.env.AZURE_STORAGE_CONTAINER_NAME;
362
+ }
363
+ }
364
+ });
365
+
366
+ // Test that default container is used when no container specified for conversions
367
+ test("Conversion should use default container when no container specified", async (t) => {
368
+ if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
369
+ t.pass("Skipping test - Azure not configured");
370
+ return;
371
+ }
372
+
373
+ const originalEnv = process.env.AZURE_STORAGE_CONTAINER_NAME;
374
+ process.env.AZURE_STORAGE_CONTAINER_NAME = "test1,test2,test3";
375
+
376
+ try {
377
+ const service = new FileConversionService(mockContext, true);
378
+
379
+ // Create a test file to save
380
+ const testContent = "This is converted file content for default container test";
381
+ const testFile = await createTestFile(testContent, "txt", "default-container-test.txt");
382
+ const requestId = uuidv4();
383
+
384
+ // Call _saveConvertedFile without container parameter (should use default)
385
+ const result = await service._saveConvertedFile(
386
+ testFile,
387
+ requestId,
388
+ null, // filename
389
+ null // container - should use default
390
+ );
391
+
392
+ t.truthy(result);
393
+ t.truthy(result.url);
394
+
395
+ // Verify the URL indicates it was uploaded to the default container
396
+ const containerFromUrl = getContainerFromUrl(result.url);
397
+ t.is(containerFromUrl, AZURE_STORAGE_CONTAINER_NAMES[0],
398
+ `File should be uploaded to default container ${AZURE_STORAGE_CONTAINER_NAMES[0]}, but was uploaded to ${containerFromUrl}`);
399
+
400
+ // Cleanup
401
+ await cleanupHashAndFile(null, result.url, baseUrl);
402
+ } finally {
403
+ // Restore environment
404
+ if (originalEnv) {
405
+ process.env.AZURE_STORAGE_CONTAINER_NAME = originalEnv;
406
+ } else {
407
+ delete process.env.AZURE_STORAGE_CONTAINER_NAME;
408
+ }
409
+ }
410
+ });
411
+
412
+ // Test saveFileToBlob function directly with container parameter
413
+ test("saveFileToBlob should respect container parameter", async (t) => {
414
+ if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
415
+ t.pass("Skipping test - Azure not configured");
416
+ return;
417
+ }
418
+
419
+ const originalEnv = process.env.AZURE_STORAGE_CONTAINER_NAME;
420
+ process.env.AZURE_STORAGE_CONTAINER_NAME = "test1,test2,test3";
421
+
422
+ try {
423
+ // Create a test file
424
+ const testContent = "This is a test for saveFileToBlob with container parameter";
425
+ const testFile = await createTestFile(testContent, "txt", "save-blob-test.txt");
426
+ const requestId = uuidv4();
427
+ const targetContainer = "test3";
428
+
429
+ // Call saveFileToBlob directly with container parameter
430
+ const result = await saveFileToBlob(testFile, requestId, null, targetContainer);
431
+
432
+ t.truthy(result);
433
+ t.truthy(result.url);
434
+ t.truthy(result.blobName);
435
+
436
+ // Verify the URL indicates it was uploaded to the correct container
437
+ const containerFromUrl = getContainerFromUrl(result.url);
438
+ t.is(containerFromUrl, targetContainer,
439
+ `File should be uploaded to container ${targetContainer}, but was uploaded to ${containerFromUrl}`);
440
+
441
+ // Cleanup
442
+ await cleanupHashAndFile(null, result.url, baseUrl);
443
+ } finally {
444
+ // Restore environment
445
+ if (originalEnv) {
446
+ process.env.AZURE_STORAGE_CONTAINER_NAME = originalEnv;
447
+ } else {
448
+ delete process.env.AZURE_STORAGE_CONTAINER_NAME;
449
+ }
450
+ }
451
+ });