claw-dashboard 1.9.0 → 2.0.0

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 (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5236 -566
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -5
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1941 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1057 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
@@ -0,0 +1,539 @@
1
+ /**
2
+ * Unit tests for RateLimiter class
3
+ */
4
+
5
+ import alerts from '../src/alerts.js';
6
+
7
+ const { RateLimiter } = alerts;
8
+
9
+ describe('RateLimiter', () => {
10
+ beforeEach(() => {
11
+ alerts.resetRateLimit();
12
+ });
13
+
14
+ afterEach(() => {
15
+ alerts.resetRateLimit();
16
+ });
17
+
18
+ describe('constructor', () => {
19
+ test('should create RateLimiter with default options', () => {
20
+ const limiter = new RateLimiter();
21
+
22
+ expect(limiter.enabled).toBe(true);
23
+ expect(limiter.windowMs).toBe(60000);
24
+ expect(limiter.maxAlerts).toBe(5);
25
+ expect(limiter.alwaysAllowCritical).toBe(true);
26
+ expect(limiter.timestamps).toEqual({});
27
+ });
28
+
29
+ test('should create RateLimiter with custom options', () => {
30
+ const limiter = new RateLimiter({
31
+ enabled: false,
32
+ windowMs: 30000,
33
+ maxAlerts: 3,
34
+ alwaysAllowCritical: false
35
+ });
36
+
37
+ expect(limiter.enabled).toBe(false);
38
+ expect(limiter.windowMs).toBe(30000);
39
+ expect(limiter.maxAlerts).toBe(3);
40
+ expect(limiter.alwaysAllowCritical).toBe(false);
41
+ });
42
+
43
+ test('should partially apply custom options with defaults for others', () => {
44
+ const limiter = new RateLimiter({
45
+ maxAlerts: 10
46
+ });
47
+
48
+ expect(limiter.enabled).toBe(true); // default
49
+ expect(limiter.windowMs).toBe(60000); // default
50
+ expect(limiter.maxAlerts).toBe(10); // custom
51
+ expect(limiter.alwaysAllowCritical).toBe(true); // default
52
+ });
53
+ });
54
+
55
+ describe('check', () => {
56
+ test('should allow all alerts when disabled', () => {
57
+ const limiter = new RateLimiter({ enabled: false });
58
+
59
+ for (let i = 0; i < 10; i++) {
60
+ const result = limiter.check('cpu', 'warning');
61
+ expect(result.allowed).toBe(true);
62
+ expect(result.reason).toBe('rate_limiting_disabled');
63
+ }
64
+ });
65
+
66
+ test('should allow alert when under limit', () => {
67
+ const limiter = new RateLimiter({ maxAlerts: 3 });
68
+
69
+ const result = limiter.check('cpu', 'warning');
70
+ expect(result.allowed).toBe(true);
71
+ expect(result.reason).toBe('ok');
72
+ });
73
+
74
+ test('should block alert when over limit', () => {
75
+ const limiter = new RateLimiter({ maxAlerts: 2 });
76
+
77
+ // Record two alerts to reach limit
78
+ limiter.record('cpu', 'warning');
79
+ limiter.record('cpu', 'warning');
80
+
81
+ const result = limiter.check('cpu', 'warning');
82
+ expect(result.allowed).toBe(false);
83
+ expect(result.reason).toBe('rate_limit_exceeded');
84
+ });
85
+
86
+ test('should always allow critical when alwaysAllowCritical is true', () => {
87
+ const limiter = new RateLimiter({
88
+ maxAlerts: 1,
89
+ alwaysAllowCritical: true
90
+ });
91
+
92
+ // Record one alert to reach limit
93
+ limiter.record('cpu', 'warning');
94
+
95
+ // Warning should be blocked
96
+ const warningResult = limiter.check('cpu', 'warning');
97
+ expect(warningResult.allowed).toBe(false);
98
+
99
+ // Critical should be allowed
100
+ const criticalResult = limiter.check('cpu', 'critical');
101
+ expect(criticalResult.allowed).toBe(true);
102
+ expect(criticalResult.reason).toBe('critical_always_allowed');
103
+ });
104
+
105
+ test('should block critical when alwaysAllowCritical is false', () => {
106
+ const limiter = new RateLimiter({
107
+ maxAlerts: 1,
108
+ alwaysAllowCritical: false
109
+ });
110
+
111
+ // Record one alert to reach limit
112
+ limiter.record('cpu', 'critical');
113
+
114
+ // Another critical should be blocked
115
+ const result = limiter.check('cpu', 'critical');
116
+ expect(result.allowed).toBe(false);
117
+ expect(result.reason).toBe('rate_limit_exceeded');
118
+ });
119
+
120
+ test('should track different alert types separately', () => {
121
+ const limiter = new RateLimiter({ maxAlerts: 1 });
122
+
123
+ // Fill up cpu
124
+ limiter.record('cpu', 'warning');
125
+ expect(limiter.check('cpu', 'warning').allowed).toBe(false);
126
+
127
+ // Memory should still be allowed
128
+ expect(limiter.check('memory', 'warning').allowed).toBe(true);
129
+ });
130
+
131
+ test('should default to warning level when not specified', () => {
132
+ const limiter = new RateLimiter({ maxAlerts: 1 });
133
+
134
+ limiter.record('cpu', 'warning');
135
+
136
+ // Check without level should use warning
137
+ const result = limiter.check('cpu');
138
+ expect(result.allowed).toBe(false);
139
+ });
140
+ });
141
+
142
+ describe('checkAndRecord', () => {
143
+ test('should check and record when allowed', () => {
144
+ const limiter = new RateLimiter({ maxAlerts: 2 });
145
+
146
+ const result = limiter.checkAndRecord('cpu', 'warning');
147
+ expect(result.allowed).toBe(true);
148
+ expect(result.reason).toBe('ok');
149
+
150
+ // Verify it was recorded
151
+ expect(limiter.getCount('cpu')).toBe(1);
152
+ });
153
+
154
+ test('should check but not record when blocked', () => {
155
+ const limiter = new RateLimiter({ maxAlerts: 1 });
156
+
157
+ // First alert
158
+ limiter.checkAndRecord('cpu', 'warning');
159
+ expect(limiter.getCount('cpu')).toBe(1);
160
+
161
+ // Second should be blocked and NOT recorded
162
+ const result = limiter.checkAndRecord('cpu', 'warning');
163
+ expect(result.allowed).toBe(false);
164
+ expect(result.reason).toBe('rate_limit_exceeded');
165
+ expect(limiter.getCount('cpu')).toBe(1); // Still 1, not incremented
166
+ });
167
+
168
+ test('should allow critical even when rate limited and record it', () => {
169
+ const limiter = new RateLimiter({ maxAlerts: 1 });
170
+
171
+ // Fill up with warning
172
+ limiter.checkAndRecord('cpu', 'warning');
173
+
174
+ // Critical should pass and be recorded
175
+ const result = limiter.checkAndRecord('cpu', 'critical');
176
+ expect(result.allowed).toBe(true);
177
+ expect(result.reason).toBe('critical_always_allowed');
178
+ expect(limiter.getCount('cpu')).toBe(2); // Both recorded
179
+ });
180
+
181
+ test('should not record when rate limiting disabled', () => {
182
+ const limiter = new RateLimiter({ enabled: false });
183
+
184
+ limiter.checkAndRecord('cpu', 'warning');
185
+ limiter.checkAndRecord('cpu', 'warning');
186
+
187
+ // Should not have recorded anything
188
+ expect(limiter.getCount('cpu')).toBe(0);
189
+ });
190
+ });
191
+
192
+ describe('record', () => {
193
+ test('should record alert timestamp', () => {
194
+ const limiter = new RateLimiter();
195
+
196
+ limiter.record('cpu', 'warning');
197
+ expect(limiter.getCount('cpu')).toBe(1);
198
+
199
+ limiter.record('cpu', 'warning');
200
+ expect(limiter.getCount('cpu')).toBe(2);
201
+ });
202
+
203
+ test('should not record when disabled', () => {
204
+ const limiter = new RateLimiter({ enabled: false });
205
+
206
+ limiter.record('cpu', 'warning');
207
+ expect(limiter.getCount('cpu')).toBe(0);
208
+ });
209
+
210
+ test('should record multiple types independently', () => {
211
+ const limiter = new RateLimiter();
212
+
213
+ limiter.record('cpu', 'warning');
214
+ limiter.record('cpu', 'warning');
215
+ limiter.record('memory', 'warning');
216
+
217
+ expect(limiter.getCount('cpu')).toBe(2);
218
+ expect(limiter.getCount('memory')).toBe(1);
219
+ expect(limiter.getCount('disk')).toBe(0);
220
+ });
221
+ });
222
+
223
+ describe('getCount', () => {
224
+ test('should return 0 when no alerts recorded', () => {
225
+ const limiter = new RateLimiter();
226
+ expect(limiter.getCount('cpu')).toBe(0);
227
+ });
228
+
229
+ test('should return count of alerts in current window', () => {
230
+ const limiter = new RateLimiter({ windowMs: 60000 });
231
+
232
+ limiter.record('cpu', 'warning');
233
+ limiter.record('cpu', 'warning');
234
+ limiter.record('cpu', 'warning');
235
+
236
+ expect(limiter.getCount('cpu')).toBe(3);
237
+ });
238
+
239
+ test('should exclude expired timestamps', async () => {
240
+ const limiter = new RateLimiter({ windowMs: 50 });
241
+
242
+ limiter.record('cpu', 'warning');
243
+ limiter.record('cpu', 'warning');
244
+ expect(limiter.getCount('cpu')).toBe(2);
245
+
246
+ // Wait for window to expire
247
+ await new Promise(resolve => setTimeout(resolve, 60));
248
+
249
+ // Old timestamps should be expired
250
+ expect(limiter.getCount('cpu')).toBe(0);
251
+ });
252
+ });
253
+
254
+ describe('getRetryAfter', () => {
255
+ test('should return 0 when no alerts recorded', () => {
256
+ const limiter = new RateLimiter();
257
+ expect(limiter.getRetryAfter('cpu')).toBe(0);
258
+ });
259
+
260
+ test('should return 0 when under limit', () => {
261
+ const limiter = new RateLimiter({ maxAlerts: 3 });
262
+
263
+ limiter.record('cpu', 'warning');
264
+ limiter.record('cpu', 'warning');
265
+
266
+ expect(limiter.getRetryAfter('cpu')).toBe(0);
267
+ });
268
+
269
+ test('should return time until oldest expires when at limit', async () => {
270
+ const windowMs = 100;
271
+ const limiter = new RateLimiter({ maxAlerts: 2, windowMs });
272
+
273
+ // Record two alerts at limit
274
+ limiter.record('cpu', 'warning');
275
+ await new Promise(resolve => setTimeout(resolve, 20));
276
+ limiter.record('cpu', 'warning');
277
+
278
+ // At limit, should return time until first expires
279
+ const retryAfter = limiter.getRetryAfter('cpu');
280
+ expect(retryAfter).toBeGreaterThan(0);
281
+ expect(retryAfter).toBeLessThanOrEqual(windowMs);
282
+ });
283
+
284
+ test('should return 0 after window expires', async () => {
285
+ const limiter = new RateLimiter({ maxAlerts: 1, windowMs: 50 });
286
+
287
+ limiter.record('cpu', 'warning');
288
+ expect(limiter.getRetryAfter('cpu')).toBeGreaterThan(0);
289
+
290
+ // Wait for window to expire
291
+ await new Promise(resolve => setTimeout(resolve, 60));
292
+
293
+ expect(limiter.getRetryAfter('cpu')).toBe(0);
294
+ });
295
+
296
+ test('should handle different types independently', () => {
297
+ const limiter = new RateLimiter({ maxAlerts: 1 });
298
+
299
+ limiter.record('cpu', 'warning');
300
+
301
+ expect(limiter.getRetryAfter('cpu')).toBeGreaterThan(0);
302
+ expect(limiter.getRetryAfter('memory')).toBe(0);
303
+ });
304
+ });
305
+
306
+ describe('getStatus', () => {
307
+ test('should return full status object', () => {
308
+ const limiter = new RateLimiter({
309
+ enabled: true,
310
+ windowMs: 30000,
311
+ maxAlerts: 3,
312
+ alwaysAllowCritical: false
313
+ });
314
+
315
+ const status = limiter.getStatus();
316
+
317
+ expect(status.enabled).toBe(true);
318
+ expect(status.windowMs).toBe(30000);
319
+ expect(status.maxAlerts).toBe(3);
320
+ expect(status.alwaysAllowCritical).toBe(false);
321
+ expect(status.types).toEqual({});
322
+ });
323
+
324
+ test('should include type status for recorded alerts', () => {
325
+ const limiter = new RateLimiter({ maxAlerts: 5 });
326
+
327
+ limiter.record('cpu', 'warning');
328
+ limiter.record('cpu', 'warning');
329
+ limiter.record('memory', 'warning');
330
+
331
+ const status = limiter.getStatus();
332
+
333
+ expect(status.types.cpu).toEqual({
334
+ current: 2,
335
+ max: 5,
336
+ retryAfter: 0
337
+ });
338
+
339
+ expect(status.types.memory).toEqual({
340
+ current: 1,
341
+ max: 5,
342
+ retryAfter: 0
343
+ });
344
+
345
+ expect(status.types.disk).toBeUndefined();
346
+ });
347
+
348
+ test('should include retryAfter when at limit', () => {
349
+ const limiter = new RateLimiter({ maxAlerts: 2 });
350
+
351
+ limiter.record('cpu', 'warning');
352
+ limiter.record('cpu', 'warning');
353
+
354
+ const status = limiter.getStatus();
355
+
356
+ expect(status.types.cpu.current).toBe(2);
357
+ expect(status.types.cpu.retryAfter).toBeGreaterThan(0);
358
+ });
359
+
360
+ test('should exclude expired timestamps from status', async () => {
361
+ const limiter = new RateLimiter({ maxAlerts: 2, windowMs: 50 });
362
+
363
+ limiter.record('cpu', 'warning');
364
+ limiter.record('cpu', 'warning');
365
+
366
+ await new Promise(resolve => setTimeout(resolve, 60));
367
+
368
+ const status = limiter.getStatus();
369
+ expect(status.types.cpu.current).toBe(0);
370
+ expect(status.types.cpu.retryAfter).toBe(0);
371
+ });
372
+ });
373
+
374
+ describe('configure', () => {
375
+ test('should update configuration', () => {
376
+ const limiter = new RateLimiter();
377
+
378
+ limiter.configure({
379
+ enabled: false,
380
+ windowMs: 30000,
381
+ maxAlerts: 10,
382
+ alwaysAllowCritical: false
383
+ });
384
+
385
+ expect(limiter.enabled).toBe(false);
386
+ expect(limiter.windowMs).toBe(30000);
387
+ expect(limiter.maxAlerts).toBe(10);
388
+ expect(limiter.alwaysAllowCritical).toBe(false);
389
+ });
390
+
391
+ test('should partially update configuration', () => {
392
+ const limiter = new RateLimiter();
393
+
394
+ limiter.configure({ maxAlerts: 20 });
395
+
396
+ expect(limiter.enabled).toBe(true); // unchanged
397
+ expect(limiter.windowMs).toBe(60000); // unchanged
398
+ expect(limiter.maxAlerts).toBe(20); // updated
399
+ });
400
+
401
+ test('should affect subsequent checks', () => {
402
+ const limiter = new RateLimiter({ maxAlerts: 1 });
403
+
404
+ limiter.record('cpu', 'warning');
405
+ expect(limiter.check('cpu', 'warning').allowed).toBe(false);
406
+
407
+ // Increase limit
408
+ limiter.configure({ maxAlerts: 3 });
409
+
410
+ // Should now be allowed
411
+ expect(limiter.check('cpu', 'warning').allowed).toBe(true);
412
+ });
413
+ });
414
+
415
+ describe('reset', () => {
416
+ test('should clear all timestamps', () => {
417
+ const limiter = new RateLimiter();
418
+
419
+ limiter.record('cpu', 'warning');
420
+ limiter.record('memory', 'warning');
421
+
422
+ expect(limiter.getCount('cpu')).toBe(1);
423
+ expect(limiter.getCount('memory')).toBe(1);
424
+
425
+ limiter.reset();
426
+
427
+ expect(limiter.getCount('cpu')).toBe(0);
428
+ expect(limiter.getCount('memory')).toBe(0);
429
+ expect(limiter.timestamps).toEqual({});
430
+ });
431
+
432
+ test('should allow alerts after reset', () => {
433
+ const limiter = new RateLimiter({ maxAlerts: 1 });
434
+
435
+ limiter.record('cpu', 'warning');
436
+ expect(limiter.check('cpu', 'warning').allowed).toBe(false);
437
+
438
+ limiter.reset();
439
+
440
+ expect(limiter.check('cpu', 'warning').allowed).toBe(true);
441
+ });
442
+
443
+ test('should preserve configuration after reset', () => {
444
+ const limiter = new RateLimiter({ maxAlerts: 10, windowMs: 30000 });
445
+
446
+ limiter.reset();
447
+
448
+ expect(limiter.maxAlerts).toBe(10);
449
+ expect(limiter.windowMs).toBe(30000);
450
+ });
451
+ });
452
+
453
+ describe('alwaysAllowCritical behavior', () => {
454
+ test('should allow critical alerts past limit when true', () => {
455
+ const limiter = new RateLimiter({
456
+ maxAlerts: 1,
457
+ alwaysAllowCritical: true
458
+ });
459
+
460
+ limiter.record('cpu', 'warning');
461
+
462
+ expect(limiter.check('cpu', 'critical').allowed).toBe(true);
463
+ expect(limiter.check('cpu', 'warning').allowed).toBe(false);
464
+ });
465
+
466
+ test('should block critical alerts past limit when false', () => {
467
+ const limiter = new RateLimiter({
468
+ maxAlerts: 1,
469
+ alwaysAllowCritical: false
470
+ });
471
+
472
+ limiter.record('cpu', 'critical');
473
+
474
+ expect(limiter.check('cpu', 'critical').allowed).toBe(false);
475
+ });
476
+
477
+ test('should respect level parameter variations', () => {
478
+ const limiter = new RateLimiter({
479
+ maxAlerts: 1,
480
+ alwaysAllowCritical: true
481
+ });
482
+
483
+ limiter.record('cpu', 'warning');
484
+
485
+ // Various non-critical levels should be blocked
486
+ expect(limiter.check('cpu', 'warning').allowed).toBe(false);
487
+ expect(limiter.check('cpu', 'info').allowed).toBe(false);
488
+
489
+ // Critical should pass
490
+ expect(limiter.check('cpu', 'critical').allowed).toBe(true);
491
+ });
492
+
493
+ test('should be configurable at runtime', () => {
494
+ const limiter = new RateLimiter({
495
+ maxAlerts: 1,
496
+ alwaysAllowCritical: true
497
+ });
498
+
499
+ limiter.record('cpu', 'warning');
500
+
501
+ // Initially allowed
502
+ expect(limiter.check('cpu', 'critical').allowed).toBe(true);
503
+
504
+ // Disable critical bypass
505
+ limiter.configure({ alwaysAllowCritical: false });
506
+
507
+ // Fill up again
508
+ limiter.record('cpu', 'critical');
509
+
510
+ // Should now be blocked
511
+ expect(limiter.check('cpu', 'critical').allowed).toBe(false);
512
+ });
513
+ });
514
+
515
+ describe('integration with global rate limiting', () => {
516
+ test('should sync with global config on creation', () => {
517
+ // Create limiter should sync its config to global
518
+ new RateLimiter({ maxAlerts: 7 });
519
+
520
+ const globalConfig = alerts.getRateLimit();
521
+ expect(globalConfig.maxAlerts).toBe(7);
522
+ });
523
+
524
+ test('should sync with global config on configure', () => {
525
+ const limiter = new RateLimiter();
526
+
527
+ limiter.configure({ maxAlerts: 15 });
528
+
529
+ const globalConfig = alerts.getRateLimit();
530
+ expect(globalConfig.maxAlerts).toBe(15);
531
+ });
532
+
533
+ test('defaultRateLimiter instance should exist', () => {
534
+ // Verify the exported defaultRateLimiter exists
535
+ expect(alerts.defaultRateLimiter).toBeDefined();
536
+ expect(alerts.defaultRateLimiter).toBeInstanceOf(RateLimiter);
537
+ });
538
+ });
539
+ });