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.
Files changed (259) hide show
  1. package/README.md +60 -0
  2. package/dist/backends/index.d.ts +43 -0
  3. package/dist/backends/index.d.ts.map +1 -1
  4. package/dist/backends/index.js +41 -0
  5. package/dist/backends/index.js.map +1 -1
  6. package/dist/backends/index.test.d.ts +5 -0
  7. package/dist/backends/index.test.d.ts.map +1 -0
  8. package/dist/backends/index.test.js +156 -0
  9. package/dist/backends/index.test.js.map +1 -0
  10. package/dist/backends/local-jsonl-boolean-search.test.js +15 -12
  11. package/dist/backends/local-jsonl-boolean-search.test.js.map +1 -1
  12. package/dist/backends/local-jsonl-cache.test.d.ts +2 -0
  13. package/dist/backends/local-jsonl-cache.test.d.ts.map +1 -0
  14. package/dist/backends/local-jsonl-cache.test.js +295 -0
  15. package/dist/backends/local-jsonl-cache.test.js.map +1 -0
  16. package/dist/backends/local-jsonl-circuit-breaker.test.d.ts +2 -0
  17. package/dist/backends/local-jsonl-circuit-breaker.test.d.ts.map +1 -0
  18. package/dist/backends/local-jsonl-circuit-breaker.test.js +180 -0
  19. package/dist/backends/local-jsonl-circuit-breaker.test.js.map +1 -0
  20. package/dist/backends/local-jsonl-export.test.d.ts +2 -0
  21. package/dist/backends/local-jsonl-export.test.d.ts.map +1 -0
  22. package/dist/backends/local-jsonl-export.test.js +704 -0
  23. package/dist/backends/local-jsonl-export.test.js.map +1 -0
  24. package/dist/backends/local-jsonl-index.test.d.ts +2 -0
  25. package/dist/backends/local-jsonl-index.test.d.ts.map +1 -0
  26. package/dist/backends/local-jsonl-index.test.js +554 -0
  27. package/dist/backends/local-jsonl-index.test.js.map +1 -0
  28. package/dist/backends/local-jsonl-logs.test.d.ts +2 -0
  29. package/dist/backends/local-jsonl-logs.test.d.ts.map +1 -0
  30. package/dist/backends/local-jsonl-logs.test.js +612 -0
  31. package/dist/backends/local-jsonl-logs.test.js.map +1 -0
  32. package/dist/backends/local-jsonl-metrics.test.d.ts +2 -0
  33. package/dist/backends/local-jsonl-metrics.test.d.ts.map +1 -0
  34. package/dist/backends/local-jsonl-metrics.test.js +876 -0
  35. package/dist/backends/local-jsonl-metrics.test.js.map +1 -0
  36. package/dist/backends/local-jsonl-traces.test.d.ts +2 -0
  37. package/dist/backends/local-jsonl-traces.test.d.ts.map +1 -0
  38. package/dist/backends/local-jsonl-traces.test.js +1729 -0
  39. package/dist/backends/local-jsonl-traces.test.js.map +1 -0
  40. package/dist/backends/local-jsonl.d.ts +9 -0
  41. package/dist/backends/local-jsonl.d.ts.map +1 -1
  42. package/dist/backends/local-jsonl.js +348 -227
  43. package/dist/backends/local-jsonl.js.map +1 -1
  44. package/dist/backends/local-jsonl.test.js +290 -21
  45. package/dist/backends/local-jsonl.test.js.map +1 -1
  46. package/dist/backends/signoz-api-circuit-breaker.test.d.ts +6 -0
  47. package/dist/backends/signoz-api-circuit-breaker.test.d.ts.map +1 -0
  48. package/dist/backends/signoz-api-circuit-breaker.test.js +548 -0
  49. package/dist/backends/signoz-api-circuit-breaker.test.js.map +1 -0
  50. package/dist/backends/signoz-api-rate-limiter.test.d.ts +6 -0
  51. package/dist/backends/signoz-api-rate-limiter.test.d.ts.map +1 -0
  52. package/dist/backends/signoz-api-rate-limiter.test.js +389 -0
  53. package/dist/backends/signoz-api-rate-limiter.test.js.map +1 -0
  54. package/dist/backends/signoz-api-ssrf.test.d.ts +6 -0
  55. package/dist/backends/signoz-api-ssrf.test.d.ts.map +1 -0
  56. package/dist/backends/signoz-api-ssrf.test.js +216 -0
  57. package/dist/backends/signoz-api-ssrf.test.js.map +1 -0
  58. package/dist/backends/signoz-api-test-helpers.d.ts +80 -0
  59. package/dist/backends/signoz-api-test-helpers.d.ts.map +1 -0
  60. package/dist/backends/signoz-api-test-helpers.js +79 -0
  61. package/dist/backends/signoz-api-test-helpers.js.map +1 -0
  62. package/dist/backends/signoz-api.d.ts +16 -0
  63. package/dist/backends/signoz-api.d.ts.map +1 -1
  64. package/dist/backends/signoz-api.js +71 -9
  65. package/dist/backends/signoz-api.js.map +1 -1
  66. package/dist/backends/signoz-api.test.d.ts +9 -0
  67. package/dist/backends/signoz-api.test.d.ts.map +1 -1
  68. package/dist/backends/signoz-api.test.js +14 -1027
  69. package/dist/backends/signoz-api.test.js.map +1 -1
  70. package/dist/lib/cache.d.ts +47 -1
  71. package/dist/lib/cache.d.ts.map +1 -1
  72. package/dist/lib/cache.js +40 -3
  73. package/dist/lib/cache.js.map +1 -1
  74. package/dist/lib/circuit-breaker.d.ts +83 -0
  75. package/dist/lib/circuit-breaker.d.ts.map +1 -0
  76. package/dist/lib/circuit-breaker.js +125 -0
  77. package/dist/lib/circuit-breaker.js.map +1 -0
  78. package/dist/lib/circuit-breaker.test.d.ts +2 -0
  79. package/dist/lib/circuit-breaker.test.d.ts.map +1 -0
  80. package/dist/lib/circuit-breaker.test.js +263 -0
  81. package/dist/lib/circuit-breaker.test.js.map +1 -0
  82. package/dist/lib/constants-symlink.test.d.ts +12 -0
  83. package/dist/lib/constants-symlink.test.d.ts.map +1 -0
  84. package/dist/lib/constants-symlink.test.js +357 -0
  85. package/dist/lib/constants-symlink.test.js.map +1 -0
  86. package/dist/lib/constants.d.ts +43 -0
  87. package/dist/lib/constants.d.ts.map +1 -1
  88. package/dist/lib/constants.js +154 -24
  89. package/dist/lib/constants.js.map +1 -1
  90. package/dist/lib/constants.test.js +156 -7
  91. package/dist/lib/constants.test.js.map +1 -1
  92. package/dist/lib/edge-cases.test.d.ts +11 -0
  93. package/dist/lib/edge-cases.test.d.ts.map +1 -0
  94. package/dist/lib/edge-cases.test.js +634 -0
  95. package/dist/lib/edge-cases.test.js.map +1 -0
  96. package/dist/lib/error-sanitizer.d.ts.map +1 -1
  97. package/dist/lib/error-sanitizer.js +62 -26
  98. package/dist/lib/error-sanitizer.js.map +1 -1
  99. package/dist/lib/error-sanitizer.test.js +186 -0
  100. package/dist/lib/error-sanitizer.test.js.map +1 -1
  101. package/dist/lib/error-types.d.ts +54 -0
  102. package/dist/lib/error-types.d.ts.map +1 -0
  103. package/dist/lib/error-types.js +154 -0
  104. package/dist/lib/error-types.js.map +1 -0
  105. package/dist/lib/error-types.test.d.ts +2 -0
  106. package/dist/lib/error-types.test.d.ts.map +1 -0
  107. package/dist/lib/error-types.test.js +196 -0
  108. package/dist/lib/error-types.test.js.map +1 -0
  109. package/dist/lib/file-utils.test.js +3 -3
  110. package/dist/lib/file-utils.test.js.map +1 -1
  111. package/dist/lib/indexer.test.js +157 -24
  112. package/dist/lib/indexer.test.js.map +1 -1
  113. package/dist/lib/input-validator.d.ts +17 -0
  114. package/dist/lib/input-validator.d.ts.map +1 -1
  115. package/dist/lib/input-validator.fuzz.test.d.ts +12 -0
  116. package/dist/lib/input-validator.fuzz.test.d.ts.map +1 -0
  117. package/dist/lib/input-validator.fuzz.test.js +290 -0
  118. package/dist/lib/input-validator.fuzz.test.js.map +1 -0
  119. package/dist/lib/input-validator.js +62 -3
  120. package/dist/lib/input-validator.js.map +1 -1
  121. package/dist/lib/input-validator.test.js +129 -1
  122. package/dist/lib/input-validator.test.js.map +1 -1
  123. package/dist/lib/logger.d.ts +46 -0
  124. package/dist/lib/logger.d.ts.map +1 -0
  125. package/dist/lib/logger.js +81 -0
  126. package/dist/lib/logger.js.map +1 -0
  127. package/dist/lib/logger.test.d.ts +2 -0
  128. package/dist/lib/logger.test.d.ts.map +1 -0
  129. package/dist/lib/logger.test.js +122 -0
  130. package/dist/lib/logger.test.js.map +1 -0
  131. package/dist/lib/query-sanitizer.d.ts +51 -3
  132. package/dist/lib/query-sanitizer.d.ts.map +1 -1
  133. package/dist/lib/query-sanitizer.js +105 -31
  134. package/dist/lib/query-sanitizer.js.map +1 -1
  135. package/dist/lib/query-sanitizer.test.js +102 -1
  136. package/dist/lib/query-sanitizer.test.js.map +1 -1
  137. package/dist/lib/server-utils.d.ts +88 -0
  138. package/dist/lib/server-utils.d.ts.map +1 -0
  139. package/dist/lib/server-utils.js +173 -0
  140. package/dist/lib/server-utils.js.map +1 -0
  141. package/dist/lib/shared-schemas.d.ts +81 -0
  142. package/dist/lib/shared-schemas.d.ts.map +1 -0
  143. package/dist/lib/shared-schemas.js +80 -0
  144. package/dist/lib/shared-schemas.js.map +1 -0
  145. package/dist/lib/shared-schemas.test.d.ts +5 -0
  146. package/dist/lib/shared-schemas.test.d.ts.map +1 -0
  147. package/dist/lib/shared-schemas.test.js +106 -0
  148. package/dist/lib/shared-schemas.test.js.map +1 -0
  149. package/dist/lib/toon-encoder.d.ts +26 -0
  150. package/dist/lib/toon-encoder.d.ts.map +1 -0
  151. package/dist/lib/toon-encoder.js +61 -0
  152. package/dist/lib/toon-encoder.js.map +1 -0
  153. package/dist/lib/toon-encoder.test.d.ts +5 -0
  154. package/dist/lib/toon-encoder.test.d.ts.map +1 -0
  155. package/dist/lib/toon-encoder.test.js +85 -0
  156. package/dist/lib/toon-encoder.test.js.map +1 -0
  157. package/dist/server.d.ts +1 -49
  158. package/dist/server.d.ts.map +1 -1
  159. package/dist/server.js +154 -162
  160. package/dist/server.js.map +1 -1
  161. package/dist/server.test.js +198 -7
  162. package/dist/server.test.js.map +1 -1
  163. package/dist/test-helpers/env-utils.d.ts +87 -0
  164. package/dist/test-helpers/env-utils.d.ts.map +1 -0
  165. package/dist/test-helpers/env-utils.js +132 -0
  166. package/dist/test-helpers/env-utils.js.map +1 -0
  167. package/dist/test-helpers/file-utils.d.ts +67 -0
  168. package/dist/test-helpers/file-utils.d.ts.map +1 -1
  169. package/dist/test-helpers/file-utils.js +165 -2
  170. package/dist/test-helpers/file-utils.js.map +1 -1
  171. package/dist/test-helpers/fuzz-generators.d.ts +58 -0
  172. package/dist/test-helpers/fuzz-generators.d.ts.map +1 -0
  173. package/dist/test-helpers/fuzz-generators.js +216 -0
  174. package/dist/test-helpers/fuzz-generators.js.map +1 -0
  175. package/dist/test-helpers/index.d.ts +11 -0
  176. package/dist/test-helpers/index.d.ts.map +1 -0
  177. package/dist/test-helpers/index.js +30 -0
  178. package/dist/test-helpers/index.js.map +1 -0
  179. package/dist/test-helpers/memfs-utils.d.ts +181 -0
  180. package/dist/test-helpers/memfs-utils.d.ts.map +1 -0
  181. package/dist/test-helpers/memfs-utils.js +292 -0
  182. package/dist/test-helpers/memfs-utils.js.map +1 -0
  183. package/dist/test-helpers/memfs-utils.test.d.ts +5 -0
  184. package/dist/test-helpers/memfs-utils.test.d.ts.map +1 -0
  185. package/dist/test-helpers/memfs-utils.test.js +338 -0
  186. package/dist/test-helpers/memfs-utils.test.js.map +1 -0
  187. package/dist/test-helpers/mock-backends.d.ts +113 -2
  188. package/dist/test-helpers/mock-backends.d.ts.map +1 -1
  189. package/dist/test-helpers/mock-backends.js +199 -3
  190. package/dist/test-helpers/mock-backends.js.map +1 -1
  191. package/dist/test-helpers/mock-backends.test.d.ts +5 -0
  192. package/dist/test-helpers/mock-backends.test.d.ts.map +1 -0
  193. package/dist/test-helpers/mock-backends.test.js +368 -0
  194. package/dist/test-helpers/mock-backends.test.js.map +1 -0
  195. package/dist/test-helpers/race-condition-helpers.d.ts +85 -0
  196. package/dist/test-helpers/race-condition-helpers.d.ts.map +1 -0
  197. package/dist/test-helpers/race-condition-helpers.js +279 -0
  198. package/dist/test-helpers/race-condition-helpers.js.map +1 -0
  199. package/dist/test-helpers/schema-validators.d.ts +32 -0
  200. package/dist/test-helpers/schema-validators.d.ts.map +1 -0
  201. package/dist/test-helpers/schema-validators.js +125 -0
  202. package/dist/test-helpers/schema-validators.js.map +1 -0
  203. package/dist/test-helpers/test-data-builders.d.ts +260 -0
  204. package/dist/test-helpers/test-data-builders.d.ts.map +1 -0
  205. package/dist/test-helpers/test-data-builders.js +337 -0
  206. package/dist/test-helpers/test-data-builders.js.map +1 -0
  207. package/dist/test-helpers/test-data-builders.test.d.ts +2 -0
  208. package/dist/test-helpers/test-data-builders.test.d.ts.map +1 -0
  209. package/dist/test-helpers/test-data-builders.test.js +306 -0
  210. package/dist/test-helpers/test-data-builders.test.js.map +1 -0
  211. package/dist/test-helpers/tool-validators.d.ts +28 -0
  212. package/dist/test-helpers/tool-validators.d.ts.map +1 -0
  213. package/dist/test-helpers/tool-validators.js +71 -0
  214. package/dist/test-helpers/tool-validators.js.map +1 -0
  215. package/dist/tools/context-stats.d.ts +1 -0
  216. package/dist/tools/context-stats.d.ts.map +1 -1
  217. package/dist/tools/context-stats.js +9 -5
  218. package/dist/tools/context-stats.js.map +1 -1
  219. package/dist/tools/context-stats.test.js +24 -10
  220. package/dist/tools/context-stats.test.js.map +1 -1
  221. package/dist/tools/get-trace-url.js +2 -2
  222. package/dist/tools/get-trace-url.js.map +1 -1
  223. package/dist/tools/health-check.js +2 -2
  224. package/dist/tools/health-check.js.map +1 -1
  225. package/dist/tools/query-evaluations.d.ts +21 -18
  226. package/dist/tools/query-evaluations.d.ts.map +1 -1
  227. package/dist/tools/query-evaluations.js +33 -19
  228. package/dist/tools/query-evaluations.js.map +1 -1
  229. package/dist/tools/query-evaluations.test.js +60 -63
  230. package/dist/tools/query-evaluations.test.js.map +1 -1
  231. package/dist/tools/query-llm-events.d.ts +19 -15
  232. package/dist/tools/query-llm-events.d.ts.map +1 -1
  233. package/dist/tools/query-llm-events.js +31 -15
  234. package/dist/tools/query-llm-events.js.map +1 -1
  235. package/dist/tools/query-llm-events.test.js +277 -12
  236. package/dist/tools/query-llm-events.test.js.map +1 -1
  237. package/dist/tools/query-logs.d.ts +22 -22
  238. package/dist/tools/query-logs.d.ts.map +1 -1
  239. package/dist/tools/query-logs.js +9 -9
  240. package/dist/tools/query-logs.js.map +1 -1
  241. package/dist/tools/query-logs.test.js +19 -72
  242. package/dist/tools/query-logs.test.js.map +1 -1
  243. package/dist/tools/query-metrics.d.ts +14 -14
  244. package/dist/tools/query-metrics.d.ts.map +1 -1
  245. package/dist/tools/query-metrics.js +9 -9
  246. package/dist/tools/query-metrics.js.map +1 -1
  247. package/dist/tools/query-metrics.test.js +12 -25
  248. package/dist/tools/query-metrics.test.js.map +1 -1
  249. package/dist/tools/query-traces.d.ts +28 -28
  250. package/dist/tools/query-traces.d.ts.map +1 -1
  251. package/dist/tools/query-traces.js +18 -18
  252. package/dist/tools/query-traces.js.map +1 -1
  253. package/dist/tools/query-traces.test.js +58 -54
  254. package/dist/tools/query-traces.test.js.map +1 -1
  255. package/dist/tools/setup-claudeignore.js +7 -7
  256. package/dist/tools/setup-claudeignore.js.map +1 -1
  257. package/dist/tools/setup-claudeignore.test.js +4 -25
  258. package/dist/tools/setup-claudeignore.test.js.map +1 -1
  259. 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