observability-toolkit 1.8.2 → 1.8.5
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 +60 -0
- package/dist/backends/index.d.ts +43 -0
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/index.js +41 -0
- package/dist/backends/index.js.map +1 -1
- package/dist/backends/index.test.d.ts +5 -0
- package/dist/backends/index.test.d.ts.map +1 -0
- package/dist/backends/index.test.js +156 -0
- package/dist/backends/index.test.js.map +1 -0
- package/dist/backends/local-jsonl-boolean-search.test.js +15 -12
- package/dist/backends/local-jsonl-boolean-search.test.js.map +1 -1
- package/dist/backends/local-jsonl-cache.test.d.ts +2 -0
- package/dist/backends/local-jsonl-cache.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-cache.test.js +295 -0
- package/dist/backends/local-jsonl-cache.test.js.map +1 -0
- package/dist/backends/local-jsonl-circuit-breaker.test.d.ts +2 -0
- package/dist/backends/local-jsonl-circuit-breaker.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-circuit-breaker.test.js +180 -0
- package/dist/backends/local-jsonl-circuit-breaker.test.js.map +1 -0
- package/dist/backends/local-jsonl-export.test.d.ts +2 -0
- package/dist/backends/local-jsonl-export.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-export.test.js +704 -0
- package/dist/backends/local-jsonl-export.test.js.map +1 -0
- package/dist/backends/local-jsonl-index.test.d.ts +2 -0
- package/dist/backends/local-jsonl-index.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-index.test.js +554 -0
- package/dist/backends/local-jsonl-index.test.js.map +1 -0
- package/dist/backends/local-jsonl-logs.test.d.ts +2 -0
- package/dist/backends/local-jsonl-logs.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-logs.test.js +612 -0
- package/dist/backends/local-jsonl-logs.test.js.map +1 -0
- package/dist/backends/local-jsonl-metrics.test.d.ts +2 -0
- package/dist/backends/local-jsonl-metrics.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-metrics.test.js +876 -0
- package/dist/backends/local-jsonl-metrics.test.js.map +1 -0
- package/dist/backends/local-jsonl-traces.test.d.ts +2 -0
- package/dist/backends/local-jsonl-traces.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-traces.test.js +1729 -0
- package/dist/backends/local-jsonl-traces.test.js.map +1 -0
- package/dist/backends/local-jsonl.d.ts +9 -0
- package/dist/backends/local-jsonl.d.ts.map +1 -1
- package/dist/backends/local-jsonl.js +348 -227
- package/dist/backends/local-jsonl.js.map +1 -1
- package/dist/backends/local-jsonl.test.js +290 -21
- package/dist/backends/local-jsonl.test.js.map +1 -1
- package/dist/backends/signoz-api-circuit-breaker.test.d.ts +6 -0
- package/dist/backends/signoz-api-circuit-breaker.test.d.ts.map +1 -0
- package/dist/backends/signoz-api-circuit-breaker.test.js +548 -0
- package/dist/backends/signoz-api-circuit-breaker.test.js.map +1 -0
- package/dist/backends/signoz-api-rate-limiter.test.d.ts +6 -0
- package/dist/backends/signoz-api-rate-limiter.test.d.ts.map +1 -0
- package/dist/backends/signoz-api-rate-limiter.test.js +389 -0
- package/dist/backends/signoz-api-rate-limiter.test.js.map +1 -0
- package/dist/backends/signoz-api-ssrf.test.d.ts +6 -0
- package/dist/backends/signoz-api-ssrf.test.d.ts.map +1 -0
- package/dist/backends/signoz-api-ssrf.test.js +216 -0
- package/dist/backends/signoz-api-ssrf.test.js.map +1 -0
- package/dist/backends/signoz-api-test-helpers.d.ts +80 -0
- package/dist/backends/signoz-api-test-helpers.d.ts.map +1 -0
- package/dist/backends/signoz-api-test-helpers.js +79 -0
- package/dist/backends/signoz-api-test-helpers.js.map +1 -0
- package/dist/backends/signoz-api.d.ts +16 -0
- package/dist/backends/signoz-api.d.ts.map +1 -1
- package/dist/backends/signoz-api.js +71 -9
- package/dist/backends/signoz-api.js.map +1 -1
- package/dist/backends/signoz-api.test.d.ts +9 -0
- package/dist/backends/signoz-api.test.d.ts.map +1 -1
- package/dist/backends/signoz-api.test.js +14 -1027
- package/dist/backends/signoz-api.test.js.map +1 -1
- package/dist/lib/cache.d.ts +47 -1
- package/dist/lib/cache.d.ts.map +1 -1
- package/dist/lib/cache.js +40 -3
- package/dist/lib/cache.js.map +1 -1
- package/dist/lib/circuit-breaker.d.ts +83 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -0
- package/dist/lib/circuit-breaker.js +125 -0
- package/dist/lib/circuit-breaker.js.map +1 -0
- package/dist/lib/circuit-breaker.test.d.ts +2 -0
- package/dist/lib/circuit-breaker.test.d.ts.map +1 -0
- package/dist/lib/circuit-breaker.test.js +263 -0
- package/dist/lib/circuit-breaker.test.js.map +1 -0
- package/dist/lib/constants-symlink.test.d.ts +12 -0
- package/dist/lib/constants-symlink.test.d.ts.map +1 -0
- package/dist/lib/constants-symlink.test.js +357 -0
- package/dist/lib/constants-symlink.test.js.map +1 -0
- package/dist/lib/constants.d.ts +43 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +154 -24
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/constants.test.js +156 -7
- package/dist/lib/constants.test.js.map +1 -1
- package/dist/lib/edge-cases.test.d.ts +11 -0
- package/dist/lib/edge-cases.test.d.ts.map +1 -0
- package/dist/lib/edge-cases.test.js +634 -0
- package/dist/lib/edge-cases.test.js.map +1 -0
- package/dist/lib/error-sanitizer.d.ts.map +1 -1
- package/dist/lib/error-sanitizer.js +62 -26
- package/dist/lib/error-sanitizer.js.map +1 -1
- package/dist/lib/error-sanitizer.test.js +186 -0
- package/dist/lib/error-sanitizer.test.js.map +1 -1
- package/dist/lib/error-types.d.ts +54 -0
- package/dist/lib/error-types.d.ts.map +1 -0
- package/dist/lib/error-types.js +154 -0
- package/dist/lib/error-types.js.map +1 -0
- package/dist/lib/error-types.test.d.ts +2 -0
- package/dist/lib/error-types.test.d.ts.map +1 -0
- package/dist/lib/error-types.test.js +196 -0
- package/dist/lib/error-types.test.js.map +1 -0
- package/dist/lib/file-utils.test.js +3 -3
- package/dist/lib/file-utils.test.js.map +1 -1
- package/dist/lib/indexer.test.js +157 -24
- package/dist/lib/indexer.test.js.map +1 -1
- package/dist/lib/input-validator.d.ts +17 -0
- package/dist/lib/input-validator.d.ts.map +1 -1
- package/dist/lib/input-validator.fuzz.test.d.ts +12 -0
- package/dist/lib/input-validator.fuzz.test.d.ts.map +1 -0
- package/dist/lib/input-validator.fuzz.test.js +290 -0
- package/dist/lib/input-validator.fuzz.test.js.map +1 -0
- package/dist/lib/input-validator.js +62 -3
- package/dist/lib/input-validator.js.map +1 -1
- package/dist/lib/input-validator.test.js +129 -1
- package/dist/lib/input-validator.test.js.map +1 -1
- package/dist/lib/logger.d.ts +46 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +81 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/logger.test.d.ts +2 -0
- package/dist/lib/logger.test.d.ts.map +1 -0
- package/dist/lib/logger.test.js +122 -0
- package/dist/lib/logger.test.js.map +1 -0
- package/dist/lib/query-sanitizer.d.ts +51 -3
- package/dist/lib/query-sanitizer.d.ts.map +1 -1
- package/dist/lib/query-sanitizer.js +105 -31
- package/dist/lib/query-sanitizer.js.map +1 -1
- package/dist/lib/query-sanitizer.test.js +102 -1
- package/dist/lib/query-sanitizer.test.js.map +1 -1
- package/dist/lib/server-utils.d.ts +88 -0
- package/dist/lib/server-utils.d.ts.map +1 -0
- package/dist/lib/server-utils.js +173 -0
- package/dist/lib/server-utils.js.map +1 -0
- package/dist/lib/shared-schemas.d.ts +81 -0
- package/dist/lib/shared-schemas.d.ts.map +1 -0
- package/dist/lib/shared-schemas.js +80 -0
- package/dist/lib/shared-schemas.js.map +1 -0
- package/dist/lib/shared-schemas.test.d.ts +5 -0
- package/dist/lib/shared-schemas.test.d.ts.map +1 -0
- package/dist/lib/shared-schemas.test.js +106 -0
- package/dist/lib/shared-schemas.test.js.map +1 -0
- package/dist/lib/toon-encoder.d.ts +26 -0
- package/dist/lib/toon-encoder.d.ts.map +1 -0
- package/dist/lib/toon-encoder.js +61 -0
- package/dist/lib/toon-encoder.js.map +1 -0
- package/dist/lib/toon-encoder.test.d.ts +5 -0
- package/dist/lib/toon-encoder.test.d.ts.map +1 -0
- package/dist/lib/toon-encoder.test.js +85 -0
- package/dist/lib/toon-encoder.test.js.map +1 -0
- package/dist/server.d.ts +1 -49
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +154 -162
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +198 -7
- package/dist/server.test.js.map +1 -1
- package/dist/test-helpers/env-utils.d.ts +87 -0
- package/dist/test-helpers/env-utils.d.ts.map +1 -0
- package/dist/test-helpers/env-utils.js +132 -0
- package/dist/test-helpers/env-utils.js.map +1 -0
- package/dist/test-helpers/file-utils.d.ts +67 -0
- package/dist/test-helpers/file-utils.d.ts.map +1 -1
- package/dist/test-helpers/file-utils.js +165 -2
- package/dist/test-helpers/file-utils.js.map +1 -1
- package/dist/test-helpers/fuzz-generators.d.ts +58 -0
- package/dist/test-helpers/fuzz-generators.d.ts.map +1 -0
- package/dist/test-helpers/fuzz-generators.js +216 -0
- package/dist/test-helpers/fuzz-generators.js.map +1 -0
- package/dist/test-helpers/index.d.ts +11 -0
- package/dist/test-helpers/index.d.ts.map +1 -0
- package/dist/test-helpers/index.js +30 -0
- package/dist/test-helpers/index.js.map +1 -0
- package/dist/test-helpers/memfs-utils.d.ts +181 -0
- package/dist/test-helpers/memfs-utils.d.ts.map +1 -0
- package/dist/test-helpers/memfs-utils.js +292 -0
- package/dist/test-helpers/memfs-utils.js.map +1 -0
- package/dist/test-helpers/memfs-utils.test.d.ts +5 -0
- package/dist/test-helpers/memfs-utils.test.d.ts.map +1 -0
- package/dist/test-helpers/memfs-utils.test.js +338 -0
- package/dist/test-helpers/memfs-utils.test.js.map +1 -0
- package/dist/test-helpers/mock-backends.d.ts +113 -2
- package/dist/test-helpers/mock-backends.d.ts.map +1 -1
- package/dist/test-helpers/mock-backends.js +199 -3
- package/dist/test-helpers/mock-backends.js.map +1 -1
- package/dist/test-helpers/mock-backends.test.d.ts +5 -0
- package/dist/test-helpers/mock-backends.test.d.ts.map +1 -0
- package/dist/test-helpers/mock-backends.test.js +368 -0
- package/dist/test-helpers/mock-backends.test.js.map +1 -0
- package/dist/test-helpers/race-condition-helpers.d.ts +85 -0
- package/dist/test-helpers/race-condition-helpers.d.ts.map +1 -0
- package/dist/test-helpers/race-condition-helpers.js +279 -0
- package/dist/test-helpers/race-condition-helpers.js.map +1 -0
- package/dist/test-helpers/schema-validators.d.ts +32 -0
- package/dist/test-helpers/schema-validators.d.ts.map +1 -0
- package/dist/test-helpers/schema-validators.js +125 -0
- package/dist/test-helpers/schema-validators.js.map +1 -0
- package/dist/test-helpers/test-data-builders.d.ts +260 -0
- package/dist/test-helpers/test-data-builders.d.ts.map +1 -0
- package/dist/test-helpers/test-data-builders.js +337 -0
- package/dist/test-helpers/test-data-builders.js.map +1 -0
- package/dist/test-helpers/test-data-builders.test.d.ts +2 -0
- package/dist/test-helpers/test-data-builders.test.d.ts.map +1 -0
- package/dist/test-helpers/test-data-builders.test.js +306 -0
- package/dist/test-helpers/test-data-builders.test.js.map +1 -0
- package/dist/test-helpers/tool-validators.d.ts +28 -0
- package/dist/test-helpers/tool-validators.d.ts.map +1 -0
- package/dist/test-helpers/tool-validators.js +71 -0
- package/dist/test-helpers/tool-validators.js.map +1 -0
- package/dist/tools/context-stats.d.ts +1 -0
- package/dist/tools/context-stats.d.ts.map +1 -1
- package/dist/tools/context-stats.js +9 -5
- package/dist/tools/context-stats.js.map +1 -1
- package/dist/tools/context-stats.test.js +24 -10
- package/dist/tools/context-stats.test.js.map +1 -1
- package/dist/tools/get-trace-url.js +2 -2
- package/dist/tools/get-trace-url.js.map +1 -1
- package/dist/tools/health-check.js +2 -2
- package/dist/tools/health-check.js.map +1 -1
- package/dist/tools/query-evaluations.d.ts +21 -18
- package/dist/tools/query-evaluations.d.ts.map +1 -1
- package/dist/tools/query-evaluations.js +33 -19
- package/dist/tools/query-evaluations.js.map +1 -1
- package/dist/tools/query-evaluations.test.js +60 -63
- package/dist/tools/query-evaluations.test.js.map +1 -1
- package/dist/tools/query-llm-events.d.ts +19 -15
- package/dist/tools/query-llm-events.d.ts.map +1 -1
- package/dist/tools/query-llm-events.js +31 -15
- package/dist/tools/query-llm-events.js.map +1 -1
- package/dist/tools/query-llm-events.test.js +277 -12
- package/dist/tools/query-llm-events.test.js.map +1 -1
- package/dist/tools/query-logs.d.ts +22 -22
- package/dist/tools/query-logs.d.ts.map +1 -1
- package/dist/tools/query-logs.js +9 -9
- package/dist/tools/query-logs.js.map +1 -1
- package/dist/tools/query-logs.test.js +19 -72
- package/dist/tools/query-logs.test.js.map +1 -1
- package/dist/tools/query-metrics.d.ts +14 -14
- package/dist/tools/query-metrics.d.ts.map +1 -1
- package/dist/tools/query-metrics.js +9 -9
- package/dist/tools/query-metrics.js.map +1 -1
- package/dist/tools/query-metrics.test.js +12 -25
- package/dist/tools/query-metrics.test.js.map +1 -1
- package/dist/tools/query-traces.d.ts +28 -28
- package/dist/tools/query-traces.d.ts.map +1 -1
- package/dist/tools/query-traces.js +18 -18
- package/dist/tools/query-traces.js.map +1 -1
- package/dist/tools/query-traces.test.js +58 -54
- package/dist/tools/query-traces.test.js.map +1 -1
- package/dist/tools/setup-claudeignore.js +7 -7
- package/dist/tools/setup-claudeignore.js.map +1 -1
- package/dist/tools/setup-claudeignore.test.js +4 -25
- package/dist/tools/setup-claudeignore.test.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
2
|
+
import * as assert from 'node:assert';
|
|
3
|
+
import { CircuitBreaker } from './circuit-breaker.js';
|
|
4
|
+
describe('CircuitBreaker', () => {
|
|
5
|
+
describe('initial state', () => {
|
|
6
|
+
it('should start in closed state', () => {
|
|
7
|
+
const breaker = new CircuitBreaker();
|
|
8
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
9
|
+
assert.strictEqual(breaker.getFailureCount(), 0);
|
|
10
|
+
});
|
|
11
|
+
it('should allow requests when closed', () => {
|
|
12
|
+
const breaker = new CircuitBreaker();
|
|
13
|
+
assert.strictEqual(breaker.canRequest(), true);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe('failure handling', () => {
|
|
17
|
+
it('should track consecutive failures', () => {
|
|
18
|
+
const breaker = new CircuitBreaker({ maxFailures: 3 });
|
|
19
|
+
breaker.recordFailure();
|
|
20
|
+
assert.strictEqual(breaker.getFailureCount(), 1);
|
|
21
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
22
|
+
breaker.recordFailure();
|
|
23
|
+
assert.strictEqual(breaker.getFailureCount(), 2);
|
|
24
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
25
|
+
});
|
|
26
|
+
it('should open after maxFailures consecutive failures', () => {
|
|
27
|
+
const breaker = new CircuitBreaker({ maxFailures: 3 });
|
|
28
|
+
breaker.recordFailure();
|
|
29
|
+
breaker.recordFailure();
|
|
30
|
+
breaker.recordFailure();
|
|
31
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
32
|
+
assert.strictEqual(breaker.getFailureCount(), 3);
|
|
33
|
+
});
|
|
34
|
+
it('should reject requests when open', () => {
|
|
35
|
+
const breaker = new CircuitBreaker({ maxFailures: 3 });
|
|
36
|
+
breaker.recordFailure();
|
|
37
|
+
breaker.recordFailure();
|
|
38
|
+
breaker.recordFailure();
|
|
39
|
+
assert.strictEqual(breaker.canRequest(), false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('success handling', () => {
|
|
43
|
+
it('should reset failure count on success', () => {
|
|
44
|
+
const breaker = new CircuitBreaker({ maxFailures: 3 });
|
|
45
|
+
breaker.recordFailure();
|
|
46
|
+
breaker.recordFailure();
|
|
47
|
+
assert.strictEqual(breaker.getFailureCount(), 2);
|
|
48
|
+
breaker.recordSuccess();
|
|
49
|
+
assert.strictEqual(breaker.getFailureCount(), 0);
|
|
50
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
51
|
+
});
|
|
52
|
+
it('should stay closed after partial failures followed by success', () => {
|
|
53
|
+
const breaker = new CircuitBreaker({ maxFailures: 3 });
|
|
54
|
+
breaker.recordFailure();
|
|
55
|
+
breaker.recordFailure();
|
|
56
|
+
breaker.recordSuccess();
|
|
57
|
+
breaker.recordFailure();
|
|
58
|
+
breaker.recordFailure();
|
|
59
|
+
// Should still be closed (only 2 consecutive failures)
|
|
60
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('half-open state', () => {
|
|
64
|
+
let originalDateNow;
|
|
65
|
+
let currentTime;
|
|
66
|
+
before(() => {
|
|
67
|
+
originalDateNow = Date.now;
|
|
68
|
+
});
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
currentTime = 1000000;
|
|
71
|
+
Date.now = () => currentTime;
|
|
72
|
+
});
|
|
73
|
+
after(() => {
|
|
74
|
+
Date.now = originalDateNow;
|
|
75
|
+
});
|
|
76
|
+
it('should transition to half-open after reset timeout', () => {
|
|
77
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
78
|
+
// Open the circuit
|
|
79
|
+
breaker.recordFailure();
|
|
80
|
+
breaker.recordFailure();
|
|
81
|
+
breaker.recordFailure();
|
|
82
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
83
|
+
// Advance time past reset period
|
|
84
|
+
currentTime += 61000;
|
|
85
|
+
// Should transition to half-open and allow request
|
|
86
|
+
assert.strictEqual(breaker.canRequest(), true);
|
|
87
|
+
assert.strictEqual(breaker.getState(), 'half-open');
|
|
88
|
+
});
|
|
89
|
+
it('should close on success in half-open state', () => {
|
|
90
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
91
|
+
// Open the circuit
|
|
92
|
+
breaker.recordFailure();
|
|
93
|
+
breaker.recordFailure();
|
|
94
|
+
breaker.recordFailure();
|
|
95
|
+
// Advance time and allow half-open
|
|
96
|
+
currentTime += 61000;
|
|
97
|
+
breaker.canRequest();
|
|
98
|
+
assert.strictEqual(breaker.getState(), 'half-open');
|
|
99
|
+
// Success should close the circuit
|
|
100
|
+
breaker.recordSuccess();
|
|
101
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
102
|
+
assert.strictEqual(breaker.getFailureCount(), 0);
|
|
103
|
+
});
|
|
104
|
+
it('should reopen on failure in half-open state', () => {
|
|
105
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
106
|
+
// Open the circuit
|
|
107
|
+
breaker.recordFailure();
|
|
108
|
+
breaker.recordFailure();
|
|
109
|
+
breaker.recordFailure();
|
|
110
|
+
// Advance time and allow half-open
|
|
111
|
+
currentTime += 61000;
|
|
112
|
+
breaker.canRequest();
|
|
113
|
+
assert.strictEqual(breaker.getState(), 'half-open');
|
|
114
|
+
// Failure should reopen
|
|
115
|
+
breaker.recordFailure();
|
|
116
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
117
|
+
});
|
|
118
|
+
it('should not allow request before reset timeout', () => {
|
|
119
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
120
|
+
// Open the circuit
|
|
121
|
+
breaker.recordFailure();
|
|
122
|
+
breaker.recordFailure();
|
|
123
|
+
breaker.recordFailure();
|
|
124
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
125
|
+
// Advance time but not past reset period
|
|
126
|
+
currentTime += 30000;
|
|
127
|
+
assert.strictEqual(breaker.canRequest(), false);
|
|
128
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('reset', () => {
|
|
132
|
+
it('should reset to initial state', () => {
|
|
133
|
+
const breaker = new CircuitBreaker({ maxFailures: 3 });
|
|
134
|
+
// Open the circuit
|
|
135
|
+
breaker.recordFailure();
|
|
136
|
+
breaker.recordFailure();
|
|
137
|
+
breaker.recordFailure();
|
|
138
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
139
|
+
// Reset
|
|
140
|
+
breaker.reset();
|
|
141
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
142
|
+
assert.strictEqual(breaker.getFailureCount(), 0);
|
|
143
|
+
assert.strictEqual(breaker.canRequest(), true);
|
|
144
|
+
});
|
|
145
|
+
it('should reset halfOpenRequestInFlight flag', () => {
|
|
146
|
+
const originalDateNow = Date.now;
|
|
147
|
+
let currentTime = 1000000;
|
|
148
|
+
Date.now = () => currentTime;
|
|
149
|
+
try {
|
|
150
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
151
|
+
// Open and transition to half-open
|
|
152
|
+
breaker.recordFailure();
|
|
153
|
+
breaker.recordFailure();
|
|
154
|
+
breaker.recordFailure();
|
|
155
|
+
currentTime += 61000;
|
|
156
|
+
breaker.canRequest();
|
|
157
|
+
// Flag should be set
|
|
158
|
+
assert.strictEqual(breaker.isHalfOpenRequestInFlight(), true);
|
|
159
|
+
// Reset should clear it
|
|
160
|
+
breaker.reset();
|
|
161
|
+
assert.strictEqual(breaker.isHalfOpenRequestInFlight(), false);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
Date.now = originalDateNow;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('half-open race condition prevention (H2)', () => {
|
|
169
|
+
let originalDateNow;
|
|
170
|
+
let currentTime;
|
|
171
|
+
before(() => {
|
|
172
|
+
originalDateNow = Date.now;
|
|
173
|
+
});
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
currentTime = 1000000;
|
|
176
|
+
Date.now = () => currentTime;
|
|
177
|
+
});
|
|
178
|
+
after(() => {
|
|
179
|
+
Date.now = originalDateNow;
|
|
180
|
+
});
|
|
181
|
+
it('should only allow one request through during half-open state', () => {
|
|
182
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
183
|
+
// Open the circuit
|
|
184
|
+
breaker.recordFailure();
|
|
185
|
+
breaker.recordFailure();
|
|
186
|
+
breaker.recordFailure();
|
|
187
|
+
// Advance time past reset period
|
|
188
|
+
currentTime += 61000;
|
|
189
|
+
// First request should be allowed (transitions to half-open)
|
|
190
|
+
assert.strictEqual(breaker.canRequest(), true);
|
|
191
|
+
assert.strictEqual(breaker.getState(), 'half-open');
|
|
192
|
+
assert.strictEqual(breaker.isHalfOpenRequestInFlight(), true);
|
|
193
|
+
// Second concurrent request should be rejected
|
|
194
|
+
assert.strictEqual(breaker.canRequest(), false);
|
|
195
|
+
assert.strictEqual(breaker.getState(), 'half-open');
|
|
196
|
+
});
|
|
197
|
+
it('should clear halfOpenRequestInFlight on success', () => {
|
|
198
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
199
|
+
// Open and transition to half-open
|
|
200
|
+
breaker.recordFailure();
|
|
201
|
+
breaker.recordFailure();
|
|
202
|
+
breaker.recordFailure();
|
|
203
|
+
currentTime += 61000;
|
|
204
|
+
breaker.canRequest();
|
|
205
|
+
assert.strictEqual(breaker.isHalfOpenRequestInFlight(), true);
|
|
206
|
+
// Success should clear the flag
|
|
207
|
+
breaker.recordSuccess();
|
|
208
|
+
assert.strictEqual(breaker.isHalfOpenRequestInFlight(), false);
|
|
209
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
210
|
+
});
|
|
211
|
+
it('should clear halfOpenRequestInFlight on failure', () => {
|
|
212
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
213
|
+
// Open and transition to half-open
|
|
214
|
+
breaker.recordFailure();
|
|
215
|
+
breaker.recordFailure();
|
|
216
|
+
breaker.recordFailure();
|
|
217
|
+
currentTime += 61000;
|
|
218
|
+
breaker.canRequest();
|
|
219
|
+
assert.strictEqual(breaker.isHalfOpenRequestInFlight(), true);
|
|
220
|
+
// Failure should clear the flag
|
|
221
|
+
breaker.recordFailure();
|
|
222
|
+
assert.strictEqual(breaker.isHalfOpenRequestInFlight(), false);
|
|
223
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
224
|
+
});
|
|
225
|
+
it('should allow next request after half-open failure and timeout', () => {
|
|
226
|
+
const breaker = new CircuitBreaker({ maxFailures: 3, resetMs: 60000 });
|
|
227
|
+
// Open the circuit
|
|
228
|
+
breaker.recordFailure();
|
|
229
|
+
breaker.recordFailure();
|
|
230
|
+
breaker.recordFailure();
|
|
231
|
+
// First half-open attempt
|
|
232
|
+
currentTime += 61000;
|
|
233
|
+
assert.strictEqual(breaker.canRequest(), true);
|
|
234
|
+
breaker.recordFailure();
|
|
235
|
+
// Should be open again
|
|
236
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
237
|
+
// Wait for another reset period
|
|
238
|
+
currentTime += 61000;
|
|
239
|
+
// Should allow another half-open request
|
|
240
|
+
assert.strictEqual(breaker.canRequest(), true);
|
|
241
|
+
assert.strictEqual(breaker.getState(), 'half-open');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('configuration', () => {
|
|
245
|
+
it('should use custom maxFailures', () => {
|
|
246
|
+
const breaker = new CircuitBreaker({ maxFailures: 5 });
|
|
247
|
+
breaker.recordFailure();
|
|
248
|
+
breaker.recordFailure();
|
|
249
|
+
breaker.recordFailure();
|
|
250
|
+
assert.strictEqual(breaker.getState(), 'closed');
|
|
251
|
+
breaker.recordFailure();
|
|
252
|
+
breaker.recordFailure();
|
|
253
|
+
assert.strictEqual(breaker.getState(), 'open');
|
|
254
|
+
});
|
|
255
|
+
it('should use custom name in logging', () => {
|
|
256
|
+
const breaker = new CircuitBreaker({ name: 'test-breaker' });
|
|
257
|
+
// Name is used internally for logging, verify it doesn't throw
|
|
258
|
+
breaker.recordFailure();
|
|
259
|
+
breaker.recordSuccess();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
//# sourceMappingURL=circuit-breaker.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.test.js","sourceRoot":"","sources":["../../src/lib/circuit-breaker.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlink protection TOCTOU tests (A2 Category 3)
|
|
3
|
+
*
|
|
4
|
+
* Tests the atomic path checking and symlink protection in getTelemetryDirectories.
|
|
5
|
+
* Verifies protection against:
|
|
6
|
+
* - Symlink swap attacks (TOCTOU)
|
|
7
|
+
* - Broken symlinks
|
|
8
|
+
* - Circular symlink chains
|
|
9
|
+
* - Symlinks escaping allowed directories
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=constants-symlink.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants-symlink.test.d.ts","sourceRoot":"","sources":["../../src/lib/constants-symlink.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlink protection TOCTOU tests (A2 Category 3)
|
|
3
|
+
*
|
|
4
|
+
* Tests the atomic path checking and symlink protection in getTelemetryDirectories.
|
|
5
|
+
* Verifies protection against:
|
|
6
|
+
* - Symlink swap attacks (TOCTOU)
|
|
7
|
+
* - Broken symlinks
|
|
8
|
+
* - Circular symlink chains
|
|
9
|
+
* - Symlinks escaping allowed directories
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, before, after, afterEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert';
|
|
13
|
+
import { mkdirSync, rmdirSync, symlinkSync, unlinkSync, existsSync, realpathSync, writeFileSync } from 'fs';
|
|
14
|
+
import { join, resolve } from 'path';
|
|
15
|
+
import { tmpdir } from 'os';
|
|
16
|
+
import { getTelemetryDirectories } from './constants.js';
|
|
17
|
+
import { createSymlinkTestDir, createCircularSymlinks, createBrokenSymlink, createSymlinkChain, } from '../test-helpers/race-condition-helpers.js';
|
|
18
|
+
describe('symlink protection TOCTOU tests', () => {
|
|
19
|
+
let testBaseDir;
|
|
20
|
+
const cleanupFns = [];
|
|
21
|
+
before(() => {
|
|
22
|
+
testBaseDir = join(tmpdir(), `symlink-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
23
|
+
mkdirSync(testBaseDir, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
after(() => {
|
|
26
|
+
// Clean up any remaining resources
|
|
27
|
+
for (const cleanup of cleanupFns) {
|
|
28
|
+
try {
|
|
29
|
+
cleanup();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Ignore cleanup errors
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Remove test directory
|
|
36
|
+
try {
|
|
37
|
+
rmdirSync(testBaseDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Ignore
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
// Run and clear cleanup functions after each test
|
|
45
|
+
while (cleanupFns.length > 0) {
|
|
46
|
+
const cleanup = cleanupFns.pop();
|
|
47
|
+
try {
|
|
48
|
+
cleanup?.();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Ignore
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
describe('basic symlink handling', () => {
|
|
56
|
+
it('should follow valid symlinks to directories', () => {
|
|
57
|
+
const { symlinkPath, targetPath, cleanup } = createSymlinkTestDir(testBaseDir, 'valid');
|
|
58
|
+
cleanupFns.push(cleanup);
|
|
59
|
+
// Create a .claude/telemetry structure
|
|
60
|
+
const telemetryTarget = join(testBaseDir, 'telemetry-target');
|
|
61
|
+
mkdirSync(telemetryTarget, { recursive: true });
|
|
62
|
+
cleanupFns.push(() => rmdirSync(telemetryTarget));
|
|
63
|
+
// Verify the symlink exists and points to a directory
|
|
64
|
+
assert.ok(existsSync(symlinkPath), 'Symlink should exist');
|
|
65
|
+
const realPath = realpathSync(symlinkPath);
|
|
66
|
+
// On macOS, /tmp is a symlink to /private/tmp, so we compare resolved paths
|
|
67
|
+
const resolvedTarget = realpathSync(targetPath);
|
|
68
|
+
assert.strictEqual(realPath, resolvedTarget, 'Should resolve to target');
|
|
69
|
+
});
|
|
70
|
+
it('should handle non-symlink directories', () => {
|
|
71
|
+
const dirPath = join(testBaseDir, 'regular-dir');
|
|
72
|
+
mkdirSync(dirPath, { recursive: true });
|
|
73
|
+
cleanupFns.push(() => rmdirSync(dirPath));
|
|
74
|
+
assert.ok(existsSync(dirPath), 'Directory should exist');
|
|
75
|
+
const realPath = realpathSync(dirPath);
|
|
76
|
+
// Compare with resolved path (handles /tmp -> /private/tmp on macOS)
|
|
77
|
+
const resolvedDir = realpathSync(resolve(dirPath));
|
|
78
|
+
assert.strictEqual(realPath, resolvedDir, 'Should resolve to itself');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('broken symlink handling', () => {
|
|
82
|
+
it('should handle symlink with non-existent target', () => {
|
|
83
|
+
const { symlinkPath, cleanup } = createBrokenSymlink(testBaseDir, 'broken');
|
|
84
|
+
cleanupFns.push(cleanup);
|
|
85
|
+
// realpathSync should throw for broken symlinks
|
|
86
|
+
assert.throws(() => realpathSync(symlinkPath), /ENOENT/, 'Should throw ENOENT for broken symlink');
|
|
87
|
+
});
|
|
88
|
+
it('should gracefully handle broken symlink in telemetry path check', () => {
|
|
89
|
+
// Create a working directory with a broken .claude/telemetry symlink
|
|
90
|
+
const workDir = join(testBaseDir, 'work-broken');
|
|
91
|
+
const claudeDir = join(workDir, '.claude');
|
|
92
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
93
|
+
cleanupFns.push(() => rmdirSync(claudeDir, { recursive: true }));
|
|
94
|
+
const telemetryLink = join(claudeDir, 'telemetry');
|
|
95
|
+
const nonExistent = join(testBaseDir, 'does-not-exist-' + Date.now());
|
|
96
|
+
symlinkSync(nonExistent, telemetryLink);
|
|
97
|
+
cleanupFns.push(() => {
|
|
98
|
+
try {
|
|
99
|
+
unlinkSync(telemetryLink);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Ignore
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// getTelemetryDirectories should not include broken symlinks
|
|
106
|
+
const dirs = getTelemetryDirectories(workDir);
|
|
107
|
+
const hasLocal = dirs.some(d => d.source === 'local' && d.path.includes(workDir));
|
|
108
|
+
assert.ok(!hasLocal, 'Should not include broken symlinks');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('circular symlink detection', () => {
|
|
112
|
+
it('should handle circular symlink chains (ELOOP)', () => {
|
|
113
|
+
const { paths, cleanup } = createCircularSymlinks(testBaseDir, 'circ');
|
|
114
|
+
cleanupFns.push(cleanup);
|
|
115
|
+
// realpathSync should throw ELOOP for circular symlinks
|
|
116
|
+
for (const p of paths) {
|
|
117
|
+
assert.throws(() => realpathSync(p), /ELOOP/, `Should throw ELOOP for circular path: ${p}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('symlink chain resolution', () => {
|
|
122
|
+
it('should follow symlink chains to final target', () => {
|
|
123
|
+
const { startPath, endPath, cleanup } = createSymlinkChain(testBaseDir, 'chain', 3);
|
|
124
|
+
cleanupFns.push(cleanup);
|
|
125
|
+
const resolved = realpathSync(startPath);
|
|
126
|
+
// Compare with resolved endPath (handles /tmp -> /private/tmp on macOS)
|
|
127
|
+
const resolvedEnd = realpathSync(endPath);
|
|
128
|
+
assert.strictEqual(resolved, resolvedEnd, 'Should resolve through chain to final target');
|
|
129
|
+
});
|
|
130
|
+
it('should handle deep symlink chains', () => {
|
|
131
|
+
const { startPath, endPath, cleanup } = createSymlinkChain(testBaseDir, 'deep', 10);
|
|
132
|
+
cleanupFns.push(cleanup);
|
|
133
|
+
const resolved = realpathSync(startPath);
|
|
134
|
+
// Compare with resolved endPath
|
|
135
|
+
const resolvedEnd = realpathSync(endPath);
|
|
136
|
+
assert.strictEqual(resolved, resolvedEnd, 'Should resolve deep chains');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('symlink escape prevention', () => {
|
|
140
|
+
it('should reject symlinks pointing outside allowed directories', () => {
|
|
141
|
+
const workDir = join(testBaseDir, 'work-escape');
|
|
142
|
+
const claudeDir = join(workDir, '.claude');
|
|
143
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
144
|
+
cleanupFns.push(() => rmdirSync(claudeDir, { recursive: true }));
|
|
145
|
+
// Create telemetry symlink pointing to /tmp (outside workDir and ~/.claude)
|
|
146
|
+
const telemetryLink = join(claudeDir, 'telemetry');
|
|
147
|
+
const escapePath = '/tmp';
|
|
148
|
+
try {
|
|
149
|
+
symlinkSync(escapePath, telemetryLink);
|
|
150
|
+
cleanupFns.push(() => {
|
|
151
|
+
try {
|
|
152
|
+
unlinkSync(telemetryLink);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Ignore
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Skip test if we can't create symlink
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Capture console.warn to verify security warning
|
|
164
|
+
const originalWarn = console.warn;
|
|
165
|
+
let warnCalled = false;
|
|
166
|
+
let warnMessage = '';
|
|
167
|
+
console.warn = (msg) => {
|
|
168
|
+
warnCalled = true;
|
|
169
|
+
warnMessage = msg;
|
|
170
|
+
};
|
|
171
|
+
try {
|
|
172
|
+
const dirs = getTelemetryDirectories(workDir);
|
|
173
|
+
// Should not include the escape symlink
|
|
174
|
+
const hasEscape = dirs.some(d => d.path === '/tmp' || d.path.startsWith('/tmp/'));
|
|
175
|
+
assert.ok(!hasEscape, 'Should not include symlink escaping to /tmp');
|
|
176
|
+
// Should have logged security warning
|
|
177
|
+
assert.ok(warnCalled, 'Should log security warning');
|
|
178
|
+
assert.ok(warnMessage.includes('[SECURITY]'), 'Warning should be marked as security');
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
console.warn = originalWarn;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
it('should allow symlinks within the working directory', () => {
|
|
185
|
+
const workDir = join(testBaseDir, 'work-internal');
|
|
186
|
+
const claudeDir = join(workDir, '.claude');
|
|
187
|
+
const targetDir = join(workDir, 'actual-telemetry');
|
|
188
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
189
|
+
mkdirSync(targetDir, { recursive: true });
|
|
190
|
+
cleanupFns.push(() => {
|
|
191
|
+
rmdirSync(claudeDir, { recursive: true });
|
|
192
|
+
rmdirSync(targetDir);
|
|
193
|
+
});
|
|
194
|
+
const telemetryLink = join(claudeDir, 'telemetry');
|
|
195
|
+
symlinkSync(targetDir, telemetryLink);
|
|
196
|
+
cleanupFns.push(() => {
|
|
197
|
+
try {
|
|
198
|
+
unlinkSync(telemetryLink);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Ignore
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const dirs = getTelemetryDirectories(workDir);
|
|
205
|
+
const hasLocal = dirs.some(d => d.source === 'local');
|
|
206
|
+
assert.ok(hasLocal, 'Should include symlinks within working directory');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe('TOCTOU race condition resistance', () => {
|
|
210
|
+
it('should use atomic path checking to prevent symlink swap', () => {
|
|
211
|
+
// This test verifies the atomicPathCheck function behavior
|
|
212
|
+
const target1 = join(testBaseDir, 'swap-target-1');
|
|
213
|
+
const target2 = join(testBaseDir, 'swap-target-2');
|
|
214
|
+
const workDir = join(testBaseDir, 'work-swap');
|
|
215
|
+
const claudeDir = join(workDir, '.claude');
|
|
216
|
+
const telemetryLink = join(claudeDir, 'telemetry');
|
|
217
|
+
mkdirSync(target1, { recursive: true });
|
|
218
|
+
mkdirSync(target2, { recursive: true });
|
|
219
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
220
|
+
cleanupFns.push(() => {
|
|
221
|
+
rmdirSync(target1);
|
|
222
|
+
rmdirSync(target2);
|
|
223
|
+
rmdirSync(claudeDir, { recursive: true });
|
|
224
|
+
});
|
|
225
|
+
// Create initial symlink
|
|
226
|
+
symlinkSync(target1, telemetryLink);
|
|
227
|
+
cleanupFns.push(() => {
|
|
228
|
+
try {
|
|
229
|
+
unlinkSync(telemetryLink);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Ignore
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// First call should see target1
|
|
236
|
+
const dirs1 = getTelemetryDirectories(workDir);
|
|
237
|
+
const localDir1 = dirs1.find(d => d.source === 'local');
|
|
238
|
+
// Now swap the symlink
|
|
239
|
+
unlinkSync(telemetryLink);
|
|
240
|
+
symlinkSync(target2, telemetryLink);
|
|
241
|
+
// Second call should see target2
|
|
242
|
+
const dirs2 = getTelemetryDirectories(workDir);
|
|
243
|
+
const localDir2 = dirs2.find(d => d.source === 'local');
|
|
244
|
+
// Verify the resolved paths are different
|
|
245
|
+
if (localDir1 && localDir2) {
|
|
246
|
+
assert.notStrictEqual(localDir1.path, localDir2.path, 'Should detect changed symlink target');
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
it('should handle rapid symlink creation and deletion', async () => {
|
|
250
|
+
const workDir = join(testBaseDir, 'work-rapid');
|
|
251
|
+
const claudeDir = join(workDir, '.claude');
|
|
252
|
+
const targetDir = join(workDir, 'rapid-target');
|
|
253
|
+
const telemetryLink = join(claudeDir, 'telemetry');
|
|
254
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
255
|
+
mkdirSync(targetDir, { recursive: true });
|
|
256
|
+
cleanupFns.push(() => {
|
|
257
|
+
rmdirSync(claudeDir, { recursive: true });
|
|
258
|
+
rmdirSync(targetDir);
|
|
259
|
+
});
|
|
260
|
+
// Rapidly create and delete symlink while querying
|
|
261
|
+
let successCount = 0;
|
|
262
|
+
let emptyCount = 0;
|
|
263
|
+
for (let i = 0; i < 10; i++) {
|
|
264
|
+
// Create symlink
|
|
265
|
+
try {
|
|
266
|
+
symlinkSync(targetDir, telemetryLink);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// May already exist
|
|
270
|
+
}
|
|
271
|
+
const dirs = getTelemetryDirectories(workDir);
|
|
272
|
+
if (dirs.some(d => d.source === 'local')) {
|
|
273
|
+
successCount++;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
emptyCount++;
|
|
277
|
+
}
|
|
278
|
+
// Delete symlink
|
|
279
|
+
try {
|
|
280
|
+
unlinkSync(telemetryLink);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// May already be deleted
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Test should complete without crashes
|
|
287
|
+
assert.ok(successCount + emptyCount === 10, 'Should handle all iterations without crashing');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
describe('file descriptor verification', () => {
|
|
291
|
+
it('should reject file paths (non-directories)', () => {
|
|
292
|
+
const workDir = join(testBaseDir, 'work-file');
|
|
293
|
+
const claudeDir = join(workDir, '.claude');
|
|
294
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
295
|
+
cleanupFns.push(() => {
|
|
296
|
+
try {
|
|
297
|
+
rmdirSync(claudeDir, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Ignore
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
// Create a file instead of directory at telemetry path
|
|
304
|
+
const telemetryPath = join(claudeDir, 'telemetry');
|
|
305
|
+
writeFileSync(telemetryPath, 'not a directory');
|
|
306
|
+
cleanupFns.push(() => {
|
|
307
|
+
try {
|
|
308
|
+
unlinkSync(telemetryPath);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Ignore
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
const dirs = getTelemetryDirectories(workDir);
|
|
315
|
+
const hasLocal = dirs.some(d => d.source === 'local' && d.path.includes('telemetry'));
|
|
316
|
+
assert.ok(!hasLocal, 'Should not include files (only directories)');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe('symlink target validation', () => {
|
|
320
|
+
it('should validate symlink points to directory not file', () => {
|
|
321
|
+
const workDir = join(testBaseDir, 'work-file-target');
|
|
322
|
+
const claudeDir = join(workDir, '.claude');
|
|
323
|
+
const filePath = join(testBaseDir, 'target-file.txt');
|
|
324
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
325
|
+
writeFileSync(filePath, 'content');
|
|
326
|
+
cleanupFns.push(() => {
|
|
327
|
+
try {
|
|
328
|
+
rmdirSync(claudeDir, { recursive: true });
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Ignore
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
unlinkSync(filePath);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Ignore
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
// Create symlink to file
|
|
341
|
+
const telemetryLink = join(claudeDir, 'telemetry');
|
|
342
|
+
symlinkSync(filePath, telemetryLink);
|
|
343
|
+
cleanupFns.push(() => {
|
|
344
|
+
try {
|
|
345
|
+
unlinkSync(telemetryLink);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// Ignore
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
const dirs = getTelemetryDirectories(workDir);
|
|
352
|
+
const hasLocal = dirs.some(d => d.source === 'local');
|
|
353
|
+
assert.ok(!hasLocal, 'Should not include symlinks to files');
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
//# sourceMappingURL=constants-symlink.test.js.map
|