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.
- 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 +5236 -566
- 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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Performance Monitor module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest } from '@jest/globals';
|
|
6
|
+
import { PerformanceMonitor } from '../src/performance-monitor.js';
|
|
7
|
+
|
|
8
|
+
describe('PerformanceMonitor', () => {
|
|
9
|
+
let monitor;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
monitor = new PerformanceMonitor();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
monitor.stop();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('start/stop', () => {
|
|
20
|
+
test('starts monitoring when start() is called', () => {
|
|
21
|
+
expect(monitor.isTracking).toBe(false);
|
|
22
|
+
monitor.start();
|
|
23
|
+
expect(monitor.isTracking).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('stops monitoring when stop() is called', () => {
|
|
27
|
+
monitor.start();
|
|
28
|
+
expect(monitor.isTracking).toBe(true);
|
|
29
|
+
monitor.stop();
|
|
30
|
+
expect(monitor.isTracking).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('record', () => {
|
|
35
|
+
test('returns null when not tracking', async () => {
|
|
36
|
+
const result = await monitor.record();
|
|
37
|
+
expect(result).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('records metrics when tracking', async () => {
|
|
41
|
+
monitor.start();
|
|
42
|
+
const result = await monitor.record(2000);
|
|
43
|
+
|
|
44
|
+
expect(result).toBeDefined();
|
|
45
|
+
expect(result.timestamp).toBeGreaterThan(0);
|
|
46
|
+
expect(result.refreshRate).toBe(2000);
|
|
47
|
+
expect(typeof result.memoryUsed).toBe('number');
|
|
48
|
+
expect(typeof result.memoryPercent).toBe('number');
|
|
49
|
+
expect(typeof result.cpuPercent).toBe('number');
|
|
50
|
+
expect(typeof result.uptime).toBe('number');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('maintains history within max limit', () => {
|
|
54
|
+
monitor.start();
|
|
55
|
+
monitor.maxHistory = 5;
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < 10; i++) {
|
|
58
|
+
monitor.record(2000);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
expect(monitor.history.length).toBeLessThanOrEqual(5);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('getMetrics', () => {
|
|
66
|
+
test('returns current metrics', () => {
|
|
67
|
+
monitor.start();
|
|
68
|
+
monitor.record(2000);
|
|
69
|
+
|
|
70
|
+
const metrics = monitor.getMetrics();
|
|
71
|
+
expect(metrics.current).toBeDefined();
|
|
72
|
+
expect(metrics.history).toBeDefined();
|
|
73
|
+
expect(metrics.aggregates).toBeDefined();
|
|
74
|
+
expect(metrics.isTracking).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('returns empty aggregates when no history', () => {
|
|
78
|
+
const metrics = monitor.getMetrics();
|
|
79
|
+
expect(metrics.aggregates.avgMemoryUsed).toBe(0);
|
|
80
|
+
expect(metrics.aggregates.peakMemoryUsed).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('getStatusString', () => {
|
|
85
|
+
test('returns inactive message when no history', () => {
|
|
86
|
+
const status = monitor.getStatusString();
|
|
87
|
+
expect(status).toBe('Performance monitoring inactive');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('returns formatted status when tracking', async () => {
|
|
91
|
+
monitor.start();
|
|
92
|
+
await monitor.record(2000);
|
|
93
|
+
|
|
94
|
+
const status = monitor.getStatusString();
|
|
95
|
+
expect(status).toContain('MEM:');
|
|
96
|
+
expect(status).toContain('MB');
|
|
97
|
+
expect(status).toContain('CPU:');
|
|
98
|
+
expect(status).toContain('Refresh:');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('includes detailed metrics when requested', async () => {
|
|
102
|
+
monitor.start();
|
|
103
|
+
await monitor.record(2000);
|
|
104
|
+
|
|
105
|
+
const status = monitor.getStatusString(true);
|
|
106
|
+
expect(status).toContain('MEM:');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('getMemorySparkline', () => {
|
|
111
|
+
test('returns data array', () => {
|
|
112
|
+
monitor.start();
|
|
113
|
+
monitor.record(2000);
|
|
114
|
+
monitor.record(2000);
|
|
115
|
+
|
|
116
|
+
const data = monitor.getMemorySparkline();
|
|
117
|
+
expect(Array.isArray(data)).toBe(true);
|
|
118
|
+
expect(data.length).toBeGreaterThan(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('returns array with zero when no history', () => {
|
|
122
|
+
const data = monitor.getMemorySparkline();
|
|
123
|
+
expect(data).toEqual([0]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('getCpuSparkline', () => {
|
|
128
|
+
test('returns data array', () => {
|
|
129
|
+
monitor.start();
|
|
130
|
+
monitor.record(2000);
|
|
131
|
+
|
|
132
|
+
const data = monitor.getCpuSparkline();
|
|
133
|
+
expect(Array.isArray(data)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('checkHealth', () => {
|
|
138
|
+
test('returns not degraded for normal metrics', async () => {
|
|
139
|
+
monitor.start();
|
|
140
|
+
await monitor.record(2000);
|
|
141
|
+
|
|
142
|
+
const health = monitor.checkHealth();
|
|
143
|
+
expect(health.degraded).toBe(false);
|
|
144
|
+
expect(health.reasons).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('returns degraded for high memory usage', () => {
|
|
148
|
+
monitor.start();
|
|
149
|
+
// Mock high memory usage
|
|
150
|
+
monitor.history.push({
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
memoryPercent: 90,
|
|
153
|
+
cpuPercent: 10,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const health = monitor.checkHealth();
|
|
157
|
+
expect(health.degraded).toBe(true);
|
|
158
|
+
expect(health.reasons.length).toBeGreaterThan(0);
|
|
159
|
+
expect(health.reasons[0]).toContain('memory');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('returns degraded for high CPU usage', () => {
|
|
163
|
+
monitor.start();
|
|
164
|
+
monitor.history.push({
|
|
165
|
+
timestamp: Date.now(),
|
|
166
|
+
memoryPercent: 50,
|
|
167
|
+
cpuPercent: 85,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const health = monitor.checkHealth();
|
|
171
|
+
expect(health.degraded).toBe(true);
|
|
172
|
+
expect(health.reasons.some(r => r.includes('CPU'))).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('reset', () => {
|
|
177
|
+
test('clears history and resets metrics', async () => {
|
|
178
|
+
monitor.start();
|
|
179
|
+
await monitor.record(2000);
|
|
180
|
+
await monitor.record(2000);
|
|
181
|
+
|
|
182
|
+
expect(monitor.history.length).toBeGreaterThan(0);
|
|
183
|
+
|
|
184
|
+
monitor.reset();
|
|
185
|
+
|
|
186
|
+
expect(monitor.history.length).toBe(0);
|
|
187
|
+
expect(monitor.metrics.avgMemoryUsed).toBe(0);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Plugin API rate limiting
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest } from '@jest/globals';
|
|
6
|
+
import { PluginAPI, BaseWidget } from '../src/widgets/plugin-api.js';
|
|
7
|
+
|
|
8
|
+
describe('PluginAPI Rate Limiting', () => {
|
|
9
|
+
let api;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
api = new PluginAPI({
|
|
13
|
+
rateLimit: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
windowMs: 60000,
|
|
16
|
+
maxCalls: 5,
|
|
17
|
+
alwaysAllowCritical: false
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
api.rateLimiter.reset();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('Constructor', () => {
|
|
27
|
+
test('should create API with default rate limit config', () => {
|
|
28
|
+
const defaultApi = new PluginAPI();
|
|
29
|
+
const status = defaultApi.getRateLimitStatus();
|
|
30
|
+
|
|
31
|
+
expect(status.enabled).toBe(true);
|
|
32
|
+
expect(status.windowMs).toBe(60000);
|
|
33
|
+
expect(status.maxAlerts).toBe(100);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should create API with custom rate limit config', () => {
|
|
37
|
+
const customApi = new PluginAPI({
|
|
38
|
+
rateLimit: {
|
|
39
|
+
enabled: true,
|
|
40
|
+
windowMs: 30000,
|
|
41
|
+
maxCalls: 10,
|
|
42
|
+
alwaysAllowCritical: true
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const status = customApi.getRateLimitStatus();
|
|
46
|
+
|
|
47
|
+
expect(status.enabled).toBe(true);
|
|
48
|
+
expect(status.windowMs).toBe(30000);
|
|
49
|
+
expect(status.maxAlerts).toBe(10);
|
|
50
|
+
expect(status.alwaysAllowCritical).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should allow disabling rate limiting', () => {
|
|
54
|
+
const unlimitedApi = new PluginAPI({
|
|
55
|
+
rateLimit: { enabled: false }
|
|
56
|
+
});
|
|
57
|
+
const status = unlimitedApi.getRateLimitStatus();
|
|
58
|
+
|
|
59
|
+
expect(status.enabled).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('getData rate limiting', () => {
|
|
64
|
+
test('should allow calls under limit', async () => {
|
|
65
|
+
api.registerDataProvider('test', async () => ({ data: 'test' }));
|
|
66
|
+
|
|
67
|
+
const result = await api.getData('test');
|
|
68
|
+
expect(result).toEqual({ data: 'test' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should block calls over limit', async () => {
|
|
72
|
+
api.registerDataProvider('test', async () => ({ data: 'test' }));
|
|
73
|
+
|
|
74
|
+
// Make maxCalls calls
|
|
75
|
+
for (let i = 0; i < 5; i++) {
|
|
76
|
+
await api.getData('test');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Next call should be rate limited
|
|
80
|
+
await expect(api.getData('test')).rejects.toThrow('Rate limit exceeded');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should include retryAfter in rate limit error', async () => {
|
|
84
|
+
api.registerDataProvider('test', async () => ({ data: 'test' }));
|
|
85
|
+
|
|
86
|
+
// Exhaust limit
|
|
87
|
+
for (let i = 0; i < 5; i++) {
|
|
88
|
+
await api.getData('test');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await api.getData('test');
|
|
93
|
+
fail('Should have thrown');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
expect(err.code).toBe('RATE_LIMIT_EXCEEDED');
|
|
96
|
+
expect(err.retryAfter).toBeDefined();
|
|
97
|
+
expect(err.retryAfter).toBeGreaterThan(0);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should not rate limit when disabled', async () => {
|
|
102
|
+
const unlimitedApi = new PluginAPI({
|
|
103
|
+
rateLimit: { enabled: false, maxCalls: 1 }
|
|
104
|
+
});
|
|
105
|
+
unlimitedApi.registerDataProvider('test', async () => ({ data: 'test' }));
|
|
106
|
+
|
|
107
|
+
// Should allow unlimited calls
|
|
108
|
+
for (let i = 0; i < 10; i++) {
|
|
109
|
+
const result = await unlimitedApi.getData('test');
|
|
110
|
+
expect(result).toEqual({ data: 'test' });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should track getData category separately', async () => {
|
|
115
|
+
api.registerDataProvider('test', async () => ({ data: 'test' }));
|
|
116
|
+
|
|
117
|
+
// Make calls
|
|
118
|
+
await api.getData('test');
|
|
119
|
+
|
|
120
|
+
const status = api.getRateLimitStatus();
|
|
121
|
+
expect(status.types.getData).toBeDefined();
|
|
122
|
+
expect(status.types.getData.current).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('executeExtension rate limiting', () => {
|
|
127
|
+
test('should allow extension calls under limit', async () => {
|
|
128
|
+
api.registerExtensionPoint('test-extension', { multiple: true });
|
|
129
|
+
api.extend('test-extension', async () => 'result');
|
|
130
|
+
|
|
131
|
+
const results = await api.executeExtension('test-extension');
|
|
132
|
+
expect(results).toHaveLength(1);
|
|
133
|
+
expect(results[0].result).toBe('result');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('should block extension calls over limit', async () => {
|
|
137
|
+
api.registerExtensionPoint('test-extension', { multiple: true });
|
|
138
|
+
api.extend('test-extension', async () => 'result');
|
|
139
|
+
|
|
140
|
+
// Make maxCalls calls
|
|
141
|
+
for (let i = 0; i < 5; i++) {
|
|
142
|
+
await api.executeExtension('test-extension');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Next call should be rate limited
|
|
146
|
+
await expect(api.executeExtension('test-extension')).rejects.toThrow('Rate limit exceeded');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('should track executeExtension category separately', async () => {
|
|
150
|
+
api.registerExtensionPoint('test-extension', { multiple: true });
|
|
151
|
+
api.extend('test-extension', async () => 'result');
|
|
152
|
+
|
|
153
|
+
await api.executeExtension('test-extension');
|
|
154
|
+
|
|
155
|
+
const status = api.getRateLimitStatus();
|
|
156
|
+
expect(status.types.executeExtension).toBeDefined();
|
|
157
|
+
expect(status.types.executeExtension.current).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('getMetrics rate limiting', () => {
|
|
162
|
+
test('should allow metrics calls under limit', async () => {
|
|
163
|
+
const dataProvider = jest.fn().mockResolvedValue({ cpu: 50 });
|
|
164
|
+
const metricsApi = new PluginAPI({
|
|
165
|
+
dataProvider,
|
|
166
|
+
rateLimit: { enabled: true, maxCalls: 5 }
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = await metricsApi.getMetrics('cpu');
|
|
170
|
+
expect(result).toEqual({ cpu: 50 });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should block metrics calls over limit', async () => {
|
|
174
|
+
const dataProvider = jest.fn().mockResolvedValue({ cpu: 50 });
|
|
175
|
+
const metricsApi = new PluginAPI({
|
|
176
|
+
dataProvider,
|
|
177
|
+
rateLimit: { enabled: true, maxCalls: 5 }
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Make maxCalls calls
|
|
181
|
+
for (let i = 0; i < 5; i++) {
|
|
182
|
+
await metricsApi.getMetrics('cpu');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Next call should be rate limited
|
|
186
|
+
await expect(metricsApi.getMetrics('cpu')).rejects.toThrow('Rate limit exceeded');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('should track getMetrics category separately', async () => {
|
|
190
|
+
const dataProvider = jest.fn().mockResolvedValue({ cpu: 50 });
|
|
191
|
+
const metricsApi = new PluginAPI({
|
|
192
|
+
dataProvider,
|
|
193
|
+
rateLimit: { enabled: true, maxCalls: 5 }
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await metricsApi.getMetrics('cpu');
|
|
197
|
+
|
|
198
|
+
const status = metricsApi.getRateLimitStatus();
|
|
199
|
+
expect(status.types.getMetrics).toBeDefined();
|
|
200
|
+
expect(status.types.getMetrics.current).toBe(1);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('Rate limit configuration', () => {
|
|
205
|
+
test('should allow runtime configuration', () => {
|
|
206
|
+
api.configureRateLimit({ maxCalls: 20 });
|
|
207
|
+
|
|
208
|
+
const status = api.getRateLimitStatus();
|
|
209
|
+
expect(status.maxAlerts).toBe(20);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should allow disabling at runtime', () => {
|
|
213
|
+
api.configureRateLimit({ enabled: false });
|
|
214
|
+
|
|
215
|
+
const status = api.getRateLimitStatus();
|
|
216
|
+
expect(status.enabled).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('should get rate limiter instance for custom use', () => {
|
|
220
|
+
const limiter = api.getRateLimiter();
|
|
221
|
+
|
|
222
|
+
expect(limiter).toBeDefined();
|
|
223
|
+
expect(typeof limiter.check).toBe('function');
|
|
224
|
+
expect(typeof limiter.checkAndRecord).toBe('function');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('Independent category tracking', () => {
|
|
229
|
+
test('should track getData and executeExtension independently', async () => {
|
|
230
|
+
api.registerDataProvider('test', async () => ({ data: 'test' }));
|
|
231
|
+
api.registerExtensionPoint('test-ext', { multiple: true });
|
|
232
|
+
api.extend('test-ext', async () => 'result');
|
|
233
|
+
|
|
234
|
+
// Fill up getData limit
|
|
235
|
+
for (let i = 0; i < 5; i++) {
|
|
236
|
+
await api.getData('test');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// executeExtension should still be allowed
|
|
240
|
+
const results = await api.executeExtension('test-ext');
|
|
241
|
+
expect(results).toHaveLength(1);
|
|
242
|
+
|
|
243
|
+
const status = api.getRateLimitStatus();
|
|
244
|
+
expect(status.types.getData.current).toBe(5);
|
|
245
|
+
expect(status.types.executeExtension.current).toBe(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('should track getMetrics independently from getData', async () => {
|
|
249
|
+
const dataProvider = jest.fn().mockResolvedValue({ cpu: 50 });
|
|
250
|
+
const metricsApi = new PluginAPI({
|
|
251
|
+
dataProvider,
|
|
252
|
+
rateLimit: { enabled: true, maxCalls: 5 }
|
|
253
|
+
});
|
|
254
|
+
metricsApi.registerDataProvider('test', async () => ({ data: 'test' }));
|
|
255
|
+
|
|
256
|
+
// Fill up getMetrics limit
|
|
257
|
+
for (let i = 0; i < 5; i++) {
|
|
258
|
+
await metricsApi.getMetrics('cpu');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// getData should still be allowed
|
|
262
|
+
await metricsApi.getData('test');
|
|
263
|
+
|
|
264
|
+
const status = metricsApi.getRateLimitStatus();
|
|
265
|
+
expect(status.types.getMetrics.current).toBe(5);
|
|
266
|
+
expect(status.types.getData.current).toBe(1);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('Integration with BaseWidget', () => {
|
|
271
|
+
test('should provide rate limiter to widgets via API', async () => {
|
|
272
|
+
class TestWidget extends BaseWidget {
|
|
273
|
+
constructor(options) {
|
|
274
|
+
super(options);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const widget = new TestWidget({ api });
|
|
279
|
+
const limiter = widget.api.getRateLimiter();
|
|
280
|
+
|
|
281
|
+
expect(limiter).toBeDefined();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('widgets can configure rate limits', async () => {
|
|
285
|
+
class TestWidget extends BaseWidget {
|
|
286
|
+
constructor(options) {
|
|
287
|
+
super(options);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
configureApiRateLimit() {
|
|
291
|
+
this.api.configureRateLimit({ maxCalls: 50 });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const widget = new TestWidget({ api });
|
|
296
|
+
widget.configureApiRateLimit();
|
|
297
|
+
|
|
298
|
+
const status = widget.api.getRateLimitStatus();
|
|
299
|
+
expect(status.maxAlerts).toBe(50);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|