@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.
- package/CHANGELOG.md +1051 -0
- package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
- package/SIMPLE_AST_FEASIBILITY.md +570 -0
- package/bin/flink.ts +13 -2
- package/cli/build.ts +24 -44
- package/cli/clean.ts +13 -25
- package/cli/cli-utils.ts +190 -17
- package/cli/dev.ts +252 -0
- package/cli/loadEnvFiles.ts +116 -0
- package/cli/run.ts +45 -62
- package/dist/bin/flink.js +61 -2
- package/dist/cli/build.js +20 -25
- package/dist/cli/clean.js +12 -10
- package/dist/cli/cli-utils.d.ts +34 -3
- package/dist/cli/cli-utils.js +193 -12
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.js +279 -0
- package/dist/cli/loadEnvFiles.d.ts +30 -0
- package/dist/cli/loadEnvFiles.js +113 -0
- package/dist/cli/run.js +47 -46
- package/dist/src/DependencyTracker.d.ts +44 -0
- package/dist/src/DependencyTracker.js +239 -0
- package/dist/src/FlinkApp.d.ts +163 -10
- package/dist/src/FlinkApp.js +847 -184
- package/dist/src/FlinkContext.d.ts +41 -0
- package/dist/src/FlinkErrors.d.ts +19 -6
- package/dist/src/FlinkErrors.js +36 -42
- package/dist/src/FlinkHttpHandler.d.ts +219 -26
- package/dist/src/FlinkHttpHandler.js +37 -1
- package/dist/src/FlinkJob.d.ts +10 -0
- package/dist/src/FlinkLog.d.ts +82 -18
- package/dist/src/FlinkLog.js +165 -13
- package/dist/src/FlinkLogFactory.d.ts +288 -0
- package/dist/src/FlinkLogFactory.js +619 -0
- package/dist/src/FlinkRepo.d.ts +10 -2
- package/dist/src/FlinkRepo.js +11 -1
- package/dist/src/FlinkRequestContext.d.ts +63 -0
- package/dist/src/FlinkRequestContext.js +74 -0
- package/dist/src/FlinkResponse.d.ts +6 -0
- package/dist/src/FlinkService.d.ts +38 -0
- package/dist/src/FlinkService.js +46 -0
- package/dist/src/LeaderElection.d.ts +45 -0
- package/dist/src/LeaderElection.js +269 -0
- package/dist/src/SchemaCache.d.ts +84 -0
- package/dist/src/SchemaCache.js +289 -0
- package/dist/src/TypeScriptCompiler.d.ts +161 -51
- package/dist/src/TypeScriptCompiler.js +1253 -617
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +760 -0
- package/dist/src/ai/ConversationAgent.d.ts +279 -0
- package/dist/src/ai/ConversationAgent.js +404 -0
- package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
- package/dist/src/ai/ConversationFlinkAgent.js +404 -0
- package/dist/src/ai/FlinkAgent.d.ts +690 -0
- package/dist/src/ai/FlinkAgent.js +729 -0
- package/dist/src/ai/FlinkTool.d.ts +135 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
- package/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/dist/src/ai/LLMAdapter.d.ts +148 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
- package/dist/src/ai/PersistentFlinkAgent.js +403 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
- package/dist/src/ai/SubAgentExecutor.js +223 -0
- package/dist/src/ai/ToolExecutor.d.ts +64 -0
- package/dist/src/ai/ToolExecutor.js +497 -0
- package/dist/src/ai/agentInstructions.d.ts +68 -0
- package/dist/src/ai/agentInstructions.js +286 -0
- package/dist/src/ai/index.d.ts +8 -0
- package/dist/src/ai/index.js +26 -0
- package/dist/src/ai/instructionFileLoader.d.ts +44 -0
- package/dist/src/ai/instructionFileLoader.js +179 -0
- package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +17 -0
- package/dist/src/loadPluginSchemas.d.ts +45 -0
- package/dist/src/loadPluginSchemas.js +143 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
- package/dist/src/schema-extraction/index.d.ts +2 -0
- package/dist/src/schema-extraction/index.js +20 -0
- package/dist/src/schema-extraction/types.d.ts +31 -0
- package/dist/src/schema-extraction/types.js +2 -0
- package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
- package/dist/src/utils/loadFlinkConfig.js +77 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
- package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
- package/dist/src/workers/WorkerPool.d.ts +60 -0
- package/dist/src/workers/WorkerPool.js +306 -0
- package/examples/logging-hierarchical-example.ts +125 -0
- package/package.json +29 -4
- package/readme.md +499 -0
- package/spec/AgentDescendantDetection.spec.ts +335 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentObserver.spec.ts +266 -0
- package/spec/AgentRunner.spec.ts +1062 -0
- package/spec/AsyncLocalStorageContext.spec.ts +223 -0
- package/spec/ConversationHooks.spec.ts +257 -0
- package/spec/FlinkAgent.spec.ts +681 -0
- package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
- package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/FlinkApp.query.spec.ts +107 -0
- package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
- package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
- package/spec/FlinkApp.validationMode.spec.ts +155 -0
- package/spec/FlinkJob.spec.ts +171 -0
- package/spec/FlinkLogFactory.spec.ts +337 -0
- package/spec/FlinkRepo.spec.ts +1 -1
- package/spec/LeaderElection.spec.ts +174 -0
- package/spec/StreamingIntegration.spec.ts +139 -0
- package/spec/ToolExecutor.spec.ts +465 -0
- package/spec/TypeScriptCompiler.spec.ts +1 -1
- package/spec/TypeScriptSourceParser.spec.ts +1215 -0
- package/spec/TypeScriptTokenizer.spec.ts +366 -0
- package/spec/ai/ContextCompaction.spec.ts +405 -0
- package/spec/ai/ConversationAgent.spec.ts +520 -0
- package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
- package/spec/ai/agentInstructions.spec.ts +358 -0
- package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
- package/spec/fixtures/agent-instructions/simple.md +3 -0
- package/spec/fixtures/agent-instructions/template.md +18 -0
- package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
- package/spec/mock-project/dist/.tsbuildinfo +1 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
- package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
- package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
- package/spec/mock-project/dist/src/FlinkContext.js +2 -0
- package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
- package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
- package/spec/mock-project/dist/src/FlinkJob.js +2 -0
- package/spec/mock-project/dist/src/FlinkLog.js +119 -0
- package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
- package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
- package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
- package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
- package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
- package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
- package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
- package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
- package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
- package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
- package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
- package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
- package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
- package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +52 -76
- package/spec/mock-project/dist/src/index.js.map +1 -0
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
- package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/Car.js +3 -1
- package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/schema-generation-nested-objects.spec.ts +97 -0
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +78 -0
- package/spec/utils.spec.ts +13 -13
- package/src/DependencyTracker.ts +166 -0
- package/src/FlinkApp.ts +919 -155
- package/src/FlinkContext.ts +43 -0
- package/src/FlinkErrors.ts +32 -12
- package/src/FlinkHttpHandler.ts +246 -28
- package/src/FlinkJob.ts +11 -0
- package/src/FlinkLog.ts +119 -12
- package/src/FlinkLogFactory.ts +699 -0
- package/src/FlinkRepo.ts +10 -3
- package/src/FlinkRequestContext.ts +95 -0
- package/src/FlinkResponse.ts +6 -0
- package/src/FlinkService.ts +49 -0
- package/src/LeaderElection.ts +203 -0
- package/src/SchemaCache.ts +232 -0
- package/src/TypeScriptCompiler.ts +1347 -610
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +646 -0
- package/src/ai/ConversationAgent.ts +413 -0
- package/src/ai/FlinkAgent.ts +1069 -0
- package/src/ai/FlinkTool.ts +165 -0
- package/src/ai/InMemoryConversationAgent.ts +149 -0
- package/src/ai/LLMAdapter.ts +126 -0
- package/src/ai/ToolExecutor.ts +485 -0
- package/src/ai/agentInstructions.ts +245 -0
- package/src/ai/index.ts +8 -0
- package/src/ai/instructionFileLoader.ts +156 -0
- package/src/auth/FlinkAuthPlugin.ts +2 -1
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +14 -0
- package/src/loadPluginSchemas.ts +141 -0
- package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
- package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
- package/src/schema-extraction/index.ts +2 -0
- package/src/schema-extraction/types.ts +34 -0
- package/src/utils/loadFlinkConfig.ts +89 -0
- package/src/utils.ts +52 -0
- 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
|
+
});
|
package/spec/FlinkRepo.spec.ts
CHANGED
|
@@ -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.
|
|
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
|
+
});
|