claw-dashboard 1.8.4 → 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.
- package/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5331 -512
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -5
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1941 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1057 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- 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
|
+
});
|