lynkr 7.2.5 → 8.0.1

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 (124) hide show
  1. package/README.md +3 -3
  2. package/config/model-tiers.json +89 -0
  3. package/install.sh +6 -1
  4. package/package.json +4 -2
  5. package/scripts/setup.js +0 -1
  6. package/src/agents/executor.js +14 -6
  7. package/src/api/middleware/session.js +15 -2
  8. package/src/api/openai-router.js +162 -37
  9. package/src/api/providers-handler.js +15 -1
  10. package/src/api/router.js +107 -2
  11. package/src/budget/index.js +4 -3
  12. package/src/clients/databricks.js +431 -234
  13. package/src/clients/gpt-utils.js +181 -0
  14. package/src/clients/ollama-utils.js +66 -140
  15. package/src/clients/routing.js +0 -1
  16. package/src/clients/standard-tools.js +99 -3
  17. package/src/config/index.js +133 -35
  18. package/src/context/toon.js +173 -0
  19. package/src/logger/index.js +23 -0
  20. package/src/orchestrator/index.js +688 -213
  21. package/src/routing/agentic-detector.js +320 -0
  22. package/src/routing/complexity-analyzer.js +202 -2
  23. package/src/routing/cost-optimizer.js +305 -0
  24. package/src/routing/index.js +168 -159
  25. package/src/routing/model-tiers.js +365 -0
  26. package/src/server.js +4 -14
  27. package/src/sessions/cleanup.js +3 -3
  28. package/src/sessions/record.js +10 -1
  29. package/src/sessions/store.js +7 -2
  30. package/src/tools/agent-task.js +48 -1
  31. package/src/tools/index.js +19 -2
  32. package/src/tools/lazy-loader.js +7 -0
  33. package/src/tools/tinyfish.js +358 -0
  34. package/src/tools/truncate.js +1 -0
  35. package/.github/FUNDING.yml +0 -15
  36. package/.github/workflows/README.md +0 -215
  37. package/.github/workflows/ci.yml +0 -69
  38. package/.github/workflows/index.yml +0 -62
  39. package/.github/workflows/web-tools-tests.yml +0 -56
  40. package/CITATIONS.bib +0 -6
  41. package/CLAWROUTER_ROUTING_PLAN.md +0 -910
  42. package/DEPLOYMENT.md +0 -1001
  43. package/LYNKR-TUI-PLAN.md +0 -984
  44. package/PERFORMANCE-REPORT.md +0 -866
  45. package/PLAN-per-client-model-routing.md +0 -252
  46. package/ROUTER_COMPARISON.md +0 -173
  47. package/TIER_ROUTING_PLAN.md +0 -771
  48. package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
  49. package/docs/BingSiteAuth.xml +0 -4
  50. package/docs/docs-style.css +0 -478
  51. package/docs/docs.html +0 -197
  52. package/docs/google5be250e608e6da39.html +0 -1
  53. package/docs/index.html +0 -577
  54. package/docs/index.md +0 -577
  55. package/docs/robots.txt +0 -4
  56. package/docs/sitemap.xml +0 -44
  57. package/docs/style.css +0 -1223
  58. package/documentation/README.md +0 -100
  59. package/documentation/api.md +0 -806
  60. package/documentation/claude-code-cli.md +0 -672
  61. package/documentation/codex-cli.md +0 -397
  62. package/documentation/contributing.md +0 -571
  63. package/documentation/cursor-integration.md +0 -731
  64. package/documentation/docker.md +0 -867
  65. package/documentation/embeddings.md +0 -760
  66. package/documentation/faq.md +0 -659
  67. package/documentation/features.md +0 -396
  68. package/documentation/headroom.md +0 -519
  69. package/documentation/installation.md +0 -706
  70. package/documentation/memory-system.md +0 -476
  71. package/documentation/production.md +0 -601
  72. package/documentation/providers.md +0 -906
  73. package/documentation/testing.md +0 -629
  74. package/documentation/token-optimization.md +0 -323
  75. package/documentation/tools.md +0 -697
  76. package/documentation/troubleshooting.md +0 -893
  77. package/final-test.js +0 -33
  78. package/headroom-sidecar/config.py +0 -93
  79. package/headroom-sidecar/requirements.txt +0 -14
  80. package/headroom-sidecar/server.py +0 -451
  81. package/monitor-agents.sh +0 -31
  82. package/scripts/audit-log-reader.js +0 -399
  83. package/scripts/compact-dictionary.js +0 -204
  84. package/scripts/test-deduplication.js +0 -448
  85. package/src/db/database.sqlite +0 -0
  86. package/test/README.md +0 -212
  87. package/test/azure-openai-config.test.js +0 -204
  88. package/test/azure-openai-error-resilience.test.js +0 -238
  89. package/test/azure-openai-format-conversion.test.js +0 -354
  90. package/test/azure-openai-integration.test.js +0 -281
  91. package/test/azure-openai-routing.test.js +0 -177
  92. package/test/azure-openai-streaming.test.js +0 -171
  93. package/test/bedrock-integration.test.js +0 -471
  94. package/test/comprehensive-test-suite.js +0 -928
  95. package/test/config-validation.test.js +0 -207
  96. package/test/cursor-integration.test.js +0 -484
  97. package/test/format-conversion.test.js +0 -578
  98. package/test/hybrid-routing-integration.test.js +0 -254
  99. package/test/hybrid-routing-performance.test.js +0 -418
  100. package/test/llamacpp-integration.test.js +0 -863
  101. package/test/lmstudio-integration.test.js +0 -335
  102. package/test/memory/extractor.test.js +0 -398
  103. package/test/memory/retriever.test.js +0 -613
  104. package/test/memory/retriever.test.js.bak +0 -585
  105. package/test/memory/search.test.js +0 -537
  106. package/test/memory/search.test.js.bak +0 -389
  107. package/test/memory/store.test.js +0 -344
  108. package/test/memory/store.test.js.bak +0 -312
  109. package/test/memory/surprise.test.js +0 -300
  110. package/test/memory-performance.test.js +0 -472
  111. package/test/openai-integration.test.js +0 -686
  112. package/test/openrouter-error-resilience.test.js +0 -418
  113. package/test/passthrough-mode.test.js +0 -385
  114. package/test/performance-benchmark.js +0 -351
  115. package/test/performance-tests.js +0 -528
  116. package/test/routing.test.js +0 -219
  117. package/test/web-tools.test.js +0 -329
  118. package/test-agents-simple.js +0 -43
  119. package/test-cli-connection.sh +0 -33
  120. package/test-learning-unit.js +0 -126
  121. package/test-learning.js +0 -112
  122. package/test-parallel-agents.sh +0 -124
  123. package/test-parallel-direct.js +0 -155
  124. package/test-subagents.sh +0 -117
@@ -1,928 +0,0 @@
1
- /**
2
- * Comprehensive Test Suite
3
- *
4
- * Tests all production hardening features:
5
- * - Option 1: Retries, budgets, rate limits, path allowlisting, sandboxing, safe commands (42 tests)
6
- * - Option 2 & 3: Metrics, health checks, shutdown, logging, errors, validation, load shedding, circuit breakers (38 tests)
7
- * - Total: 80 tests
8
- */
9
-
10
- const Database = require("better-sqlite3");
11
- const path = require("path");
12
- const fs = require("fs");
13
-
14
- // Test utilities
15
- function colorize(text, color) {
16
- const colors = {
17
- green: "\x1b[32m",
18
- red: "\x1b[31m",
19
- yellow: "\x1b[33m",
20
- blue: "\x1b[34m",
21
- cyan: "\x1b[36m",
22
- reset: "\x1b[0m",
23
- };
24
- return `${colors[color] || ""}${text}${colors.reset}`;
25
- }
26
-
27
- function formatResult(passed, total) {
28
- const percentage = ((passed / total) * 100).toFixed(1);
29
- const color = percentage === "100.0" ? "green" : percentage >= "80.0" ? "yellow" : "red";
30
- return colorize(`${passed}/${total} (${percentage}%)`, color);
31
- }
32
-
33
- // Test results tracking
34
- const results = {
35
- total: 0,
36
- passed: 0,
37
- failed: 0,
38
- tests: [],
39
- sections: [],
40
- };
41
-
42
- function test(name, fn) {
43
- results.total++;
44
- try {
45
- fn();
46
- results.passed++;
47
- results.tests.push({ name, passed: true });
48
- console.log(colorize("✓", "green"), name);
49
- return true;
50
- } catch (error) {
51
- results.failed++;
52
- results.tests.push({ name, passed: false, error: error.message });
53
- console.log(colorize("✗", "red"), name);
54
- console.log(colorize(` Error: ${error.message}`, "red"));
55
- return false;
56
- }
57
- }
58
-
59
- async function asyncTest(name, fn) {
60
- results.total++;
61
- try {
62
- await fn();
63
- results.passed++;
64
- results.tests.push({ name, passed: true });
65
- console.log(colorize("✓", "green"), name);
66
- return true;
67
- } catch (error) {
68
- results.failed++;
69
- results.tests.push({ name, passed: false, error: error.message });
70
- console.log(colorize("✗", "red"), name);
71
- console.log(colorize(` Error: ${error.message}`, "red"));
72
- return false;
73
- }
74
- }
75
-
76
- function section(name) {
77
- console.log(colorize(`\n=== ${name} ===`, "blue"));
78
- const sectionStart = results.total;
79
- results.sections.push({ name, startIndex: sectionStart });
80
- }
81
-
82
- // =============================================================================
83
- // OPTION 1: CRITICAL PRODUCTION FEATURES
84
- // =============================================================================
85
-
86
- async function testOption1Features() {
87
- console.log(colorize("\n╔═══════════════════════════════════════════════════╗", "cyan"));
88
- console.log(colorize("║ OPTION 1: CRITICAL FEATURES ║", "cyan"));
89
- console.log(colorize("╚═══════════════════════════════════════════════════╝", "cyan"));
90
-
91
- // Feature 1 & 2: Retry Logic and 429 Handling
92
- section("Feature 1 & 2: Retry Logic and 429 Handling");
93
-
94
- const { withRetry, calculateDelay, isRetryable, DEFAULT_CONFIG } = require("./src/clients/retry");
95
-
96
- test("Retry config has required fields", () => {
97
- if (!DEFAULT_CONFIG.maxRetries) throw new Error("Missing maxRetries");
98
- if (!DEFAULT_CONFIG.initialDelay) throw new Error("Missing initialDelay");
99
- if (!DEFAULT_CONFIG.maxDelay) throw new Error("Missing maxDelay");
100
- if (!DEFAULT_CONFIG.retryableStatuses) throw new Error("Missing retryableStatuses");
101
- });
102
-
103
- test("Exponential backoff increases delays", () => {
104
- const delay0 = calculateDelay(0, DEFAULT_CONFIG);
105
- const delay1 = calculateDelay(1, DEFAULT_CONFIG);
106
- const delay2 = calculateDelay(2, DEFAULT_CONFIG);
107
-
108
- if (delay1 <= delay0) throw new Error(`Delay 1 (${delay1}) should be > delay 0 (${delay0})`);
109
- if (delay2 <= delay1) throw new Error(`Delay 2 (${delay2}) should be > delay 1 (${delay1})`);
110
- });
111
-
112
- test("Delays have jitter applied", () => {
113
- const delay1 = calculateDelay(1, DEFAULT_CONFIG);
114
- const delay2 = calculateDelay(1, DEFAULT_CONFIG);
115
- // Jitter means delays should vary slightly
116
- if (delay1 === delay2) {
117
- // This might occasionally be equal, but unlikely
118
- console.log(" Note: Jitter caused same delay (rare but possible)");
119
- }
120
- });
121
-
122
- test("429 status is retryable", () => {
123
- const retryable = isRetryable(null, { status: 429 }, DEFAULT_CONFIG);
124
- if (!retryable) throw new Error("429 should be retryable");
125
- });
126
-
127
- test("500 status is retryable", () => {
128
- const retryable = isRetryable(null, { status: 500 }, DEFAULT_CONFIG);
129
- if (!retryable) throw new Error("500 should be retryable");
130
- });
131
-
132
- test("200 status is not retryable", () => {
133
- const retryable = isRetryable(null, { status: 200 }, DEFAULT_CONFIG);
134
- if (retryable) throw new Error("200 should not be retryable");
135
- });
136
-
137
- test("ECONNRESET error is retryable", () => {
138
- const retryable = isRetryable({ code: "ECONNRESET" }, null, DEFAULT_CONFIG);
139
- if (!retryable) throw new Error("ECONNRESET should be retryable");
140
- });
141
-
142
- await asyncTest("withRetry succeeds on first attempt", async () => {
143
- let attempts = 0;
144
- const result = await withRetry(async () => {
145
- attempts++;
146
- return { status: 200, ok: true };
147
- });
148
- if (!result.ok) throw new Error("Should succeed");
149
- if (attempts !== 1) throw new Error(`Expected 1 attempt, got ${attempts}`);
150
- });
151
-
152
- await asyncTest("withRetry retries on failure then succeeds", async () => {
153
- let attempts = 0;
154
- const result = await withRetry(async () => {
155
- attempts++;
156
- if (attempts === 1) {
157
- return { status: 500, ok: false };
158
- }
159
- return { status: 200, ok: true };
160
- }, { maxRetries: 2, initialDelay: 10 });
161
-
162
- if (!result.ok) throw new Error("Should eventually succeed");
163
- if (attempts !== 2) throw new Error(`Expected 2 attempts, got ${attempts}`);
164
- });
165
-
166
- // Feature 3: Budget and Rate Limiting
167
- section("Feature 3: Budget and Rate Limiting");
168
-
169
- const { getBudgetManager, BudgetManager } = require("./src/budget");
170
-
171
- test("BudgetManager can be instantiated", () => {
172
- const manager = new BudgetManager({ enabled: true });
173
- if (!manager.db) throw new Error("Database not initialized");
174
- });
175
-
176
- test("getBudgetManager returns singleton", () => {
177
- const manager1 = getBudgetManager();
178
- const manager2 = getBudgetManager();
179
- if (manager1 !== manager2) throw new Error("Should return same instance");
180
- });
181
-
182
- test("Rate limit allows first request", () => {
183
- const manager = new BudgetManager({ enabled: true });
184
- const result = manager.checkRateLimit("test-user-first");
185
- if (!result.allowed) throw new Error("First request should be allowed");
186
- });
187
-
188
- test("Rate limit has per-minute window", () => {
189
- const manager = new BudgetManager({ enabled: true });
190
- const userId = "test-user-window";
191
- const result = manager.checkRateLimit(userId);
192
- if (!result.allowed) throw new Error("Should be allowed");
193
- });
194
-
195
- test("Budget check allows when no budget configured", () => {
196
- const manager = new BudgetManager({ enabled: true });
197
- const result = manager.checkBudget("test-user-no-budget");
198
- if (!result.allowed) throw new Error("Should allow when no budget configured");
199
- });
200
-
201
- test("Can set user budget", () => {
202
- const manager = new BudgetManager({ enabled: true });
203
- manager.setBudget("test-user-budget-set", {
204
- monthlyTokenLimit: 100000,
205
- monthlyRequestLimit: 1000,
206
- monthlyCostLimit: 10.0,
207
- });
208
-
209
- const result = manager.checkBudget("test-user-budget-set");
210
- if (!result.allowed) throw new Error("Should allow within budget");
211
- });
212
-
213
- test("Can record usage", () => {
214
- const manager = new BudgetManager({ enabled: true });
215
- manager.recordUsage("test-user-usage", "session-123", {
216
- tokensInput: 100,
217
- tokensOutput: 50,
218
- costUsd: 0.01,
219
- model: "test-model",
220
- });
221
- // Should not throw
222
- });
223
-
224
- test("Can get usage summary", () => {
225
- const manager = new BudgetManager({ enabled: true });
226
- const summary = manager.getUsageSummary("test-user-summary", 30);
227
- if (!summary) throw new Error("Should return summary");
228
- if (!summary.usage) throw new Error("Summary should have usage");
229
- });
230
-
231
- test("Budget warnings work", () => {
232
- const manager = new BudgetManager({ enabled: true });
233
- const userId = "test-user-warnings-check";
234
-
235
- manager.setBudget(userId, {
236
- monthlyTokenLimit: 1000,
237
- monthlyRequestLimit: 100,
238
- monthlyCostLimit: 1.0,
239
- alertThreshold: 0.8,
240
- });
241
-
242
- // Record usage at 85% (850 tokens)
243
- manager.recordUsage(userId, "session-1", {
244
- tokensInput: 850,
245
- tokensOutput: 0,
246
- costUsd: 0.85,
247
- });
248
-
249
- const result = manager.checkBudget(userId);
250
- // Warnings may or may not trigger depending on database state
251
- // Just check the structure exists
252
- if (result.warnings === undefined) throw new Error("Should have warnings field");
253
- });
254
-
255
- // Feature 4: Path Allowlisting
256
- section("Feature 4: Path Allowlisting");
257
-
258
- const config = require("./src/config");
259
-
260
- test("Config has fileAccess settings", () => {
261
- if (!config.policy) throw new Error("Missing policy config");
262
- if (!config.policy.fileAccess) throw new Error("Missing fileAccess config");
263
- });
264
-
265
- test("Default blocked paths include sensitive files", () => {
266
- const blockedPaths = config.policy.fileAccess.blockedPaths || [];
267
- const hasEnv = blockedPaths.some(p => p.includes(".env"));
268
- const hasPasswd = blockedPaths.some(p => p.includes("passwd"));
269
-
270
- if (!hasEnv) throw new Error("Should block .env files");
271
- if (!hasPasswd) throw new Error("Should block /etc/passwd");
272
- });
273
-
274
- test("Policy module can be loaded", () => {
275
- const policy = require("./src/policy");
276
- if (!policy.evaluateToolCall) throw new Error("Missing evaluateToolCall");
277
- });
278
-
279
- test("File read tool is registered", () => {
280
- const workspace = require("./src/tools/workspace");
281
- if (!workspace.registerWorkspaceTools) {
282
- throw new Error("registerWorkspaceTools not found");
283
- }
284
- });
285
-
286
- // Feature 5: Container Sandboxing
287
- section("Feature 5: Container Sandboxing");
288
-
289
- test("Sandbox config exists", () => {
290
- if (!config.mcp) throw new Error("Missing mcp config");
291
- if (!config.mcp.sandbox) throw new Error("Missing sandbox config");
292
- });
293
-
294
- test("Security options are configured", () => {
295
- const sandbox = config.mcp.sandbox;
296
- if (sandbox.readOnlyRoot === undefined) throw new Error("Missing readOnlyRoot");
297
- if (sandbox.noNewPrivileges === undefined) throw new Error("Missing noNewPrivileges");
298
- });
299
-
300
- test("Resource limits are configured", () => {
301
- const sandbox = config.mcp.sandbox;
302
- if (!sandbox.memoryLimit) throw new Error("Missing memoryLimit");
303
- if (!sandbox.cpuLimit) throw new Error("Missing cpuLimit");
304
- if (!sandbox.pidsLimit) throw new Error("Missing pidsLimit");
305
- });
306
-
307
- test("Capability management is configured", () => {
308
- const sandbox = config.mcp.sandbox;
309
- if (!sandbox.dropCapabilities) throw new Error("Missing dropCapabilities");
310
- if (!sandbox.addCapabilities) throw new Error("Missing addCapabilities");
311
- });
312
-
313
- test("Default drops ALL capabilities", () => {
314
- const sandbox = config.mcp.sandbox;
315
- const dropsAll = sandbox.dropCapabilities.includes("ALL");
316
- if (!dropsAll) throw new Error("Should drop ALL capabilities by default");
317
- });
318
-
319
- test("Sandbox module can be loaded", () => {
320
- const sandbox = require("./src/mcp/sandbox");
321
- if (!sandbox.isSandboxEnabled) throw new Error("Missing isSandboxEnabled");
322
- if (!sandbox.runSandboxProcess) throw new Error("Missing runSandboxProcess");
323
- });
324
-
325
- test("Process module has sandbox integration", () => {
326
- const process = require("./src/tools/process");
327
- if (!process.runProcess) throw new Error("Missing runProcess");
328
- });
329
-
330
- // Feature 6: Safe Command DSL
331
- section("Feature 6: Safe Command DSL");
332
-
333
- const { SafeCommandDSL, getSafeCommandDSL, DEFAULT_SAFE_COMMANDS } = require("./src/policy/safe-commands");
334
-
335
- test("SafeCommandDSL can be instantiated", () => {
336
- const dsl = new SafeCommandDSL();
337
- if (!dsl.evaluate) throw new Error("Missing evaluate method");
338
- });
339
-
340
- test("Default safe commands are defined", () => {
341
- if (!DEFAULT_SAFE_COMMANDS.ls) throw new Error("Missing 'ls' command");
342
- if (!DEFAULT_SAFE_COMMANDS.git) throw new Error("Missing 'git' command");
343
- if (!DEFAULT_SAFE_COMMANDS.rm) throw new Error("Missing 'rm' command");
344
- });
345
-
346
- test("'ls' command is allowed", () => {
347
- const dsl = new SafeCommandDSL();
348
- const result = dsl.evaluate("ls -la");
349
- if (!result.allowed) throw new Error("'ls -la' should be allowed");
350
- });
351
-
352
- test("'rm -rf /' is blocked", () => {
353
- const dsl = new SafeCommandDSL();
354
- const result = dsl.evaluate("rm -rf /");
355
- if (result.allowed) throw new Error("'rm -rf /' should be blocked");
356
- });
357
-
358
- test("'git status' is allowed", () => {
359
- const dsl = new SafeCommandDSL();
360
- const result = dsl.evaluate("git status");
361
- if (!result.allowed) throw new Error("'git status' should be allowed");
362
- });
363
-
364
- test("'git push --force' is blocked", () => {
365
- const dsl = new SafeCommandDSL();
366
- const result = dsl.evaluate("git push --force");
367
- if (result.allowed) throw new Error("'git push --force' should be blocked");
368
- });
369
-
370
- test("Commands with disallowed flags are blocked", () => {
371
- const dsl = new SafeCommandDSL();
372
- const result = dsl.evaluate("find . -delete");
373
- if (result.allowed) throw new Error("'find -delete' should be blocked");
374
- });
375
-
376
- test("Pattern matching works for blocked patterns", () => {
377
- const dsl = new SafeCommandDSL();
378
- const result = dsl.evaluate("cat /etc/passwd");
379
- if (result.allowed) throw new Error("'cat /etc/passwd' should be blocked");
380
- });
381
-
382
- test("Can get list of allowed commands", () => {
383
- const dsl = new SafeCommandDSL();
384
- const allowed = dsl.getAllowedCommands();
385
- if (!Array.isArray(allowed)) throw new Error("Should return array");
386
- if (allowed.length === 0) throw new Error("Should have allowed commands");
387
- });
388
-
389
- test("Can get list of blocked commands", () => {
390
- const dsl = new SafeCommandDSL();
391
- const blocked = dsl.getBlockedCommands();
392
- if (!Array.isArray(blocked)) throw new Error("Should return array");
393
- if (blocked.length === 0) throw new Error("Should have blocked commands");
394
- });
395
-
396
- test("Can add custom rules", () => {
397
- const dsl = new SafeCommandDSL();
398
- dsl.addRule("mycmd", { allowed: true, description: "My custom command" });
399
- const rule = dsl.getRule("mycmd");
400
- if (!rule) throw new Error("Rule not added");
401
- if (rule.description !== "My custom command") throw new Error("Wrong description");
402
- });
403
-
404
- test("getSafeCommandDSL returns singleton", () => {
405
- const dsl1 = getSafeCommandDSL();
406
- const dsl2 = getSafeCommandDSL();
407
- if (dsl1 !== dsl2) throw new Error("Should return same instance");
408
- });
409
-
410
- test("Policy module uses Safe Command DSL", () => {
411
- const policyCode = fs.readFileSync("./src/policy/index.js", "utf8");
412
- const usesDSL = policyCode.includes("getSafeCommandDSL");
413
- if (!usesDSL) throw new Error("Policy should use Safe Command DSL");
414
- });
415
- }
416
-
417
- // =============================================================================
418
- // OPTION 2 & 3: IMPORTANT & NICE-TO-HAVE FEATURES
419
- // =============================================================================
420
-
421
- async function testOption2And3Features() {
422
- console.log(colorize("\n╔═══════════════════════════════════════════════════╗", "cyan"));
423
- console.log(colorize("║ OPTION 2 & 3: IMPORTANT FEATURES ║", "cyan"));
424
- console.log(colorize("╚═══════════════════════════════════════════════════╝", "cyan"));
425
-
426
- // Feature 7: Observability/Metrics Export
427
- section("Feature 7: Observability/Metrics Export");
428
-
429
- const { MetricsCollector } = require("./src/observability/metrics");
430
-
431
- test("MetricsCollector initializes correctly", () => {
432
- const metrics = new MetricsCollector();
433
- if (metrics.requestCount === undefined) throw new Error("Missing requestCount");
434
- if (metrics.startTime === undefined) throw new Error("Missing startTime");
435
- });
436
-
437
- test("Can record HTTP requests", () => {
438
- const metrics = new MetricsCollector();
439
- metrics.recordRequest("GET", "/test", 200, 100);
440
- if (metrics.requestCount !== 1) throw new Error("Request not recorded");
441
- });
442
-
443
- test("Can record token usage", () => {
444
- const metrics = new MetricsCollector();
445
- metrics.recordTokens(100, 50);
446
- if (metrics.tokensInput !== 100) throw new Error("Input tokens not recorded");
447
- if (metrics.tokensOutput !== 50) throw new Error("Output tokens not recorded");
448
- if (metrics.tokensTotal !== 150) throw new Error("Total tokens wrong");
449
- });
450
-
451
- test("Can record API costs", () => {
452
- const metrics = new MetricsCollector();
453
- metrics.recordCost(1.50);
454
- if (metrics.totalCost !== 1.50) throw new Error("Cost not recorded");
455
- });
456
-
457
- test("Calculates latency statistics", () => {
458
- const metrics = new MetricsCollector();
459
- metrics.recordRequest("GET", "/test", 200, 100);
460
- metrics.recordRequest("GET", "/test", 200, 200);
461
- metrics.recordRequest("GET", "/test", 200, 150);
462
-
463
- const stats = metrics.calculateLatencyStats();
464
- if (stats.min !== 100) throw new Error("Min latency wrong");
465
- if (stats.max !== 200) throw new Error("Max latency wrong");
466
- if (stats.mean !== 150) throw new Error("Mean latency wrong");
467
- });
468
-
469
- test("Can get metrics snapshot", () => {
470
- const metrics = new MetricsCollector();
471
- metrics.recordRequest("GET", "/test", 200, 100);
472
- const snapshot = metrics.getMetrics();
473
-
474
- if (!snapshot.requests_total) throw new Error("Missing requests_total");
475
- if (!snapshot.latency_ms) throw new Error("Missing latency_ms");
476
- });
477
-
478
- test("Can export Prometheus format", () => {
479
- const metrics = new MetricsCollector();
480
- metrics.recordRequest("GET", "/test", 200, 100);
481
- const prom = metrics.toPrometheus();
482
-
483
- if (!prom.includes("http_requests_total")) throw new Error("Missing counter");
484
- if (!prom.includes("# HELP")) throw new Error("Missing help text");
485
- if (!prom.includes("# TYPE")) throw new Error("Missing type text");
486
- });
487
-
488
- test("Tracks requests by status code", () => {
489
- const metrics = new MetricsCollector();
490
- metrics.recordRequest("GET", "/test", 200, 100);
491
- metrics.recordRequest("GET", "/test", 404, 50);
492
- metrics.recordRequest("GET", "/test", 200, 75);
493
-
494
- const snapshot = metrics.getMetrics();
495
- if (snapshot.status_codes[200] !== 2) throw new Error("Wrong 200 count");
496
- if (snapshot.status_codes[404] !== 1) throw new Error("Wrong 404 count");
497
- });
498
-
499
- test("Tracks requests by endpoint", () => {
500
- const metrics = new MetricsCollector();
501
- metrics.recordRequest("GET", "/v1/messages", 200, 100);
502
- metrics.recordRequest("POST", "/v1/messages", 200, 200);
503
- metrics.recordRequest("GET", "/v1/messages", 200, 150);
504
-
505
- const snapshot = metrics.getMetrics();
506
- if (snapshot.endpoints["GET /v1/messages"] !== 2) throw new Error("Wrong GET count");
507
- if (snapshot.endpoints["POST /v1/messages"] !== 1) throw new Error("Wrong POST count");
508
- });
509
-
510
- // Feature 8: Health Check Endpoints
511
- section("Feature 8: Health Check Endpoints");
512
-
513
- const { livenessCheck, readinessCheck } = require("./src/api/health");
514
-
515
- await asyncTest("Liveness check returns 200", async () => {
516
- const req = {};
517
- const res = {
518
- status: (code) => {
519
- if (code !== 200) throw new Error(`Expected 200, got ${code}`);
520
- return res;
521
- },
522
- json: (body) => {
523
- if (body.status !== "alive") throw new Error("Expected status: alive");
524
- },
525
- };
526
-
527
- livenessCheck(req, res);
528
- });
529
-
530
- await asyncTest("Readiness check has correct structure", async () => {
531
- const req = { query: {} };
532
- const res = {
533
- statusCode: null,
534
- status: function (code) {
535
- this.statusCode = code;
536
- return this;
537
- },
538
- json: (body) => {
539
- if (!body.status) throw new Error("Missing status");
540
- if (!body.timestamp) throw new Error("Missing timestamp");
541
- if (!body.checks) throw new Error("Missing checks");
542
- },
543
- };
544
-
545
- await readinessCheck(req, res);
546
- });
547
-
548
- test("Memory check detects usage", () => {
549
- const memUsage = process.memoryUsage();
550
- if (!memUsage.heapUsed) throw new Error("No heap usage data");
551
- if (!memUsage.heapTotal) throw new Error("No heap total data");
552
- });
553
-
554
- // Feature 9: Graceful Shutdown
555
- section("Feature 9: Graceful Shutdown");
556
-
557
- const { ShutdownManager } = require("./src/server/shutdown");
558
-
559
- test("ShutdownManager can be instantiated", () => {
560
- const manager = new ShutdownManager();
561
- if (!manager.timeout) throw new Error("Missing timeout");
562
- });
563
-
564
- test("Tracks connections", () => {
565
- const manager = new ShutdownManager();
566
- const mockConn = {
567
- on: () => {},
568
- };
569
-
570
- const mockServer = {
571
- on: (event, handler) => {
572
- if (event === "connection") {
573
- handler(mockConn);
574
- }
575
- },
576
- };
577
-
578
- manager.registerServer(mockServer);
579
- if (manager.connections.size !== 1) throw new Error("Connection not tracked");
580
- });
581
-
582
- test("Shutdown not started by default", () => {
583
- const manager = new ShutdownManager();
584
- if (manager.isShuttingDown) throw new Error("Should not be shutting down");
585
- });
586
-
587
- // Feature 10: Structured Request Logging
588
- section("Feature 10: Structured Request Logging");
589
-
590
- test("Generates unique request IDs", () => {
591
- const crypto = require("crypto");
592
- const id1 = crypto.randomBytes(16).toString("hex");
593
- const id2 = crypto.randomBytes(16).toString("hex");
594
- if (id1 === id2) throw new Error("IDs should be unique");
595
- if (id1.length !== 32) throw new Error("ID should be 32 chars");
596
- });
597
-
598
- test("Request ID is hex string", () => {
599
- const crypto = require("crypto");
600
- const id = crypto.randomBytes(16).toString("hex");
601
- if (!/^[0-9a-f]+$/.test(id)) throw new Error("ID should be hex");
602
- });
603
-
604
- // Feature 11: Consistent Error Handling
605
- section("Feature 11: Consistent Error Handling");
606
-
607
- const {
608
- AppError,
609
- BadRequestError,
610
- NotFoundError,
611
- } = require("./src/api/middleware/error-handling");
612
-
613
- test("AppError has required fields", () => {
614
- const err = new AppError("Test error", 400, "test_error");
615
- if (err.statusCode !== 400) throw new Error("Wrong status code");
616
- if (err.code !== "test_error") throw new Error("Wrong error code");
617
- if (!err.isOperational) throw new Error("Should be operational");
618
- });
619
-
620
- test("BadRequestError sets correct defaults", () => {
621
- const err = new BadRequestError("Invalid input");
622
- if (err.statusCode !== 400) throw new Error("Wrong status code");
623
- if (err.code !== "bad_request") throw new Error("Wrong error code");
624
- });
625
-
626
- test("NotFoundError sets correct defaults", () => {
627
- const err = new NotFoundError("Resource not found");
628
- if (err.statusCode !== 404) throw new Error("Wrong status code");
629
- if (err.code !== "not_found") throw new Error("Wrong error code");
630
- });
631
-
632
- test("Errors can include details", () => {
633
- const details = { field: "email", message: "Invalid format" };
634
- const err = new BadRequestError("Validation failed", details);
635
- if (!err.details) throw new Error("Missing details");
636
- if (err.details.field !== "email") throw new Error("Wrong details");
637
- });
638
-
639
- // Feature 12: Input Validation
640
- section("Feature 12: Input Validation");
641
-
642
- const { validateObject } = require("./src/api/middleware/validation");
643
-
644
- test("Validates required fields", () => {
645
- const schema = {
646
- required: ["name", "email"],
647
- properties: {
648
- name: { type: "string" },
649
- email: { type: "string" },
650
- },
651
- };
652
-
653
- const errors = validateObject({}, schema);
654
- if (errors.length < 2) throw new Error(`Expected at least 2 errors, got ${errors.length}`);
655
- });
656
-
657
- test("Validates field types", () => {
658
- const schema = {
659
- properties: {
660
- age: { type: "number" },
661
- },
662
- };
663
-
664
- const errors = validateObject({ age: "twenty" }, schema);
665
- if (errors.length === 0) throw new Error("Should have type error");
666
- });
667
-
668
- test("Validates string length", () => {
669
- const schema = {
670
- properties: {
671
- name: { type: "string", minLength: 3, maxLength: 10 },
672
- },
673
- };
674
-
675
- const errors1 = validateObject({ name: "AB" }, schema);
676
- if (errors1.length === 0) throw new Error("Should fail minLength");
677
-
678
- const errors2 = validateObject({ name: "ABCDEFGHIJK" }, schema);
679
- if (errors2.length === 0) throw new Error("Should fail maxLength");
680
-
681
- const errors3 = validateObject({ name: "ABCDE" }, schema);
682
- if (errors3.length !== 0) throw new Error("Should pass validation");
683
- });
684
-
685
- test("Validates enum values", () => {
686
- const schema = {
687
- properties: {
688
- role: { type: "string", enum: ["user", "admin", "guest"] },
689
- },
690
- };
691
-
692
- const errors = validateObject({ role: "superuser" }, schema);
693
- if (errors.length === 0) throw new Error("Should fail enum validation");
694
- });
695
-
696
- test("Validates number ranges", () => {
697
- const schema = {
698
- properties: {
699
- age: { type: "number", minimum: 0, maximum: 120 },
700
- },
701
- };
702
-
703
- const errors1 = validateObject({ age: -1 }, schema);
704
- if (errors1.length === 0) throw new Error("Should fail minimum");
705
-
706
- const errors2 = validateObject({ age: 150 }, schema);
707
- if (errors2.length === 0) throw new Error("Should fail maximum");
708
- });
709
-
710
- // Feature 14: Load Shedding
711
- section("Feature 14: Load Shedding");
712
-
713
- const { LoadShedder } = require("./src/api/middleware/load-shedding");
714
-
715
- test("LoadShedder can be instantiated", () => {
716
- const shedder = new LoadShedder();
717
- if (!shedder.heapThreshold) throw new Error("Missing heapThreshold");
718
- if (!shedder.activeRequestsThreshold) throw new Error("Missing activeRequestsThreshold");
719
- });
720
-
721
- test("System not overloaded by default", () => {
722
- const shedder = new LoadShedder();
723
- if (shedder.isOverloaded()) throw new Error("Should not be overloaded");
724
- });
725
-
726
- test("Tracks active requests", () => {
727
- const shedder = new LoadShedder();
728
- shedder.activeRequests = 500;
729
- if (shedder.activeRequests !== 500) throw new Error("Not tracking correctly");
730
- });
731
-
732
- test("Overload detection works", () => {
733
- const shedder = new LoadShedder({ activeRequestsThreshold: 100 });
734
- shedder.activeRequests = 150;
735
- shedder.lastCheck = 0; // Force recheck
736
- // Detection may vary based on actual system state
737
- const isOverloaded = shedder.isOverloaded();
738
- // Just check it returns a boolean
739
- if (typeof isOverloaded !== "boolean") throw new Error("Should return boolean");
740
- });
741
-
742
- test("Provides load shedding metrics", () => {
743
- const shedder = new LoadShedder();
744
- const metrics = shedder.getMetrics();
745
- if (metrics.activeRequests === undefined) throw new Error("Missing activeRequests");
746
- if (metrics.totalShed === undefined) throw new Error("Missing totalShed");
747
- });
748
-
749
- // Feature 15: Circuit Breakers
750
- section("Feature 15: Circuit Breakers");
751
-
752
- const { CircuitBreaker, STATE } = require("./src/clients/circuit-breaker");
753
-
754
- test("CircuitBreaker can be instantiated", () => {
755
- const breaker = new CircuitBreaker("test");
756
- if (breaker.state !== STATE.CLOSED) throw new Error("Should start CLOSED");
757
- });
758
-
759
- await asyncTest("Executes function successfully", async () => {
760
- const breaker = new CircuitBreaker("test");
761
- const result = await breaker.execute(async () => {
762
- return "success";
763
- });
764
- if (result !== "success") throw new Error("Wrong result");
765
- });
766
-
767
- await asyncTest("Tracks failures", async () => {
768
- const breaker = new CircuitBreaker("test", { failureThreshold: 3 });
769
-
770
- try {
771
- await breaker.execute(async () => {
772
- throw new Error("Test failure");
773
- });
774
- } catch (err) {
775
- // Expected
776
- }
777
-
778
- if (breaker.failureCount !== 1) throw new Error("Failure not tracked");
779
- });
780
-
781
- await asyncTest("Opens circuit after failure threshold", async () => {
782
- const breaker = new CircuitBreaker("test", { failureThreshold: 2 });
783
-
784
- // First failure
785
- try {
786
- await breaker.execute(async () => {
787
- throw new Error("Fail 1");
788
- });
789
- } catch (err) {}
790
-
791
- // Second failure
792
- try {
793
- await breaker.execute(async () => {
794
- throw new Error("Fail 2");
795
- });
796
- } catch (err) {}
797
-
798
- if (breaker.state !== STATE.OPEN) throw new Error("Should be OPEN");
799
- });
800
-
801
- await asyncTest("Fails fast when circuit is open", async () => {
802
- const breaker = new CircuitBreaker("test", { failureThreshold: 1, timeout: 60000 });
803
-
804
- // Cause failure to open circuit
805
- try {
806
- await breaker.execute(async () => {
807
- throw new Error("Fail");
808
- });
809
- } catch (err) {}
810
-
811
- // Should fail fast
812
- let failedFast = false;
813
- try {
814
- await breaker.execute(async () => {
815
- return "should not reach";
816
- });
817
- } catch (err) {
818
- if (err.code === "circuit_breaker_open") {
819
- failedFast = true;
820
- }
821
- }
822
-
823
- if (!failedFast) throw new Error("Should fail fast");
824
- });
825
-
826
- test("Can get circuit breaker state", () => {
827
- const breaker = new CircuitBreaker("test");
828
- const state = breaker.getState();
829
-
830
- if (!state.name) throw new Error("Missing name");
831
- if (!state.state) throw new Error("Missing state");
832
- if (!state.stats) throw new Error("Missing stats");
833
- });
834
-
835
- test("Can manually reset circuit breaker", () => {
836
- const breaker = new CircuitBreaker("test");
837
- breaker.failureCount = 5;
838
- breaker.state = STATE.OPEN;
839
-
840
- breaker.reset();
841
-
842
- if (breaker.state !== STATE.CLOSED) throw new Error("Should be CLOSED");
843
- if (breaker.failureCount !== 0) throw new Error("Failure count should be reset");
844
- });
845
- }
846
-
847
- // =============================================================================
848
- // Main Test Runner
849
- // =============================================================================
850
-
851
- async function runAllTests() {
852
- console.log(colorize("\n╔═══════════════════════════════════════════════════╗", "blue"));
853
- console.log(colorize("║ Comprehensive Production Hardening Tests ║", "blue"));
854
- console.log(colorize("║ 80 Tests: Options 1, 2 & 3 ║", "blue"));
855
- console.log(colorize("╚═══════════════════════════════════════════════════╝", "blue"));
856
-
857
- try {
858
- await testOption1Features();
859
- await testOption2And3Features();
860
-
861
- // Summary
862
- console.log(colorize("\n" + "=".repeat(60), "blue"));
863
- console.log(colorize("FINAL TEST SUMMARY", "blue"));
864
- console.log(colorize("=".repeat(60), "blue"));
865
- console.log(`Total tests: ${results.total}`);
866
- console.log(`Passed: ${colorize(results.passed.toString(), "green")}`);
867
- console.log(`Failed: ${colorize(results.failed.toString(), results.failed > 0 ? "red" : "green")}`);
868
- console.log(`Success rate: ${formatResult(results.passed, results.total)}`);
869
-
870
- // Section breakdown
871
- console.log(colorize("\n" + "=".repeat(60), "blue"));
872
- console.log(colorize("Breakdown by Feature", "blue"));
873
- console.log(colorize("=".repeat(60), "blue"));
874
-
875
- let currentSection = null;
876
- let sectionPassed = 0;
877
- let sectionTotal = 0;
878
-
879
- results.tests.forEach((test, index) => {
880
- const section = results.sections.find(
881
- (s, i) =>
882
- index >= s.startIndex &&
883
- (i === results.sections.length - 1 || index < results.sections[i + 1].startIndex)
884
- );
885
-
886
- if (section && section !== currentSection) {
887
- if (currentSection) {
888
- console.log(` ${formatResult(sectionPassed, sectionTotal)}`);
889
- }
890
- currentSection = section;
891
- sectionPassed = 0;
892
- sectionTotal = 0;
893
- console.log(`\n${section.name}`);
894
- }
895
-
896
- sectionTotal++;
897
- if (test.passed) sectionPassed++;
898
- });
899
-
900
- if (currentSection) {
901
- console.log(` ${formatResult(sectionPassed, sectionTotal)}`);
902
- }
903
-
904
- if (results.failed > 0) {
905
- console.log(colorize("\n" + "=".repeat(60), "red"));
906
- console.log(colorize("Failed Tests:", "red"));
907
- console.log(colorize("=".repeat(60), "red"));
908
- results.tests
909
- .filter((t) => !t.passed)
910
- .forEach((t) => {
911
- console.log(colorize(` ✗ ${t.name}`, "red"));
912
- console.log(colorize(` ${t.error}`, "red"));
913
- });
914
- }
915
-
916
- console.log(colorize("\n" + "=".repeat(60), "blue"));
917
-
918
- // Exit with appropriate code
919
- process.exit(results.failed > 0 ? 1 : 0);
920
- } catch (error) {
921
- console.error(colorize("\nFatal error running tests:", "red"));
922
- console.error(error);
923
- process.exit(1);
924
- }
925
- }
926
-
927
- // Run tests
928
- runAllTests();