@flink-app/flink 0.14.3 → 2.0.0-alpha.100

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 (280) hide show
  1. package/CHANGELOG.md +1051 -0
  2. package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
  3. package/SIMPLE_AST_FEASIBILITY.md +570 -0
  4. package/bin/flink.ts +13 -2
  5. package/cli/build.ts +24 -44
  6. package/cli/clean.ts +13 -25
  7. package/cli/cli-utils.ts +190 -17
  8. package/cli/dev.ts +252 -0
  9. package/cli/loadEnvFiles.ts +116 -0
  10. package/cli/run.ts +45 -62
  11. package/dist/bin/flink.js +61 -2
  12. package/dist/cli/build.js +20 -25
  13. package/dist/cli/clean.js +12 -10
  14. package/dist/cli/cli-utils.d.ts +34 -3
  15. package/dist/cli/cli-utils.js +193 -12
  16. package/dist/cli/dev.d.ts +2 -0
  17. package/dist/cli/dev.js +279 -0
  18. package/dist/cli/loadEnvFiles.d.ts +30 -0
  19. package/dist/cli/loadEnvFiles.js +113 -0
  20. package/dist/cli/run.js +47 -46
  21. package/dist/src/DependencyTracker.d.ts +44 -0
  22. package/dist/src/DependencyTracker.js +239 -0
  23. package/dist/src/FlinkApp.d.ts +163 -10
  24. package/dist/src/FlinkApp.js +847 -184
  25. package/dist/src/FlinkContext.d.ts +41 -0
  26. package/dist/src/FlinkErrors.d.ts +19 -6
  27. package/dist/src/FlinkErrors.js +36 -42
  28. package/dist/src/FlinkHttpHandler.d.ts +219 -26
  29. package/dist/src/FlinkHttpHandler.js +37 -1
  30. package/dist/src/FlinkJob.d.ts +10 -0
  31. package/dist/src/FlinkLog.d.ts +82 -18
  32. package/dist/src/FlinkLog.js +165 -13
  33. package/dist/src/FlinkLogFactory.d.ts +288 -0
  34. package/dist/src/FlinkLogFactory.js +619 -0
  35. package/dist/src/FlinkRepo.d.ts +10 -2
  36. package/dist/src/FlinkRepo.js +11 -1
  37. package/dist/src/FlinkRequestContext.d.ts +63 -0
  38. package/dist/src/FlinkRequestContext.js +74 -0
  39. package/dist/src/FlinkResponse.d.ts +6 -0
  40. package/dist/src/FlinkService.d.ts +38 -0
  41. package/dist/src/FlinkService.js +46 -0
  42. package/dist/src/LeaderElection.d.ts +45 -0
  43. package/dist/src/LeaderElection.js +269 -0
  44. package/dist/src/SchemaCache.d.ts +84 -0
  45. package/dist/src/SchemaCache.js +289 -0
  46. package/dist/src/TypeScriptCompiler.d.ts +161 -51
  47. package/dist/src/TypeScriptCompiler.js +1253 -617
  48. package/dist/src/TypeScriptUtils.js +4 -0
  49. package/dist/src/ai/AgentRunner.d.ts +39 -0
  50. package/dist/src/ai/AgentRunner.js +760 -0
  51. package/dist/src/ai/ConversationAgent.d.ts +279 -0
  52. package/dist/src/ai/ConversationAgent.js +404 -0
  53. package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
  54. package/dist/src/ai/ConversationFlinkAgent.js +404 -0
  55. package/dist/src/ai/FlinkAgent.d.ts +690 -0
  56. package/dist/src/ai/FlinkAgent.js +729 -0
  57. package/dist/src/ai/FlinkTool.d.ts +135 -0
  58. package/dist/src/ai/FlinkTool.js +2 -0
  59. package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
  60. package/dist/src/ai/InMemoryConversationAgent.js +209 -0
  61. package/dist/src/ai/LLMAdapter.d.ts +148 -0
  62. package/dist/src/ai/LLMAdapter.js +2 -0
  63. package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
  64. package/dist/src/ai/PersistentFlinkAgent.js +403 -0
  65. package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
  66. package/dist/src/ai/SubAgentExecutor.js +223 -0
  67. package/dist/src/ai/ToolExecutor.d.ts +64 -0
  68. package/dist/src/ai/ToolExecutor.js +497 -0
  69. package/dist/src/ai/agentInstructions.d.ts +68 -0
  70. package/dist/src/ai/agentInstructions.js +286 -0
  71. package/dist/src/ai/index.d.ts +8 -0
  72. package/dist/src/ai/index.js +26 -0
  73. package/dist/src/ai/instructionFileLoader.d.ts +44 -0
  74. package/dist/src/ai/instructionFileLoader.js +179 -0
  75. package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
  76. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  77. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  78. package/dist/src/index.d.ts +14 -0
  79. package/dist/src/index.js +17 -0
  80. package/dist/src/loadPluginSchemas.d.ts +45 -0
  81. package/dist/src/loadPluginSchemas.js +143 -0
  82. package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
  83. package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
  84. package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
  85. package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
  86. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
  87. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
  88. package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
  89. package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
  90. package/dist/src/schema-extraction/index.d.ts +2 -0
  91. package/dist/src/schema-extraction/index.js +20 -0
  92. package/dist/src/schema-extraction/types.d.ts +31 -0
  93. package/dist/src/schema-extraction/types.js +2 -0
  94. package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
  95. package/dist/src/utils/loadFlinkConfig.js +77 -0
  96. package/dist/src/utils.d.ts +30 -0
  97. package/dist/src/utils.js +52 -0
  98. package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
  99. package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
  100. package/dist/src/workers/WorkerPool.d.ts +60 -0
  101. package/dist/src/workers/WorkerPool.js +306 -0
  102. package/examples/logging-hierarchical-example.ts +125 -0
  103. package/package.json +29 -4
  104. package/readme.md +499 -0
  105. package/spec/AgentDescendantDetection.spec.ts +335 -0
  106. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  107. package/spec/AgentObserver.spec.ts +266 -0
  108. package/spec/AgentRunner.spec.ts +1062 -0
  109. package/spec/AsyncLocalStorageContext.spec.ts +223 -0
  110. package/spec/ConversationHooks.spec.ts +257 -0
  111. package/spec/FlinkAgent.spec.ts +681 -0
  112. package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
  113. package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
  114. package/spec/FlinkApp.onError.spec.ts +1 -2
  115. package/spec/FlinkApp.query.spec.ts +107 -0
  116. package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
  117. package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
  118. package/spec/FlinkApp.validationMode.spec.ts +155 -0
  119. package/spec/FlinkJob.spec.ts +171 -0
  120. package/spec/FlinkLogFactory.spec.ts +337 -0
  121. package/spec/FlinkRepo.spec.ts +1 -1
  122. package/spec/LeaderElection.spec.ts +174 -0
  123. package/spec/StreamingIntegration.spec.ts +139 -0
  124. package/spec/ToolExecutor.spec.ts +465 -0
  125. package/spec/TypeScriptCompiler.spec.ts +1 -1
  126. package/spec/TypeScriptSourceParser.spec.ts +1215 -0
  127. package/spec/TypeScriptTokenizer.spec.ts +366 -0
  128. package/spec/ai/ContextCompaction.spec.ts +405 -0
  129. package/spec/ai/ConversationAgent.spec.ts +520 -0
  130. package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
  131. package/spec/ai/agentInstructions.spec.ts +358 -0
  132. package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
  133. package/spec/fixtures/agent-instructions/simple.md +3 -0
  134. package/spec/fixtures/agent-instructions/template.md +18 -0
  135. package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
  136. package/spec/mock-project/dist/.tsbuildinfo +1 -0
  137. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
  138. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
  139. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
  140. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
  141. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
  142. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
  143. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
  144. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
  145. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
  146. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  147. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  148. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
  149. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
  150. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
  151. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
  152. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
  153. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
  154. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
  155. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
  156. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
  157. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  158. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  159. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  160. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  161. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  162. package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
  163. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  164. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  165. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  166. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  167. package/spec/mock-project/dist/src/FlinkLog.js +119 -0
  168. package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
  169. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  170. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  171. package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
  172. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  173. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  174. package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
  175. package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
  176. package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
  177. package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
  178. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  179. package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
  180. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  181. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
  182. package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
  183. package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
  184. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  185. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  186. package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
  187. package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
  188. package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
  189. package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
  190. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
  191. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
  192. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
  193. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
  194. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
  195. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
  196. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
  197. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
  198. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
  199. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
  200. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
  201. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
  202. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
  203. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
  204. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
  205. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
  206. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
  207. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
  208. package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
  209. package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
  210. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
  211. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
  212. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
  213. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
  214. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
  215. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
  216. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
  217. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
  218. package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
  219. package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
  220. package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
  221. package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
  222. package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
  223. package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
  224. package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
  225. package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
  226. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  227. package/spec/mock-project/dist/src/index.js +52 -76
  228. package/spec/mock-project/dist/src/index.js.map +1 -0
  229. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  230. package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
  231. package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
  232. package/spec/mock-project/dist/src/schemas/Car.js +3 -1
  233. package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
  234. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
  235. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
  236. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
  237. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
  238. package/spec/mock-project/dist/src/utils.js +290 -0
  239. package/spec/mock-project/tsconfig.json +6 -1
  240. package/spec/schema-generation-nested-objects.spec.ts +97 -0
  241. package/spec/testHelpers.ts +49 -0
  242. package/spec/utils.caseConversion.spec.ts +78 -0
  243. package/spec/utils.spec.ts +13 -13
  244. package/src/DependencyTracker.ts +166 -0
  245. package/src/FlinkApp.ts +919 -155
  246. package/src/FlinkContext.ts +43 -0
  247. package/src/FlinkErrors.ts +32 -12
  248. package/src/FlinkHttpHandler.ts +246 -28
  249. package/src/FlinkJob.ts +11 -0
  250. package/src/FlinkLog.ts +119 -12
  251. package/src/FlinkLogFactory.ts +699 -0
  252. package/src/FlinkRepo.ts +10 -3
  253. package/src/FlinkRequestContext.ts +95 -0
  254. package/src/FlinkResponse.ts +6 -0
  255. package/src/FlinkService.ts +49 -0
  256. package/src/LeaderElection.ts +203 -0
  257. package/src/SchemaCache.ts +232 -0
  258. package/src/TypeScriptCompiler.ts +1347 -610
  259. package/src/TypeScriptUtils.ts +5 -0
  260. package/src/ai/AgentRunner.ts +646 -0
  261. package/src/ai/ConversationAgent.ts +413 -0
  262. package/src/ai/FlinkAgent.ts +1069 -0
  263. package/src/ai/FlinkTool.ts +165 -0
  264. package/src/ai/InMemoryConversationAgent.ts +149 -0
  265. package/src/ai/LLMAdapter.ts +126 -0
  266. package/src/ai/ToolExecutor.ts +485 -0
  267. package/src/ai/agentInstructions.ts +245 -0
  268. package/src/ai/index.ts +8 -0
  269. package/src/ai/instructionFileLoader.ts +156 -0
  270. package/src/auth/FlinkAuthPlugin.ts +2 -1
  271. package/src/handlers/StreamWriterFactory.ts +84 -0
  272. package/src/index.ts +14 -0
  273. package/src/loadPluginSchemas.ts +141 -0
  274. package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
  275. package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
  276. package/src/schema-extraction/index.ts +2 -0
  277. package/src/schema-extraction/types.ts +34 -0
  278. package/src/utils/loadFlinkConfig.ts +89 -0
  279. package/src/utils.ts +52 -0
  280. package/tsconfig.json +6 -1
@@ -0,0 +1,337 @@
1
+ import { FlinkLogFactory } from "../src/FlinkLogFactory";
2
+
3
+ describe("FlinkLogFactory - Hierarchical Prefix Matching", () => {
4
+ beforeEach(() => {
5
+ // Reset factory state before each test
6
+ FlinkLogFactory.resetComponentLevels();
7
+ FlinkLogFactory.resetHierarchicalLevels();
8
+ FlinkLogFactory.resetWildcardLevels();
9
+ FlinkLogFactory.setGlobalLevel("info");
10
+ });
11
+
12
+ describe("Basic Prefix Matching (Java-style)", () => {
13
+ it("should match flink.ai.* prefix", () => {
14
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
15
+
16
+ const openaiLog = FlinkLogFactory.createLogger("flink.ai.openai");
17
+ const anthropicLog = FlinkLogFactory.createLogger("flink.ai.anthropic");
18
+ const dbLog = FlinkLogFactory.createLogger("flink.database.mongodb");
19
+
20
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug");
21
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.anthropic")).toBe("debug");
22
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database.mongodb")).toBeNull(); // Falls back to global
23
+ });
24
+
25
+ it("should match multi-level prefixes", () => {
26
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
27
+
28
+ const deepLog = FlinkLogFactory.createLogger("flink.ai.openai.v4.gpt");
29
+
30
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.v4.gpt")).toBe("debug");
31
+ });
32
+
33
+ it("should handle prefix with trailing dot", () => {
34
+ FlinkLogFactory.setHierarchicalLevel("flink.ai.", "debug");
35
+
36
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug");
37
+ });
38
+
39
+ it("should handle prefix without trailing dot", () => {
40
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
41
+
42
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug");
43
+ });
44
+ });
45
+
46
+ describe("Case Insensitivity", () => {
47
+ it("should normalize logger names to lowercase", () => {
48
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
49
+
50
+ const log1 = FlinkLogFactory.createLogger("flink.ai.openai");
51
+ const log2 = FlinkLogFactory.createLogger("Flink.AI.OpenAI");
52
+ const log3 = FlinkLogFactory.createLogger("FLINK.AI.OPENAI");
53
+
54
+ // All should return the same logger instance
55
+ expect(log1).toBe(log2);
56
+ expect(log2).toBe(log3);
57
+ });
58
+
59
+ it("should resolve levels case-insensitively", () => {
60
+ FlinkLogFactory.setHierarchicalLevel("Flink.AI", "debug");
61
+
62
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug");
63
+ expect(FlinkLogFactory.getEffectiveLevel("Flink.AI.OpenAI")).toBe("debug");
64
+ expect(FlinkLogFactory.getEffectiveLevel("FLINK.AI.OPENAI")).toBe("debug");
65
+ });
66
+ });
67
+
68
+ describe("Precedence Rules", () => {
69
+ it("should prefer exact match over prefix", () => {
70
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
71
+ FlinkLogFactory.setComponentLevel("flink.ai.openai", "trace");
72
+
73
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("trace"); // Exact
74
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.anthropic")).toBe("debug"); // Prefix
75
+ });
76
+
77
+ it("should prefer more specific prefix over less specific", () => {
78
+ FlinkLogFactory.setGlobalLevel("warn");
79
+ FlinkLogFactory.setHierarchicalLevel("flink", "info");
80
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
81
+ FlinkLogFactory.setHierarchicalLevel("flink.ai.openai", "trace");
82
+
83
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.v4")).toBe("trace"); // Most specific: flink.ai.openai
84
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.claude")).toBe("debug"); // flink.ai
85
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database.mongodb")).toBe("info"); // flink
86
+ expect(FlinkLogFactory.getEffectiveLevel("other.service")).toBeNull(); // Global fallback
87
+ });
88
+
89
+ it("should handle overlapping prefixes correctly", () => {
90
+ FlinkLogFactory.setHierarchicalLevel("flink", "warn");
91
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
92
+
93
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug"); // More specific
94
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database")).toBe("warn"); // Less specific
95
+ });
96
+ });
97
+
98
+ describe("Wildcard Patterns", () => {
99
+ it("should support single-level wildcard (*)", () => {
100
+ FlinkLogFactory.setWildcardLevel("flink.ai.*", "debug");
101
+
102
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug"); // Matches
103
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.v4")).toBeNull(); // Too deep
104
+ });
105
+
106
+ it("should support multi-level wildcard (**)", () => {
107
+ FlinkLogFactory.setWildcardLevel("flink.ai.**", "trace");
108
+
109
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("trace");
110
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.v4")).toBe("trace");
111
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.v4.gpt")).toBe("trace");
112
+ });
113
+
114
+ it("should support partial segment wildcard", () => {
115
+ FlinkLogFactory.setWildcardLevel("flink.database.mongo*", "warn");
116
+
117
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database.mongodb")).toBe("warn");
118
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database.mongoose")).toBe("warn");
119
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database.redis")).toBeNull();
120
+ });
121
+
122
+ it("should prefer prefix match over wildcard", () => {
123
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
124
+ FlinkLogFactory.setWildcardLevel("flink.ai.*", "trace");
125
+
126
+ // Prefix is checked first and matches (more intuitive)
127
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug");
128
+ });
129
+ });
130
+
131
+ describe("Environment Variable Parsing", () => {
132
+ beforeEach(() => {
133
+ // Clear environment
134
+ delete process.env.LOG_LEVEL;
135
+ });
136
+
137
+ it("should parse LOG_LEVEL env var for global level", () => {
138
+ process.env.LOG_LEVEL = "debug";
139
+
140
+ // Reset to force re-initialization
141
+ (FlinkLogFactory as any).initialized = false;
142
+ FlinkLogFactory.configure();
143
+
144
+ expect(FlinkLogFactory.getGlobalLevel()).toBe("debug");
145
+ });
146
+
147
+ it("should ignore invalid LOG_LEVEL values", () => {
148
+ process.env.LOG_LEVEL = "invalid";
149
+
150
+ (FlinkLogFactory as any).initialized = false;
151
+ FlinkLogFactory.configure();
152
+
153
+ expect(FlinkLogFactory.getGlobalLevel()).toBe("info"); // Default
154
+ });
155
+
156
+ it("should be case-insensitive", () => {
157
+ process.env.LOG_LEVEL = "DEBUG";
158
+
159
+ (FlinkLogFactory as any).initialized = false;
160
+ FlinkLogFactory.configure();
161
+
162
+ expect(FlinkLogFactory.getGlobalLevel()).toBe("debug");
163
+ });
164
+ });
165
+
166
+ describe("Config File Loading", () => {
167
+ it("should parse components map with wildcards", () => {
168
+ const config = {
169
+ global: "info" as const,
170
+ showTimestamps: false,
171
+ components: {
172
+ "flink.ai.openai": "trace" as const,
173
+ "flink.ai.*": "debug" as const,
174
+ "flink.database.**": "warn" as const,
175
+ "flink.handlers.": "info" as const
176
+ }
177
+ };
178
+
179
+ (FlinkLogFactory as any).initialized = false;
180
+ FlinkLogFactory.configure(config);
181
+
182
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("trace");
183
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.anthropic")).toBe("debug");
184
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database.mongodb.connection")).toBe("warn");
185
+ expect(FlinkLogFactory.getEffectiveLevel("flink.handlers.car")).toBe("info");
186
+ expect(FlinkLogFactory.getShowTimestamps()).toBe(false);
187
+ });
188
+
189
+ it("should prioritize config file over env var", () => {
190
+ process.env.LOG_LEVEL = "error";
191
+
192
+ const config = {
193
+ global: "debug" as const,
194
+ components: {}
195
+ };
196
+
197
+ (FlinkLogFactory as any).initialized = false;
198
+ FlinkLogFactory.configure(config);
199
+
200
+ expect(FlinkLogFactory.getGlobalLevel()).toBe("debug"); // Config wins
201
+ });
202
+ });
203
+
204
+ describe("Programmatic API", () => {
205
+ it("should allow setting hierarchical levels", () => {
206
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
207
+
208
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug");
209
+ });
210
+
211
+ it("should allow setting wildcard levels", () => {
212
+ FlinkLogFactory.setWildcardLevel("flink.ai.*", "trace");
213
+
214
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("trace");
215
+ });
216
+
217
+ it("should allow setting exact levels", () => {
218
+ FlinkLogFactory.setComponentLevel("flink.ai.openai", "trace");
219
+
220
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("trace");
221
+ });
222
+
223
+ it("should allow clearing exact levels", () => {
224
+ FlinkLogFactory.setComponentLevel("flink.ai.openai", "trace");
225
+ FlinkLogFactory.setComponentLevel("flink.ai.openai", null);
226
+
227
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBeNull();
228
+ });
229
+
230
+ it("should allow resetting hierarchical levels", () => {
231
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
232
+ FlinkLogFactory.resetHierarchicalLevels();
233
+
234
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBeNull();
235
+ });
236
+
237
+ it("should allow resetting wildcard levels", () => {
238
+ FlinkLogFactory.setWildcardLevel("flink.ai.*", "debug");
239
+ FlinkLogFactory.resetWildcardLevels();
240
+
241
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBeNull();
242
+ });
243
+ });
244
+
245
+ describe("Edge Cases", () => {
246
+ it("should handle empty segments in pattern", () => {
247
+ // Double dots should be normalized
248
+ FlinkLogFactory.setHierarchicalLevel("flink..ai", "debug");
249
+
250
+ // Should not match due to empty segment
251
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBeNull();
252
+ });
253
+
254
+ it("should handle single segment names", () => {
255
+ FlinkLogFactory.setComponentLevel("performance", "debug");
256
+
257
+ expect(FlinkLogFactory.getEffectiveLevel("performance")).toBe("debug");
258
+ });
259
+
260
+ it("should handle flink prefix requirement", () => {
261
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
262
+
263
+ // Without flink prefix
264
+ expect(FlinkLogFactory.getEffectiveLevel("ai.openai")).toBeNull();
265
+
266
+ // With flink prefix
267
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug");
268
+ });
269
+
270
+ it("should not match partial prefix", () => {
271
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
272
+
273
+ // Should not match "flink.air" - must be followed by dot
274
+ expect(FlinkLogFactory.getEffectiveLevel("flink.airline")).toBeNull();
275
+ });
276
+
277
+ it("should handle exact match for name with dots", () => {
278
+ FlinkLogFactory.setComponentLevel("flink.ai.openai", "trace");
279
+
280
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("trace");
281
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.v4")).toBeNull();
282
+ });
283
+ });
284
+
285
+ describe("Specificity Ordering", () => {
286
+ it("should sort hierarchical configs by specificity", () => {
287
+ FlinkLogFactory.setHierarchicalLevel("flink", "warn");
288
+ FlinkLogFactory.setHierarchicalLevel("flink.ai.openai", "trace");
289
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
290
+
291
+ // Most specific should win
292
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.v4")).toBe("trace");
293
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.claude")).toBe("debug");
294
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database")).toBe("warn");
295
+ });
296
+
297
+ it("should sort wildcard configs by specificity", () => {
298
+ FlinkLogFactory.setWildcardLevel("flink.*", "warn");
299
+ FlinkLogFactory.setWildcardLevel("flink.ai.**", "trace");
300
+ FlinkLogFactory.setWildcardLevel("flink.ai.*", "debug");
301
+
302
+ // More specific wildcard should win
303
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug"); // flink.ai.* (2 segments)
304
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.v4")).toBe("trace"); // flink.ai.** (2 segments, multi-level)
305
+ });
306
+ });
307
+
308
+ describe("Real-World Scenarios", () => {
309
+ it("should handle typical development setup", () => {
310
+ FlinkLogFactory.setGlobalLevel("warn");
311
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "debug");
312
+ FlinkLogFactory.setComponentLevel("flink.ai.openai", "trace");
313
+
314
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("trace"); // Trace for OpenAI
315
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.anthropic")).toBe("debug"); // Debug for other AI
316
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database.mongodb")).toBeNull(); // Warn (global)
317
+ });
318
+
319
+ it("should handle production debugging", () => {
320
+ FlinkLogFactory.setGlobalLevel("error");
321
+ FlinkLogFactory.setHierarchicalLevel("flink.ai.openai", "debug");
322
+
323
+ // Only OpenAI components get debug, everything else is error
324
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("debug");
325
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai.streaming")).toBe("debug");
326
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.anthropic")).toBeNull(); // Error (global)
327
+ });
328
+
329
+ it("should handle test environment", () => {
330
+ FlinkLogFactory.setHierarchicalLevel("flink.ai", "trace");
331
+ FlinkLogFactory.setHierarchicalLevel("flink.database", "debug");
332
+
333
+ expect(FlinkLogFactory.getEffectiveLevel("flink.ai.openai")).toBe("trace");
334
+ expect(FlinkLogFactory.getEffectiveLevel("flink.database.mongodb")).toBe("debug");
335
+ });
336
+ });
337
+ });
@@ -60,7 +60,7 @@ describe("FlinkRepo", () => {
60
60
  it("should update document", async () => {
61
61
  const createdDoc = await repo.create({ name: "bar" });
62
62
 
63
- const updatedDoc = await repo.updateOne(createdDoc._id + "", {
63
+ const updatedDoc = await repo.updateById(createdDoc._id + "", {
64
64
  name: "foo",
65
65
  "nested.field": 1,
66
66
  });
@@ -0,0 +1,174 @@
1
+ import { LeaderElection } from "../src/LeaderElection";
2
+
3
+ describe("LeaderElection", () => {
4
+ let mockCollection: any;
5
+ let mockDb: any;
6
+
7
+ beforeEach(() => {
8
+ mockCollection = {
9
+ createIndex: jasmine.createSpy("createIndex").and.resolveTo(undefined),
10
+ findOneAndUpdate: jasmine.createSpy("findOneAndUpdate"),
11
+ deleteOne: jasmine.createSpy("deleteOne").and.resolveTo(undefined),
12
+ };
13
+
14
+ mockDb = {
15
+ collection: jasmine.createSpy("collection").and.returnValue(mockCollection),
16
+ };
17
+ });
18
+
19
+ it("should create collection with provided name", () => {
20
+ new LeaderElection(mockDb, { collectionName: "_my_leader" });
21
+ expect(mockDb.collection).toHaveBeenCalledWith("_my_leader");
22
+ });
23
+
24
+ it("should use default collection name", () => {
25
+ new LeaderElection(mockDb);
26
+ expect(mockDb.collection).toHaveBeenCalledWith("_flink_leader");
27
+ });
28
+
29
+ it("should create TTL index on start", async () => {
30
+ const le = new LeaderElection(mockDb, { leaseDurationMs: 10000 });
31
+
32
+ mockCollection.findOneAndUpdate.and.resolveTo({
33
+ instanceId: "will-not-match",
34
+ });
35
+
36
+ await le.start(
37
+ () => {},
38
+ () => {}
39
+ );
40
+ await le.stop();
41
+
42
+ expect(mockCollection.createIndex).toHaveBeenCalledWith(
43
+ { lastHeartbeat: 1 },
44
+ { expireAfterSeconds: 20 } // 2x lease duration in seconds
45
+ );
46
+ });
47
+
48
+ it("should call onBecameLeader when claiming leadership", async () => {
49
+ const onBecameLeader = jasmine.createSpy("onBecameLeader");
50
+ const onLostLeadership = jasmine.createSpy("onLostLeadership");
51
+
52
+ const le = new LeaderElection(mockDb, {
53
+ leaseDurationMs: 10000,
54
+ heartbeatIntervalMs: 50000,
55
+ });
56
+
57
+ // findOneAndUpdate returns a document with our instanceId
58
+ // We need to intercept the instanceId set in the update
59
+ mockCollection.findOneAndUpdate.and.callFake((_filter: any, update: any) => {
60
+ return Promise.resolve({
61
+ instanceId: update.$set.instanceId,
62
+ });
63
+ });
64
+
65
+ await le.start(onBecameLeader, onLostLeadership);
66
+
67
+ expect(le.isLeader).toBe(true);
68
+ expect(onBecameLeader).toHaveBeenCalledTimes(1);
69
+ expect(onLostLeadership).not.toHaveBeenCalled();
70
+
71
+ await le.stop();
72
+ });
73
+
74
+ it("should not become leader when another instance holds the lock", async () => {
75
+ const onBecameLeader = jasmine.createSpy("onBecameLeader");
76
+ const onLostLeadership = jasmine.createSpy("onLostLeadership");
77
+
78
+ const le = new LeaderElection(mockDb, {
79
+ leaseDurationMs: 10000,
80
+ heartbeatIntervalMs: 50000,
81
+ });
82
+
83
+ // findOneAndUpdate returns a document with a different instanceId
84
+ mockCollection.findOneAndUpdate.and.resolveTo({
85
+ instanceId: "other-instance",
86
+ });
87
+
88
+ await le.start(onBecameLeader, onLostLeadership);
89
+
90
+ expect(le.isLeader).toBe(false);
91
+ expect(onBecameLeader).not.toHaveBeenCalled();
92
+
93
+ await le.stop();
94
+ });
95
+
96
+ it("should handle duplicate key error gracefully", async () => {
97
+ const onBecameLeader = jasmine.createSpy("onBecameLeader");
98
+ const onLostLeadership = jasmine.createSpy("onLostLeadership");
99
+
100
+ const le = new LeaderElection(mockDb, {
101
+ leaseDurationMs: 10000,
102
+ heartbeatIntervalMs: 50000,
103
+ });
104
+
105
+ const duplicateKeyError = new Error("E11000 duplicate key");
106
+ (duplicateKeyError as any).code = 11000;
107
+ mockCollection.findOneAndUpdate.and.rejectWith(duplicateKeyError);
108
+
109
+ await le.start(onBecameLeader, onLostLeadership);
110
+
111
+ expect(le.isLeader).toBe(false);
112
+ expect(onBecameLeader).not.toHaveBeenCalled();
113
+
114
+ await le.stop();
115
+ });
116
+
117
+ it("should release leadership on stop", async () => {
118
+ const le = new LeaderElection(mockDb, {
119
+ leaseDurationMs: 10000,
120
+ heartbeatIntervalMs: 50000,
121
+ });
122
+
123
+ mockCollection.findOneAndUpdate.and.callFake((_filter: any, update: any) => {
124
+ return Promise.resolve({
125
+ instanceId: update.$set.instanceId,
126
+ });
127
+ });
128
+
129
+ await le.start(
130
+ () => {},
131
+ () => {}
132
+ );
133
+
134
+ expect(le.isLeader).toBe(true);
135
+
136
+ await le.stop();
137
+
138
+ expect(mockCollection.deleteOne).toHaveBeenCalled();
139
+ expect(le.isLeader).toBe(false);
140
+ });
141
+
142
+ it("should call onLostLeadership when losing the lock", async () => {
143
+ const onBecameLeader = jasmine.createSpy("onBecameLeader");
144
+ const onLostLeadership = jasmine.createSpy("onLostLeadership");
145
+
146
+ const le = new LeaderElection(mockDb, {
147
+ leaseDurationMs: 10000,
148
+ heartbeatIntervalMs: 100,
149
+ });
150
+
151
+ let callCount = 0;
152
+ mockCollection.findOneAndUpdate.and.callFake((_filter: any, update: any) => {
153
+ callCount++;
154
+ if (callCount === 1) {
155
+ // First call: we become leader
156
+ return Promise.resolve({ instanceId: update.$set.instanceId });
157
+ }
158
+ // Subsequent calls: another instance took over
159
+ return Promise.resolve({ instanceId: "other-instance" });
160
+ });
161
+
162
+ await le.start(onBecameLeader, onLostLeadership);
163
+
164
+ expect(le.isLeader).toBe(true);
165
+
166
+ // Wait for next heartbeat cycle
167
+ await new Promise((resolve) => setTimeout(resolve, 200));
168
+
169
+ expect(onLostLeadership).toHaveBeenCalledTimes(1);
170
+ expect(le.isLeader).toBe(false);
171
+
172
+ await le.stop();
173
+ });
174
+ });
@@ -0,0 +1,139 @@
1
+ import { FlinkAgent, StreamChunk } from "../src/ai/FlinkAgent";
2
+ import { FlinkContext } from "../src/FlinkContext";
3
+ import { LLMAdapter, LLMStreamChunk } from "../src/ai/LLMAdapter";
4
+
5
+ describe("Streaming Integration", () => {
6
+ let mockCtx: FlinkContext;
7
+
8
+ beforeEach(() => {
9
+ mockCtx = {
10
+ repos: {},
11
+ plugins: {},
12
+ };
13
+ });
14
+
15
+ class StreamingTestAgent extends FlinkAgent<FlinkContext> {
16
+ id = "streaming-test-agent";
17
+ description = "Test agent for streaming";
18
+ instructions() { return "You are a test agent"; }
19
+ tools = [];
20
+
21
+ async query(message: string) {
22
+ const response = this.execute({ message });
23
+ return response;
24
+ }
25
+
26
+ setContext(ctx: FlinkContext) {
27
+ (this as any).ctx = ctx;
28
+ }
29
+
30
+ setMockLLMAdapter(adapter: LLMAdapter) {
31
+ this.__init(new Map([["default", adapter]]), {});
32
+ }
33
+ }
34
+
35
+ it("should stream text deltas in real-time", async () => {
36
+ const agent = new StreamingTestAgent();
37
+ agent.setContext(mockCtx);
38
+
39
+ // Mock LLM that streams chunks
40
+ const mockAdapter: LLMAdapter = {
41
+ stream: async function* () {
42
+ yield { type: "text", delta: "Hello " } as LLMStreamChunk;
43
+ yield { type: "text", delta: "world " } as LLMStreamChunk;
44
+ yield { type: "text", delta: "from agent" } as LLMStreamChunk;
45
+ yield { type: "usage", usage: { inputTokens: 10, outputTokens: 5 } } as LLMStreamChunk;
46
+ yield { type: "done", stopReason: "end_turn" } as LLMStreamChunk;
47
+ },
48
+ };
49
+
50
+ agent.setMockLLMAdapter(mockAdapter);
51
+
52
+ const response = agent.query("Hello");
53
+
54
+ const textChunks: string[] = [];
55
+ for await (const text of (await response).textStream) {
56
+ textChunks.push(text);
57
+ }
58
+
59
+ expect(textChunks.length).toBe(3);
60
+ expect(textChunks.join("")).toBe("Hello world from agent");
61
+ });
62
+
63
+ it("should emit text_delta events during streaming", async () => {
64
+ const agent = new StreamingTestAgent();
65
+ agent.setContext(mockCtx);
66
+
67
+ const mockAdapter: LLMAdapter = {
68
+ stream: async function* () {
69
+ yield { type: "text", delta: "Pro" } as LLMStreamChunk;
70
+ yield { type: "text", delta: "gressive " } as LLMStreamChunk;
71
+ yield { type: "text", delta: "text" } as LLMStreamChunk;
72
+ yield { type: "usage", usage: { inputTokens: 10, outputTokens: 5 } } as LLMStreamChunk;
73
+ yield { type: "done", stopReason: "end_turn" } as LLMStreamChunk;
74
+ },
75
+ };
76
+
77
+ agent.setMockLLMAdapter(mockAdapter);
78
+
79
+ const response = agent.query("Hello");
80
+
81
+ const events: StreamChunk[] = [];
82
+ for await (const chunk of (await response).fullStream) {
83
+ events.push(chunk);
84
+ }
85
+
86
+ const textDeltas = events.filter((e) => e.type === "text_delta");
87
+ expect(textDeltas.length).toBe(3);
88
+ expect((textDeltas[0] as any).delta).toBe("Pro");
89
+ expect((textDeltas[1] as any).delta).toBe("gressive ");
90
+ expect((textDeltas[2] as any).delta).toBe("text");
91
+ });
92
+
93
+ it("should allow consuming result after streaming completes", async () => {
94
+ const agent = new StreamingTestAgent();
95
+ agent.setContext(mockCtx);
96
+
97
+ const mockAdapter: LLMAdapter = {
98
+ stream: async function* () {
99
+ yield { type: "text", delta: "Complete " } as LLMStreamChunk;
100
+ yield { type: "text", delta: "response" } as LLMStreamChunk;
101
+ yield { type: "usage", usage: { inputTokens: 10, outputTokens: 5 } } as LLMStreamChunk;
102
+ yield { type: "done", stopReason: "end_turn" } as LLMStreamChunk;
103
+ },
104
+ };
105
+
106
+ agent.setMockLLMAdapter(mockAdapter);
107
+
108
+ const response = agent.query("Hello");
109
+
110
+ // Consume stream first
111
+ const textChunks: string[] = [];
112
+ for await (const text of (await response).textStream) {
113
+ textChunks.push(text);
114
+ }
115
+
116
+ // Then await result
117
+ const result = await (await response).result;
118
+
119
+ expect(result.message).toContain("Complete response");
120
+ expect(textChunks.join("")).toBe("Complete response");
121
+ });
122
+
123
+ it("should handle streaming errors gracefully", async () => {
124
+ const agent = new StreamingTestAgent();
125
+ agent.setContext(mockCtx);
126
+
127
+ const mockAdapter: LLMAdapter = {
128
+ stream: async function* () {
129
+ throw new Error("Streaming failed");
130
+ },
131
+ };
132
+
133
+ agent.setMockLLMAdapter(mockAdapter);
134
+
135
+ const response = agent.query("Hello");
136
+
137
+ await expectAsync((await response).result).toBeRejectedWithError("Streaming failed");
138
+ });
139
+ });