@flink-app/flink 1.0.0 → 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 +991 -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 +823 -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 +157 -18
- 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 +27 -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.routeOrdering.spec.ts +61 -0
- package/spec/FlinkApp.undefinedResponse.spec.ts +123 -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 +895 -154
- package/src/FlinkContext.ts +43 -0
- package/src/FlinkErrors.ts +32 -12
- package/src/FlinkHttpHandler.ts +182 -20
- 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,171 @@
|
|
|
1
|
+
import { FlinkApp, autoRegisteredJobs } from "../src/FlinkApp";
|
|
2
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
3
|
+
import { FlinkJobFile } from "../src/FlinkJob";
|
|
4
|
+
import { FlinkLogFactory } from "../src/FlinkLogFactory";
|
|
5
|
+
|
|
6
|
+
interface TestContext extends FlinkContext {}
|
|
7
|
+
|
|
8
|
+
describe("FlinkJob error handling", () => {
|
|
9
|
+
let app: FlinkApp<TestContext>;
|
|
10
|
+
let consoleErrorSpy: jasmine.Spy;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
consoleErrorSpy = spyOn(console, "error");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
autoRegisteredJobs.length = 0;
|
|
18
|
+
if (app?.started) {
|
|
19
|
+
await app.stop();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should catch and log errors from afterDelay 0ms jobs without crashing", async () => {
|
|
24
|
+
const job: FlinkJobFile = {
|
|
25
|
+
Job: { id: "failing-job-0ms", afterDelay: "0ms" },
|
|
26
|
+
default: async () => {
|
|
27
|
+
throw new Error("Job error 0ms");
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
autoRegisteredJobs.push(job);
|
|
32
|
+
|
|
33
|
+
app = new FlinkApp<TestContext>({ name: "test-job-errors-0ms", disableHttpServer: true });
|
|
34
|
+
await app.start();
|
|
35
|
+
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
37
|
+
|
|
38
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should catch and log errors from afterDelay jobs without crashing", async () => {
|
|
42
|
+
const job: FlinkJobFile = {
|
|
43
|
+
Job: { id: "failing-job-delay", afterDelay: "10ms" },
|
|
44
|
+
default: async () => {
|
|
45
|
+
throw new Error("Job error with delay");
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
autoRegisteredJobs.push(job);
|
|
50
|
+
|
|
51
|
+
app = new FlinkApp<TestContext>({ name: "test-job-errors-delay", disableHttpServer: true });
|
|
52
|
+
await app.start();
|
|
53
|
+
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
55
|
+
|
|
56
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should catch and log errors from interval jobs without crashing", async () => {
|
|
60
|
+
const job: FlinkJobFile = {
|
|
61
|
+
Job: { id: "failing-interval-job", interval: "10ms" },
|
|
62
|
+
default: async () => {
|
|
63
|
+
throw new Error("Interval job error");
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
autoRegisteredJobs.push(job);
|
|
68
|
+
|
|
69
|
+
app = new FlinkApp<TestContext>({ name: "test-job-errors-interval", disableHttpServer: true });
|
|
70
|
+
await app.start();
|
|
71
|
+
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
73
|
+
|
|
74
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should run afterDelay 0ms job exactly once", async () => {
|
|
78
|
+
let runCount = 0;
|
|
79
|
+
|
|
80
|
+
const job: FlinkJobFile = {
|
|
81
|
+
Job: { id: "once-job-0ms", afterDelay: "0ms" },
|
|
82
|
+
default: async () => {
|
|
83
|
+
runCount++;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
autoRegisteredJobs.push(job);
|
|
88
|
+
|
|
89
|
+
app = new FlinkApp<TestContext>({ name: "test-job-once-0ms", disableHttpServer: true });
|
|
90
|
+
await app.start();
|
|
91
|
+
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
93
|
+
|
|
94
|
+
expect(runCount).toBe(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("FlinkJob leader election with runOnAllInstances", () => {
|
|
99
|
+
let app: FlinkApp<TestContext>;
|
|
100
|
+
|
|
101
|
+
afterEach(async () => {
|
|
102
|
+
autoRegisteredJobs.length = 0;
|
|
103
|
+
if (app?.started) {
|
|
104
|
+
await app.stop();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should run all jobs when leader election is enabled but no db is configured", async () => {
|
|
109
|
+
const schedulerLog = FlinkLogFactory.createLogger("flink.scheduler");
|
|
110
|
+
const warnSpy = spyOn(schedulerLog, "warn");
|
|
111
|
+
|
|
112
|
+
let leaderJobRan = false;
|
|
113
|
+
let allInstanceJobRan = false;
|
|
114
|
+
|
|
115
|
+
autoRegisteredJobs.push({
|
|
116
|
+
Job: { id: "leader-only-job", afterDelay: "0ms" },
|
|
117
|
+
default: async () => {
|
|
118
|
+
leaderJobRan = true;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
autoRegisteredJobs.push({
|
|
123
|
+
Job: { id: "all-instance-job", afterDelay: "0ms", runOnAllInstances: true },
|
|
124
|
+
default: async () => {
|
|
125
|
+
allInstanceJobRan = true;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
app = new FlinkApp<TestContext>({
|
|
130
|
+
name: "test-leader-no-db",
|
|
131
|
+
disableHttpServer: true,
|
|
132
|
+
scheduling: { leaderElection: true },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await app.start();
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
137
|
+
|
|
138
|
+
// Without db, falls back to running all jobs
|
|
139
|
+
expect(leaderJobRan).toBe(true);
|
|
140
|
+
expect(allInstanceJobRan).toBe(true);
|
|
141
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
142
|
+
const warnMessage = warnSpy.calls.mostRecent().args[0];
|
|
143
|
+
expect(warnMessage).toContain("Leader election is enabled but no database is configured");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should not start scheduler for leader-only jobs when not leader", async () => {
|
|
147
|
+
// Without a real MongoDB, we can't fully test leader election.
|
|
148
|
+
// This test verifies that when leaderElection is enabled without db,
|
|
149
|
+
// the warning is shown and all jobs still run as a fallback.
|
|
150
|
+
let jobRanCount = 0;
|
|
151
|
+
|
|
152
|
+
autoRegisteredJobs.push({
|
|
153
|
+
Job: { id: "interval-job", interval: "50ms" },
|
|
154
|
+
default: async () => {
|
|
155
|
+
jobRanCount++;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app = new FlinkApp<TestContext>({
|
|
160
|
+
name: "test-no-db-fallback",
|
|
161
|
+
disableHttpServer: true,
|
|
162
|
+
scheduling: { leaderElection: true },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await app.start();
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
167
|
+
|
|
168
|
+
// Should have run at least once since it falls back without db
|
|
169
|
+
expect(jobRanCount).toBeGreaterThan(0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -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
|
+
});
|