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.
- package/README.md +3 -3
- package/config/model-tiers.json +89 -0
- package/install.sh +6 -1
- package/package.json +4 -2
- package/scripts/setup.js +0 -1
- package/src/agents/executor.js +14 -6
- package/src/api/middleware/session.js +15 -2
- package/src/api/openai-router.js +162 -37
- package/src/api/providers-handler.js +15 -1
- package/src/api/router.js +107 -2
- package/src/budget/index.js +4 -3
- package/src/clients/databricks.js +431 -234
- package/src/clients/gpt-utils.js +181 -0
- package/src/clients/ollama-utils.js +66 -140
- package/src/clients/routing.js +0 -1
- package/src/clients/standard-tools.js +99 -3
- package/src/config/index.js +133 -35
- package/src/context/toon.js +173 -0
- package/src/logger/index.js +23 -0
- package/src/orchestrator/index.js +688 -213
- package/src/routing/agentic-detector.js +320 -0
- package/src/routing/complexity-analyzer.js +202 -2
- package/src/routing/cost-optimizer.js +305 -0
- package/src/routing/index.js +168 -159
- package/src/routing/model-tiers.js +365 -0
- package/src/server.js +4 -14
- package/src/sessions/cleanup.js +3 -3
- package/src/sessions/record.js +10 -1
- package/src/sessions/store.js +7 -2
- package/src/tools/agent-task.js +48 -1
- package/src/tools/index.js +19 -2
- package/src/tools/lazy-loader.js +7 -0
- package/src/tools/tinyfish.js +358 -0
- package/src/tools/truncate.js +1 -0
- package/.github/FUNDING.yml +0 -15
- package/.github/workflows/README.md +0 -215
- package/.github/workflows/ci.yml +0 -69
- package/.github/workflows/index.yml +0 -62
- package/.github/workflows/web-tools-tests.yml +0 -56
- package/CITATIONS.bib +0 -6
- package/CLAWROUTER_ROUTING_PLAN.md +0 -910
- package/DEPLOYMENT.md +0 -1001
- package/LYNKR-TUI-PLAN.md +0 -984
- package/PERFORMANCE-REPORT.md +0 -866
- package/PLAN-per-client-model-routing.md +0 -252
- package/ROUTER_COMPARISON.md +0 -173
- package/TIER_ROUTING_PLAN.md +0 -771
- package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
- package/docs/BingSiteAuth.xml +0 -4
- package/docs/docs-style.css +0 -478
- package/docs/docs.html +0 -197
- package/docs/google5be250e608e6da39.html +0 -1
- package/docs/index.html +0 -577
- package/docs/index.md +0 -577
- package/docs/robots.txt +0 -4
- package/docs/sitemap.xml +0 -44
- package/docs/style.css +0 -1223
- package/documentation/README.md +0 -100
- package/documentation/api.md +0 -806
- package/documentation/claude-code-cli.md +0 -672
- package/documentation/codex-cli.md +0 -397
- package/documentation/contributing.md +0 -571
- package/documentation/cursor-integration.md +0 -731
- package/documentation/docker.md +0 -867
- package/documentation/embeddings.md +0 -760
- package/documentation/faq.md +0 -659
- package/documentation/features.md +0 -396
- package/documentation/headroom.md +0 -519
- package/documentation/installation.md +0 -706
- package/documentation/memory-system.md +0 -476
- package/documentation/production.md +0 -601
- package/documentation/providers.md +0 -906
- package/documentation/testing.md +0 -629
- package/documentation/token-optimization.md +0 -323
- package/documentation/tools.md +0 -697
- package/documentation/troubleshooting.md +0 -893
- package/final-test.js +0 -33
- package/headroom-sidecar/config.py +0 -93
- package/headroom-sidecar/requirements.txt +0 -14
- package/headroom-sidecar/server.py +0 -451
- package/monitor-agents.sh +0 -31
- package/scripts/audit-log-reader.js +0 -399
- package/scripts/compact-dictionary.js +0 -204
- package/scripts/test-deduplication.js +0 -448
- package/src/db/database.sqlite +0 -0
- package/test/README.md +0 -212
- package/test/azure-openai-config.test.js +0 -204
- package/test/azure-openai-error-resilience.test.js +0 -238
- package/test/azure-openai-format-conversion.test.js +0 -354
- package/test/azure-openai-integration.test.js +0 -281
- package/test/azure-openai-routing.test.js +0 -177
- package/test/azure-openai-streaming.test.js +0 -171
- package/test/bedrock-integration.test.js +0 -471
- package/test/comprehensive-test-suite.js +0 -928
- package/test/config-validation.test.js +0 -207
- package/test/cursor-integration.test.js +0 -484
- package/test/format-conversion.test.js +0 -578
- package/test/hybrid-routing-integration.test.js +0 -254
- package/test/hybrid-routing-performance.test.js +0 -418
- package/test/llamacpp-integration.test.js +0 -863
- package/test/lmstudio-integration.test.js +0 -335
- package/test/memory/extractor.test.js +0 -398
- package/test/memory/retriever.test.js +0 -613
- package/test/memory/retriever.test.js.bak +0 -585
- package/test/memory/search.test.js +0 -537
- package/test/memory/search.test.js.bak +0 -389
- package/test/memory/store.test.js +0 -344
- package/test/memory/store.test.js.bak +0 -312
- package/test/memory/surprise.test.js +0 -300
- package/test/memory-performance.test.js +0 -472
- package/test/openai-integration.test.js +0 -686
- package/test/openrouter-error-resilience.test.js +0 -418
- package/test/passthrough-mode.test.js +0 -385
- package/test/performance-benchmark.js +0 -351
- package/test/performance-tests.js +0 -528
- package/test/routing.test.js +0 -219
- package/test/web-tools.test.js +0 -329
- package/test-agents-simple.js +0 -43
- package/test-cli-connection.sh +0 -33
- package/test-learning-unit.js +0 -126
- package/test-learning.js +0 -112
- package/test-parallel-agents.sh +0 -124
- package/test-parallel-direct.js +0 -155
- 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();
|