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,437 @@
1
+ /**
2
+ * Unit tests for alert threshold logic in alerts.js
3
+ */
4
+
5
+ import alerts from '../src/alerts.js';
6
+
7
+ describe('Alert Threshold Logic', () => {
8
+ beforeEach(() => {
9
+ // Reset thresholds and clear alerts before each test
10
+ alerts.resetThresholds();
11
+ alerts.clearAllAlerts();
12
+ alerts.resetRateLimit();
13
+ });
14
+
15
+ describe('checkThreshold', () => {
16
+ test('should return null when value is below warning threshold', () => {
17
+ const result = alerts.checkThreshold('cpu', 50);
18
+ expect(result).toBeNull();
19
+ });
20
+
21
+ test('should create warning alert when value reaches warning threshold', () => {
22
+ const result = alerts.checkThreshold('cpu', 70);
23
+
24
+ expect(result).not.toBeNull();
25
+ expect(result.type).toBe('cpu');
26
+ expect(result.level).toBe(alerts.AlertLevel.WARNING);
27
+ expect(result.value).toBe(70);
28
+ expect(result.threshold).toBe(70);
29
+ expect(result.message).toContain('Warning');
30
+ });
31
+
32
+ test('should create critical alert when value reaches critical threshold', () => {
33
+ const result = alerts.checkThreshold('cpu', 90);
34
+
35
+ expect(result).not.toBeNull();
36
+ expect(result.type).toBe('cpu');
37
+ expect(result.level).toBe(alerts.AlertLevel.CRITICAL);
38
+ expect(result.value).toBe(90);
39
+ expect(result.threshold).toBe(90);
40
+ expect(result.message).toContain('Critical');
41
+ });
42
+
43
+ test('should upgrade warning to critical when value crosses threshold', () => {
44
+ // First create a warning
45
+ alerts.checkThreshold('cpu', 70);
46
+ const criticalAlert = alerts.checkThreshold('cpu', 90);
47
+
48
+ expect(criticalAlert).not.toBeNull();
49
+ expect(criticalAlert.level).toBe(alerts.AlertLevel.CRITICAL);
50
+
51
+ // Should have critical alert in active alerts
52
+ const activeAlerts = alerts.getActiveAlerts();
53
+ const cpuAlert = activeAlerts.find(a => a.type === 'cpu');
54
+ expect(cpuAlert.level).toBe(alerts.AlertLevel.CRITICAL);
55
+ });
56
+
57
+ test('should not create duplicate alerts for same level', () => {
58
+ alerts.checkThreshold('cpu', 75);
59
+ const activeBefore = alerts.getActiveAlerts().length;
60
+
61
+ alerts.checkThreshold('cpu', 78);
62
+ const activeAfter = alerts.getActiveAlerts().length;
63
+
64
+ // Should not add duplicate - just update existing
65
+ expect(activeBefore).toBe(activeAfter);
66
+ });
67
+
68
+ test('should create cleared alert when value drops below threshold', () => {
69
+ // Create a warning alert first
70
+ alerts.checkThreshold('cpu', 75);
71
+
72
+ // Clear the alert by dropping below threshold
73
+ const clearedAlert = alerts.checkThreshold('cpu', 50);
74
+
75
+ expect(clearedAlert).not.toBeNull();
76
+ expect(clearedAlert.level).toBe(alerts.AlertLevel.CLEARED);
77
+ expect(clearedAlert.message).toContain('normalized');
78
+ });
79
+
80
+ test('should return null when updating existing alert with same level', () => {
81
+ alerts.checkThreshold('cpu', 75);
82
+ const result = alerts.checkThreshold('cpu', 80);
83
+
84
+ // Should update but return null (no new alert)
85
+ expect(result).toBeNull();
86
+ });
87
+
88
+ test('should handle memory metric type', () => {
89
+ const result = alerts.checkThreshold('memory', 80);
90
+
91
+ expect(result).not.toBeNull();
92
+ expect(result.type).toBe('memory');
93
+ expect(result.level).toBe(alerts.AlertLevel.WARNING);
94
+ });
95
+
96
+ test('should handle disk metric type', () => {
97
+ const result = alerts.checkThreshold('disk', 85);
98
+
99
+ expect(result).not.toBeNull();
100
+ expect(result.type).toBe('disk');
101
+ expect(result.level).toBe(alerts.AlertLevel.WARNING);
102
+ });
103
+
104
+ test('should return null for unknown metric type', () => {
105
+ const result = alerts.checkThreshold('unknown', 50);
106
+ expect(result).toBeNull();
107
+ });
108
+
109
+ test('should use custom thresholds when set', () => {
110
+ alerts.setThresholds({
111
+ cpu: { warning: 50, critical: 80 }
112
+ });
113
+
114
+ // Should trigger warning at 50
115
+ const warningResult = alerts.checkThreshold('cpu', 50);
116
+ expect(warningResult.level).toBe(alerts.AlertLevel.WARNING);
117
+
118
+ // Should trigger critical at 80
119
+ const criticalResult = alerts.checkThreshold('cpu', 80);
120
+ expect(criticalResult.level).toBe(alerts.AlertLevel.CRITICAL);
121
+ });
122
+
123
+ test('should handle edge case at exact threshold values', () => {
124
+ // Reset to ensure clean state
125
+ alerts.clearAllAlerts();
126
+
127
+ // Exact warning threshold
128
+ const warningResult = alerts.checkThreshold('cpu', 70);
129
+ expect(warningResult.level).toBe(alerts.AlertLevel.WARNING);
130
+
131
+ // Exact critical threshold
132
+ const criticalResult = alerts.checkThreshold('cpu', 90);
133
+ expect(criticalResult.level).toBe(alerts.AlertLevel.CRITICAL);
134
+
135
+ // Clear the alert - value below warning but existing critical alert exists
136
+ const clearedResult = alerts.checkThreshold('cpu', 69);
137
+ // Should create a cleared alert since there was an active critical alert
138
+ expect(clearedResult).not.toBeNull();
139
+ expect(clearedResult.level).toBe(alerts.AlertLevel.CLEARED);
140
+ });
141
+ });
142
+
143
+ describe('checkAllMetrics', () => {
144
+ test('should check all provided metrics', () => {
145
+ const metrics = {
146
+ cpu: 85,
147
+ memory: 80,
148
+ disk: 90
149
+ };
150
+
151
+ const results = alerts.checkAllMetrics(metrics);
152
+
153
+ expect(results.length).toBe(3);
154
+ expect(results.map(r => r.type)).toContain('cpu');
155
+ expect(results.map(r => r.type)).toContain('memory');
156
+ expect(results.map(r => r.type)).toContain('disk');
157
+ });
158
+
159
+ test('should only check metrics that are defined', () => {
160
+ const metrics = {
161
+ cpu: 85
162
+ // memory and disk not provided
163
+ };
164
+
165
+ const results = alerts.checkAllMetrics(metrics);
166
+
167
+ expect(results.length).toBe(1);
168
+ expect(results[0].type).toBe('cpu');
169
+ });
170
+
171
+ test('should return empty array when no metrics exceed thresholds', () => {
172
+ const metrics = {
173
+ cpu: 50,
174
+ memory: 50,
175
+ disk: 50
176
+ };
177
+
178
+ const results = alerts.checkAllMetrics(metrics);
179
+
180
+ expect(results.length).toBe(0);
181
+ });
182
+
183
+ test('should handle undefined metrics object', () => {
184
+ const results = alerts.checkAllMetrics(undefined);
185
+ expect(results).toEqual([]);
186
+ });
187
+
188
+ test('should handle empty metrics object', () => {
189
+ const results = alerts.checkAllMetrics({});
190
+ expect(results).toEqual([]);
191
+ });
192
+
193
+ test('should return only new alerts, not updates', () => {
194
+ const metrics1 = { cpu: 75 };
195
+ const results1 = alerts.checkAllMetrics(metrics1);
196
+ expect(results1.length).toBe(1);
197
+
198
+ // Update same metric
199
+ const metrics2 = { cpu: 80 };
200
+ const results2 = alerts.checkAllMetrics(metrics2);
201
+ // This returns null for updates, so should be 0
202
+ expect(results2.length).toBe(0);
203
+ });
204
+ });
205
+
206
+ describe('Alert Level Constants', () => {
207
+ test('should have correct alert level values', () => {
208
+ expect(alerts.AlertLevel.INFO).toBe('info');
209
+ expect(alerts.AlertLevel.WARNING).toBe('warning');
210
+ expect(alerts.AlertLevel.CRITICAL).toBe('critical');
211
+ expect(alerts.AlertLevel.CLEARED).toBe('cleared');
212
+ });
213
+ });
214
+
215
+ describe('Threshold Management', () => {
216
+ test('getThresholds should return current thresholds', () => {
217
+ const thresholds = alerts.getThresholds();
218
+
219
+ expect(thresholds.cpu.warning).toBe(70);
220
+ expect(thresholds.cpu.critical).toBe(90);
221
+ expect(thresholds.memory.warning).toBe(75);
222
+ expect(thresholds.memory.critical).toBe(90);
223
+ expect(thresholds.disk.warning).toBe(80);
224
+ expect(thresholds.disk.critical).toBe(95);
225
+ });
226
+
227
+ test('resetThresholds should restore defaults', () => {
228
+ alerts.setThresholds({ cpu: { warning: 10, critical: 20 } });
229
+ alerts.resetThresholds();
230
+
231
+ const thresholds = alerts.getThresholds();
232
+ expect(thresholds.cpu.warning).toBe(70);
233
+ expect(thresholds.cpu.critical).toBe(90);
234
+ });
235
+ });
236
+
237
+ describe('Alert Management', () => {
238
+ test('getActiveAlerts should return non-dismissed alerts', () => {
239
+ alerts.checkThreshold('cpu', 85);
240
+ alerts.checkThreshold('memory', 85);
241
+
242
+ const active = alerts.getActiveAlerts();
243
+ expect(active.length).toBe(2);
244
+ });
245
+
246
+ test('dismissAlert should mark alert as dismissed', () => {
247
+ const alert = alerts.checkThreshold('cpu', 85);
248
+ alerts.dismissAlert(alert.id);
249
+
250
+ const active = alerts.getActiveAlerts();
251
+ expect(active.find(a => a.id === alert.id)).toBeUndefined();
252
+ });
253
+
254
+ test('getAlertsByLevel should filter by level', () => {
255
+ alerts.checkThreshold('cpu', 95); // critical (above 90%)
256
+ alerts.checkThreshold('memory', 75); // warning
257
+
258
+ const critical = alerts.getAlertsByLevel(alerts.AlertLevel.CRITICAL);
259
+ expect(critical.length).toBe(1);
260
+ expect(critical[0].type).toBe('cpu');
261
+
262
+ const warning = alerts.getAlertsByLevel(alerts.AlertLevel.WARNING);
263
+ expect(warning.length).toBe(1);
264
+ expect(warning[0].type).toBe('memory');
265
+ });
266
+
267
+ test('clearAllAlerts should dismiss all active alerts', () => {
268
+ alerts.checkThreshold('cpu', 85);
269
+ alerts.checkThreshold('memory', 85);
270
+
271
+ alerts.clearAllAlerts();
272
+
273
+ const active = alerts.getActiveAlerts();
274
+ expect(active.length).toBe(0);
275
+ });
276
+
277
+ test('getAlertHistory should return all alerts including dismissed', () => {
278
+ const alert = alerts.checkThreshold('cpu', 85);
279
+ alerts.dismissAlert(alert.id);
280
+
281
+ const history = alerts.getAlertHistory();
282
+ expect(history.length).toBeGreaterThan(0);
283
+ });
284
+ });
285
+ });
286
+
287
+ describe('Rate Limiting', () => {
288
+ beforeEach(() => {
289
+ alerts.resetThresholds();
290
+ alerts.clearAllAlerts();
291
+ alerts.resetRateLimit();
292
+ });
293
+
294
+ describe('shouldRateLimitAlert', () => {
295
+ test('should allow alerts when rate limiting is disabled', () => {
296
+ alerts.setRateLimit({ enabled: false });
297
+
298
+ // Should never rate limit when disabled
299
+ for (let i = 0; i < 10; i++) {
300
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
301
+ }
302
+ });
303
+
304
+ test('should allow first few alerts within limit', () => {
305
+ alerts.setRateLimit({ enabled: true, maxAlerts: 3, windowMs: 60000 });
306
+
307
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
308
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
309
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
310
+ });
311
+
312
+ test('should rate limit alerts exceeding maxAlerts', () => {
313
+ alerts.setRateLimit({ enabled: true, maxAlerts: 2, windowMs: 60000 });
314
+
315
+ // First two should pass (check + record)
316
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
317
+ alerts.recordAlertTimestamp('cpu');
318
+
319
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
320
+ alerts.recordAlertTimestamp('cpu');
321
+
322
+ // Third should be rate limited
323
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(true);
324
+ });
325
+
326
+ test('should track rate limits separately per alert type', () => {
327
+ alerts.setRateLimit({ enabled: true, maxAlerts: 1, windowMs: 60000 });
328
+
329
+ // cpu should be limited after one
330
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
331
+ alerts.recordAlertTimestamp('cpu');
332
+
333
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(true);
334
+
335
+ // memory should still be allowed
336
+ expect(alerts.shouldRateLimitAlert('memory')).toBe(false);
337
+ });
338
+
339
+ test('should respect custom windowMs', async () => {
340
+ alerts.setRateLimit({ enabled: true, maxAlerts: 1, windowMs: 50 });
341
+
342
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
343
+ alerts.recordAlertTimestamp('cpu');
344
+
345
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(true);
346
+
347
+ // Wait for window to expire
348
+ await new Promise(resolve => setTimeout(resolve, 60));
349
+
350
+ // Should be allowed again after window expires
351
+ expect(alerts.shouldRateLimitAlert('cpu')).toBe(false);
352
+ });
353
+ });
354
+
355
+ describe('checkThreshold with rate limiting', () => {
356
+ test('should suppress warning alerts when rate limited', () => {
357
+ alerts.setRateLimit({ enabled: true, maxAlerts: 1, windowMs: 60000 });
358
+
359
+ // First alert goes through
360
+ const alert1 = alerts.checkThreshold('cpu', 75);
361
+ expect(alert1).not.toBeNull();
362
+
363
+ // Second warning should be suppressed
364
+ const alert2 = alerts.checkThreshold('cpu', 80);
365
+ expect(alert2).toBeNull();
366
+ });
367
+
368
+ test('should always allow critical alerts even when rate limited', () => {
369
+ alerts.setRateLimit({ enabled: true, maxAlerts: 1, windowMs: 60000 });
370
+
371
+ // First alert (warning) goes through
372
+ const alert1 = alerts.checkThreshold('cpu', 75);
373
+ expect(alert1).not.toBeNull();
374
+
375
+ // Critical should always go through
376
+ const alert2 = alerts.checkThreshold('cpu', 95);
377
+ expect(alert2).not.toBeNull();
378
+ expect(alert2.level).toBe(alerts.AlertLevel.CRITICAL);
379
+ });
380
+
381
+ test('should always allow cleared alerts', () => {
382
+ alerts.setRateLimit({ enabled: true, maxAlerts: 1, windowMs: 60000 });
383
+
384
+ // Create an alert
385
+ alerts.checkThreshold('cpu', 85);
386
+
387
+ // Clear the alert - should always go through
388
+ const cleared = alerts.checkThreshold('cpu', 50);
389
+ expect(cleared).not.toBeNull();
390
+ expect(cleared.level).toBe(alerts.AlertLevel.CLEARED);
391
+ });
392
+ });
393
+
394
+ describe('getRateLimit and setRateLimit', () => {
395
+ test('should return current rate limit config', () => {
396
+ const config = alerts.getRateLimit();
397
+
398
+ expect(config.enabled).toBe(true);
399
+ expect(config.windowMs).toBe(60000);
400
+ expect(config.maxAlerts).toBe(5);
401
+ });
402
+
403
+ test('should update rate limit config', () => {
404
+ alerts.setRateLimit({ enabled: false, maxAlerts: 10 });
405
+
406
+ const config = alerts.getRateLimit();
407
+ expect(config.enabled).toBe(false);
408
+ expect(config.maxAlerts).toBe(10);
409
+ });
410
+
411
+ test('should preserve unspecified values when setting config', () => {
412
+ alerts.setRateLimit({ maxAlerts: 3 });
413
+
414
+ const config = alerts.getRateLimit();
415
+ expect(config.maxAlerts).toBe(3);
416
+ expect(config.enabled).toBe(true); // preserved
417
+ expect(config.windowMs).toBe(60000); // preserved
418
+ });
419
+ });
420
+
421
+ describe('resetRateLimit', () => {
422
+ test('should reset rate limit state', () => {
423
+ alerts.setRateLimit({ enabled: true, maxAlerts: 1, windowMs: 60000 });
424
+
425
+ // Trigger rate limit
426
+ alerts.shouldRateLimitAlert('cpu');
427
+ alerts.shouldRateLimitAlert('cpu');
428
+
429
+ // Reset
430
+ alerts.resetRateLimit();
431
+
432
+ const config = alerts.getRateLimit();
433
+ expect(config.enabled).toBe(true);
434
+ expect(config.maxAlerts).toBe(5); // back to default
435
+ });
436
+ });
437
+ });