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,1321 @@
1
+ /**
2
+ * Integration Tests for Claw Dashboard
3
+ * Tests end-to-end workflows covering multiple module interactions
4
+ */
5
+
6
+ import { jest } from '@jest/globals';
7
+
8
+ // Mock systeminformation to avoid system-dependent behavior
9
+ const mockCurrentLoad = jest.fn();
10
+ const mockMem = jest.fn();
11
+ const mockGraphics = jest.fn();
12
+ const mockNetworkStats = jest.fn();
13
+ const mockFsSize = jest.fn();
14
+ const mockOsInfo = jest.fn();
15
+ const mockVersions = jest.fn();
16
+ const mockTime = jest.fn();
17
+ const mockProcesses = jest.fn();
18
+
19
+ jest.unstable_mockModule('systeminformation', () => ({
20
+ currentLoad: mockCurrentLoad,
21
+ mem: mockMem,
22
+ graphics: mockGraphics,
23
+ networkStats: mockNetworkStats,
24
+ fsSize: mockFsSize,
25
+ osInfo: mockOsInfo,
26
+ versions: mockVersions,
27
+ time: mockTime,
28
+ processes: mockProcesses,
29
+ }));
30
+
31
+ // Import modules after mocking
32
+ import alerts from '../src/alerts.js';
33
+ import performanceMonitor from '../src/performance-monitor.js';
34
+ import cache from '../src/cache.js';
35
+ import retry from '../src/retry.js';
36
+ import config from '../src/config.js';
37
+ import logger from '../src/logger.js';
38
+
39
+ // Default mock responses
40
+ const defaultCpuData = {
41
+ currentLoad: 25.5,
42
+ currentLoadUser: 20.0,
43
+ currentLoadSystem: 5.5,
44
+ cpus: [{ load: 25.5 }],
45
+ };
46
+
47
+ const defaultMemData = {
48
+ total: 8589934592, // 8GB
49
+ active: 4294967296, // 4GB
50
+ available: 4294967296,
51
+ used: 3221225472, // 3GB
52
+ free: 5368709120,
53
+ percent: 37.5,
54
+ };
55
+
56
+ const defaultOsData = {
57
+ platform: 'darwin',
58
+ distro: 'macOS',
59
+ release: '14.0',
60
+ codename: 'Sonoma',
61
+ hostname: 'test-host',
62
+ arch: 'arm64',
63
+ };
64
+
65
+ describe('Integration: Alert + Performance Monitor Workflow', () => {
66
+ beforeEach(() => {
67
+ // Reset all module states
68
+ alerts.resetThresholds();
69
+ alerts.clearAllAlerts();
70
+ alerts.resetRateLimit();
71
+ performanceMonitor.reset();
72
+ performanceMonitor.stop();
73
+
74
+ // Setup systeminformation mocks
75
+ mockCurrentLoad.mockReset().mockResolvedValue(defaultCpuData);
76
+ mockMem.mockReset().mockResolvedValue(defaultMemData);
77
+ mockGraphics.mockReset().mockResolvedValue({ controllers: [] });
78
+ mockNetworkStats.mockReset().mockResolvedValue([]);
79
+ mockFsSize.mockReset().mockResolvedValue([]);
80
+ mockOsInfo.mockReset().mockResolvedValue(defaultOsData);
81
+ mockVersions.mockReset().mockResolvedValue({ node: '20.0.0', npm: '10.0.0' });
82
+ mockTime.mockReset().mockResolvedValue({ uptime: 3600 });
83
+ mockProcesses.mockReset().mockResolvedValue({ list: [] });
84
+ });
85
+
86
+ afterEach(() => {
87
+ performanceMonitor.stop();
88
+ });
89
+
90
+ test('performance monitoring triggers alerts when thresholds exceeded', async () => {
91
+ // Start performance monitoring
92
+ performanceMonitor.start();
93
+
94
+ // Simulate recording high CPU usage over multiple intervals
95
+ // We can't directly inject values, but we can verify the integration points
96
+
97
+ // Verify performance monitor is tracking
98
+ expect(performanceMonitor.isTracking).toBe(true);
99
+
100
+ // Record some metrics (will use actual system values)
101
+ const snapshot1 = await performanceMonitor.record(2000);
102
+ expect(snapshot1).toBeDefined();
103
+ expect(snapshot1.cpuPercent).toBeDefined();
104
+ expect(snapshot1.memoryPercent).toBeDefined();
105
+
106
+ // Check if alerts would be triggered based on recorded values
107
+ const alertMetrics = {
108
+ cpu: snapshot1.cpuPercent,
109
+ memory: snapshot1.memoryPercent,
110
+ };
111
+
112
+ const newAlerts = alerts.checkAllMetrics(alertMetrics);
113
+
114
+ // Alerts are created only if thresholds are exceeded
115
+ // This verifies the integration between performance monitoring and alerts
116
+ if (snapshot1.cpuPercent >= 70 || snapshot1.memoryPercent >= 75) {
117
+ expect(newAlerts.length).toBeGreaterThan(0);
118
+ }
119
+ });
120
+
121
+ test('performance health check integrates with alert system', () => {
122
+ performanceMonitor.start();
123
+
124
+ // Record multiple snapshots to build history
125
+ for (let i = 0; i < 5; i++) {
126
+ performanceMonitor.record(2000);
127
+ }
128
+
129
+ // Get health status
130
+ const health = performanceMonitor.checkHealth();
131
+
132
+ // Verify health check returns expected structure
133
+ expect(health).toHaveProperty('degraded');
134
+ expect(health).toHaveProperty('reasons');
135
+ expect(Array.isArray(health.reasons)).toBe(true);
136
+
137
+ // If degraded, verify alerts would be triggered
138
+ if (health.degraded) {
139
+ health.reasons.forEach(reason => {
140
+ // Each degradation reason should correspond to an alert type
141
+ expect(reason).toMatch(/(memory|CPU|event loop)/i);
142
+ });
143
+ }
144
+ });
145
+
146
+ test('metrics history flows correctly through multiple records', async () => {
147
+ performanceMonitor.start();
148
+ performanceMonitor.maxHistory = 10;
149
+
150
+ // Record multiple snapshots
151
+ const snapshots = [];
152
+ for (let i = 0; i < 15; i++) {
153
+ const snapshot = await performanceMonitor.record(2000);
154
+ snapshots.push(snapshot);
155
+ }
156
+
157
+ // Verify history is capped at maxHistory
158
+ expect(performanceMonitor.history.length).toBeLessThanOrEqual(10);
159
+
160
+ // Verify aggregates are calculated
161
+ const metrics = performanceMonitor.getMetrics();
162
+ expect(metrics.aggregates.avgMemoryUsed).toBeGreaterThan(0);
163
+ expect(metrics.aggregates.peakMemoryUsed).toBeGreaterThanOrEqual(metrics.aggregates.avgMemoryUsed);
164
+
165
+ // Verify sparkline data is available
166
+ const memorySparkline = performanceMonitor.getMemorySparkline();
167
+ const cpuSparkline = performanceMonitor.getCpuSparkline();
168
+ expect(Array.isArray(memorySparkline)).toBe(true);
169
+ expect(Array.isArray(cpuSparkline)).toBe(true);
170
+ });
171
+ });
172
+
173
+ describe('Integration: Cache + Retry Workflow', () => {
174
+ beforeEach(() => {
175
+ cache.clear();
176
+ });
177
+
178
+ test('cached data avoids retry overhead', async () => {
179
+ const cacheKey = 'test-integration-key';
180
+ const cacheValue = { data: 'test-value', timestamp: Date.now() };
181
+
182
+ // Set cache with long TTL
183
+ cache.set(cacheKey, cacheValue, 60000);
184
+
185
+ // Verify cache hit
186
+ const cached = cache.get(cacheKey);
187
+ expect(cached).toEqual(cacheValue);
188
+
189
+ // Cache hit should be immediate (no retry needed)
190
+ const startTime = Date.now();
191
+ cache.get(cacheKey);
192
+ const elapsed = Date.now() - startTime;
193
+ expect(elapsed).toBeLessThan(10); // Should be nearly instant
194
+ });
195
+
196
+ test('cache miss triggers fresh fetch with retry support', async () => {
197
+ const cacheKey = 'test-miss-key';
198
+
199
+ // Ensure cache miss by clearing
200
+ cache.clear();
201
+ const cached = cache.get(cacheKey);
202
+ expect(cached).toBeNull();
203
+
204
+ // Verify retry module is available for fallback
205
+ expect(retry.withRetry).toBeDefined();
206
+ });
207
+
208
+ test('cache TTL expiration works correctly', async () => {
209
+ const cacheKey = 'test-ttl-key';
210
+ const shortTTL = 50; // 50ms for fast test
211
+
212
+ cache.set(cacheKey, { value: 'fresh' }, shortTTL);
213
+
214
+ // Should be cached initially
215
+ expect(cache.get(cacheKey)).toBeDefined();
216
+
217
+ // Wait for expiration
218
+ await new Promise(resolve => setTimeout(resolve, shortTTL + 10));
219
+
220
+ // Should be expired now
221
+ expect(cache.get(cacheKey)).toBeNull();
222
+ });
223
+
224
+ test('cache debounce prevents duplicate fetches', async () => {
225
+ let fetchCount = 0;
226
+ let lastResult = null;
227
+
228
+ const fetchFn = async () => {
229
+ fetchCount++;
230
+ lastResult = { data: 'fetched', count: fetchCount };
231
+ return lastResult;
232
+ };
233
+
234
+ const debouncedFetch = cache.debounce(fetchFn, 50);
235
+
236
+ // Call multiple times rapidly
237
+ debouncedFetch();
238
+ debouncedFetch();
239
+ debouncedFetch();
240
+
241
+ // Wait for debounce to complete
242
+ await new Promise(resolve => setTimeout(resolve, 100));
243
+
244
+ // Should only fetch once due to debouncing
245
+ expect(fetchCount).toBe(1);
246
+ expect(lastResult).toBeDefined();
247
+ });
248
+
249
+ test('cache throttle limits fetch frequency', async () => {
250
+ let fetchCount = 0;
251
+ const results = [];
252
+
253
+ const fetchFn = async () => {
254
+ fetchCount++;
255
+ const result = { data: 'fetched', count: fetchCount };
256
+ results.push(result);
257
+ return result;
258
+ };
259
+
260
+ const throttledFetch = cache.throttle(fetchFn, 100);
261
+
262
+ // Call multiple times
263
+ await throttledFetch();
264
+ await throttledFetch();
265
+ await throttledFetch();
266
+
267
+ // Should only execute once within throttle window
268
+ expect(fetchCount).toBe(1);
269
+ });
270
+ });
271
+
272
+ describe('Integration: Retry + Logger Workflow', () => {
273
+ test('retry attempts are logged appropriately', async () => {
274
+ let attemptCount = 0;
275
+ const maxRetries = 2;
276
+
277
+ const failingFn = async () => {
278
+ attemptCount++;
279
+ if (attemptCount <= maxRetries) {
280
+ throw new Error('Network timeout'); // Matches retryable pattern
281
+ }
282
+ return 'success';
283
+ };
284
+
285
+ const retryOptions = {
286
+ maxRetries,
287
+ initialDelay: 10,
288
+ maxDelay: 50,
289
+ backoffMultiplier: 1,
290
+ };
291
+
292
+ // withRetry returns a wrapped function
293
+ const wrappedFn = retry.withRetry(failingFn, retryOptions);
294
+ const result = await wrappedFn();
295
+
296
+ // Should succeed after retries
297
+ expect(result).toBe('success');
298
+ // Initial attempt + retries = maxRetries + 1
299
+ expect(attemptCount).toBeGreaterThanOrEqual(1);
300
+ });
301
+
302
+ test('retry gives up after max attempts and logs error', async () => {
303
+ const maxRetries = 2;
304
+ let attemptCount = 0;
305
+
306
+ const alwaysFailingFn = async () => {
307
+ attemptCount++;
308
+ throw new Error('Network error'); // Matches retryable pattern
309
+ };
310
+
311
+ const retryOptions = {
312
+ maxRetries,
313
+ initialDelay: 10,
314
+ maxDelay: 50,
315
+ backoffMultiplier: 1,
316
+ };
317
+
318
+ const wrappedFn = retry.withRetry(alwaysFailingFn, retryOptions);
319
+
320
+ await expect(wrappedFn()).rejects.toThrow('Network error');
321
+
322
+ // Should attempt maxRetries + 1 times
323
+ expect(attemptCount).toBe(maxRetries + 1);
324
+ });
325
+
326
+ test('retry respects retryable errors', async () => {
327
+ let attemptCount = 0;
328
+
329
+ const networkErrorFn = async () => {
330
+ attemptCount++;
331
+ const error = new Error('Connection refused');
332
+ error.code = 'ECONNREFUSED';
333
+ throw error;
334
+ };
335
+
336
+ const retryOptions = {
337
+ maxRetries: 2,
338
+ initialDelay: 10,
339
+ maxDelay: 50,
340
+ backoffMultiplier: 1,
341
+ retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT'],
342
+ };
343
+
344
+ const wrappedFn = retry.withRetry(networkErrorFn, retryOptions);
345
+
346
+ await expect(wrappedFn()).rejects.toThrow('Connection refused');
347
+
348
+ // Should retry because ECONNREFUSED is retryable (initial + 2 retries = 3 attempts)
349
+ expect(attemptCount).toBe(3);
350
+ });
351
+
352
+ test('retry does not retry non-retryable errors', async () => {
353
+ let attemptCount = 0;
354
+
355
+ const validationErrorFn = async () => {
356
+ attemptCount++;
357
+ throw new Error('Invalid input');
358
+ };
359
+
360
+ const retryOptions = {
361
+ maxRetries: 3,
362
+ initialDelay: 10,
363
+ retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT'],
364
+ };
365
+
366
+ const wrappedFn = retry.withRetry(validationErrorFn, retryOptions);
367
+
368
+ await expect(wrappedFn()).rejects.toThrow('Invalid input');
369
+
370
+ // Should not retry - only one attempt
371
+ expect(attemptCount).toBe(1);
372
+ });
373
+ });
374
+
375
+ describe('Integration: Config + Validation + Settings Workflow', () => {
376
+ test('default settings match config defaults', () => {
377
+ const defaultSettings = config.DEFAULT_SETTINGS;
378
+
379
+ // Verify key defaults match config
380
+ expect(defaultSettings.refreshInterval).toBe(config.REFRESH_INTERVALS.DEFAULT);
381
+ expect(defaultSettings.theme).toBe('auto');
382
+ expect(defaultSettings.exportFormat).toBe('json');
383
+
384
+ // Verify gateway endpoint defaults
385
+ expect(defaultSettings.gatewayEndpoints).toBeDefined();
386
+ expect(defaultSettings.gatewayEndpoints.length).toBeGreaterThan(0);
387
+ expect(defaultSettings.gatewayEndpoints[0].port).toBe(config.GATEWAY.DEFAULT_PORT);
388
+ });
389
+
390
+ test('alert thresholds match config', () => {
391
+ const currentThresholds = alerts.getThresholds();
392
+
393
+ expect(currentThresholds.cpu.warning).toBe(config.ALERT_THRESHOLDS.CPU.warning);
394
+ expect(currentThresholds.cpu.critical).toBe(config.ALERT_THRESHOLDS.CPU.critical);
395
+ expect(currentThresholds.memory.warning).toBe(config.ALERT_THRESHOLDS.MEMORY.warning);
396
+ expect(currentThresholds.disk.warning).toBe(config.ALERT_THRESHOLDS.DISK.warning);
397
+ });
398
+
399
+ test('rate limit config matches defaults', () => {
400
+ const rateLimit = alerts.getRateLimit();
401
+
402
+ expect(rateLimit.enabled).toBe(config.ALERT_RATE_LIMIT.ENABLED);
403
+ expect(rateLimit.windowMs).toBe(config.ALERT_RATE_LIMIT.WINDOW_MS);
404
+ expect(rateLimit.maxAlerts).toBe(config.ALERT_RATE_LIMIT.MAX_ALERTS);
405
+ });
406
+
407
+ test('cache TTL config is consistent', () => {
408
+ // Verify cache TTL constants are reasonable
409
+ expect(config.CACHE_TTL.CPU).toBeLessThan(config.CACHE_TTL.GPU); // GPU is expensive
410
+ expect(config.CACHE_TTL.DISK).toBeGreaterThan(config.CACHE_TTL.CPU); // Disk rarely changes
411
+ expect(config.CACHE_TTL.CONTAINER).toBeGreaterThan(config.CACHE_TTL.NETWORK);
412
+ });
413
+ });
414
+
415
+ describe('Integration: Full Dashboard Refresh Cycle Simulation', () => {
416
+ beforeEach(() => {
417
+ alerts.resetThresholds();
418
+ alerts.clearAllAlerts();
419
+ alerts.resetRateLimit();
420
+ performanceMonitor.reset();
421
+ performanceMonitor.stop();
422
+ cache.clear();
423
+ });
424
+
425
+ afterEach(() => {
426
+ performanceMonitor.stop();
427
+ });
428
+
429
+ test('simulates complete dashboard refresh cycle', async () => {
430
+ // Phase 1: Start monitoring
431
+ performanceMonitor.start();
432
+
433
+ // Phase 2: Simulate data fetch with caching
434
+ const systemData = {
435
+ cpu: 45,
436
+ memory: 60,
437
+ disk: 50,
438
+ };
439
+
440
+ // Cache the data
441
+ cache.set('system-metrics', systemData, config.CACHE_TTL.CPU);
442
+
443
+ // Phase 3: Check alerts
444
+ const newAlerts = alerts.checkAllMetrics(systemData);
445
+
446
+ // No alerts should be created for normal values
447
+ expect(newAlerts.length).toBe(0);
448
+
449
+ // Phase 4: Record performance metrics
450
+ const perfSnapshot = await performanceMonitor.record(2000);
451
+ expect(perfSnapshot).toBeDefined();
452
+
453
+ // Phase 5: Verify health
454
+ const health = performanceMonitor.checkHealth();
455
+ expect(health.degraded).toBe(false);
456
+ });
457
+
458
+ test('handles alert escalation workflow', () => {
459
+ performanceMonitor.start();
460
+
461
+ // Normal state
462
+ let newAlerts = alerts.checkAllMetrics({ cpu: 50, memory: 50, disk: 50 });
463
+ expect(newAlerts.length).toBe(0);
464
+
465
+ // Warning state
466
+ newAlerts = alerts.checkAllMetrics({ cpu: 75, memory: 50, disk: 50 });
467
+ expect(newAlerts.length).toBe(1);
468
+ expect(newAlerts[0].type).toBe('cpu');
469
+ expect(newAlerts[0].level).toBe(alerts.AlertLevel.WARNING);
470
+
471
+ // Critical escalation
472
+ newAlerts = alerts.checkAllMetrics({ cpu: 95, memory: 50, disk: 50 });
473
+ // Should update existing alert, not create new one
474
+ const activeAlerts = alerts.getActiveAlerts();
475
+ const cpuAlert = activeAlerts.find(a => a.type === 'cpu');
476
+ expect(cpuAlert.level).toBe(alerts.AlertLevel.CRITICAL);
477
+
478
+ // Recovery
479
+ newAlerts = alerts.checkAllMetrics({ cpu: 50, memory: 50, disk: 50 });
480
+ expect(newAlerts.length).toBe(1);
481
+ expect(newAlerts[0].level).toBe(alerts.AlertLevel.CLEARED);
482
+ });
483
+
484
+ test('handles multiple simultaneous alerts', () => {
485
+ // Trigger multiple alerts at once
486
+ const newAlerts = alerts.checkAllMetrics({
487
+ cpu: 95, // Critical
488
+ memory: 92, // Critical
489
+ disk: 96, // Critical
490
+ });
491
+
492
+ expect(newAlerts.length).toBe(3);
493
+ expect(newAlerts.map(a => a.type)).toEqual(expect.arrayContaining(['cpu', 'memory', 'disk']));
494
+
495
+ const activeAlerts = alerts.getActiveAlerts();
496
+ expect(activeAlerts.length).toBe(3);
497
+
498
+ // All should be critical
499
+ activeAlerts.forEach(alert => {
500
+ expect(alert.level).toBe(alerts.AlertLevel.CRITICAL);
501
+ });
502
+ });
503
+
504
+ test('respects alert rate limiting in workflow', () => {
505
+ // Setup: clear and reset first, then configure rate limiting
506
+ alerts.clearAllAlerts();
507
+ alerts.resetRateLimit();
508
+ alerts.setRateLimit({ enabled: true, maxAlerts: 2, windowMs: 1000 });
509
+
510
+ // First alert of type 'cpu' should be created and recorded
511
+ let alert1 = alerts.checkThreshold('cpu', 75);
512
+ expect(alert1).not.toBeNull();
513
+
514
+ // Dismiss it to allow new alert
515
+ alerts.dismissAlert(alert1.id);
516
+
517
+ // Second alert of same type should also be created
518
+ let alert2 = alerts.checkThreshold('cpu', 76);
519
+ expect(alert2).not.toBeNull();
520
+
521
+ // Dismiss again
522
+ alerts.dismissAlert(alert2.id);
523
+
524
+ // Third alert of same type - should be rate limited (max 2 per window per type)
525
+ const alert3 = alerts.checkThreshold('cpu', 77);
526
+ expect(alert3).toBeNull();
527
+
528
+ // Different alert type (memory) should still work (rate limit is per-type)
529
+ const memAlert = alerts.checkThreshold('memory', 80);
530
+ expect(memAlert).not.toBeNull();
531
+ });
532
+ });
533
+
534
+ describe('Integration: Error Handling Across Modules', () => {
535
+ test('handles graceful degradation when modules fail', async () => {
536
+ // Simulate performance monitor being inactive
537
+ performanceMonitor.stop();
538
+
539
+ // Should return null gracefully
540
+ const snapshot = await performanceMonitor.record();
541
+ expect(snapshot).toBeNull();
542
+
543
+ // Status should indicate inactive
544
+ const status = performanceMonitor.getStatusString();
545
+ expect(status).toContain('inactive');
546
+
547
+ // Alerts should still work independently
548
+ const alert = alerts.checkThreshold('cpu', 95);
549
+ expect(alert).not.toBeNull();
550
+ });
551
+
552
+ test('cache fallback works when fetch fails', async () => {
553
+ const cacheKey = 'fallback-test';
554
+ const cachedValue = { data: 'stale-but-available' };
555
+
556
+ // Pre-populate cache
557
+ cache.set(cacheKey, cachedValue, 100);
558
+
559
+ // Simulate fetch failure with retry
560
+ const failingFetch = async () => {
561
+ throw new Error('Network error');
562
+ };
563
+
564
+ const retryOptions = {
565
+ maxRetries: 1,
566
+ initialDelay: 10,
567
+ retryableErrors: ['Network error'],
568
+ };
569
+
570
+ // Retry should fail, but cache has fallback data
571
+ await expect(retry.withRetry(failingFetch, 'test', retryOptions))
572
+ .rejects.toThrow('Network error');
573
+
574
+ // Cache should still have data (if not expired)
575
+ // In real workflow, would use cached value as fallback
576
+ const stillCached = cache.get(cacheKey);
577
+ expect(stillCached).toBeDefined();
578
+ });
579
+
580
+ test('logger captures errors from all modules', () => {
581
+ // This test verifies logger integration
582
+ // Logger writes to file, so we verify it doesn't throw
583
+
584
+ expect(() => {
585
+ logger.error('Integration test error');
586
+ logger.warn('Integration test warning');
587
+ logger.info('Integration test info');
588
+ logger.debug('Integration test debug');
589
+ }).not.toThrow();
590
+ });
591
+ });
592
+
593
+ describe('Integration: Gateway Configuration Workflow', () => {
594
+ test('gateway config flows through settings', () => {
595
+ const defaultSettings = config.DEFAULT_SETTINGS;
596
+
597
+ // Verify gateway endpoint structure
598
+ const endpoint = defaultSettings.gatewayEndpoints[0];
599
+ expect(endpoint).toHaveProperty('name');
600
+ expect(endpoint).toHaveProperty('host');
601
+ expect(endpoint).toHaveProperty('port');
602
+ expect(endpoint).toHaveProperty('token');
603
+ expect(endpoint).toHaveProperty('enabled');
604
+ expect(endpoint).toHaveProperty('type');
605
+
606
+ // Verify port matches config
607
+ expect(endpoint.port).toBe(config.GATEWAY.DEFAULT_PORT);
608
+
609
+ // Verify timeout config exists
610
+ expect(config.GATEWAY.TIMEOUT_MS).toBeGreaterThan(0);
611
+ });
612
+
613
+ test('validation constraints are consistent with config', () => {
614
+ const validation = config.VALIDATION;
615
+
616
+ // Refresh interval constraints
617
+ expect(validation.REFRESH_INTERVAL.MIN).toBeLessThanOrEqual(config.REFRESH_INTERVALS.DEFAULT);
618
+ expect(validation.REFRESH_INTERVAL.MAX).toBeGreaterThan(config.REFRESH_INTERVALS.DEFAULT);
619
+
620
+ // Theme validation includes default
621
+ expect(validation.VALID_THEMES).toContain('auto');
622
+ expect(validation.VALID_THEMES).toContain('default');
623
+
624
+ // Sort modes are defined
625
+ expect(validation.VALID_SORT_MODES.length).toBeGreaterThan(0);
626
+ });
627
+ });
628
+
629
+ describe('Integration: Gateway Manager + Cache + Retry Workflow', () => {
630
+ let gatewayManager;
631
+
632
+ beforeEach(async () => {
633
+ // Import gateway manager as a singleton instance
634
+ const gwModule = await import('../src/gateway-manager.js');
635
+ gatewayManager = gwModule.gatewayManager || gwModule.default;
636
+ cache.clear();
637
+ // Reinitialize with default settings for each test
638
+ gatewayManager.init({ gatewayEndpoints: [] });
639
+ });
640
+
641
+ test('gateway manager initializes with default endpoints', () => {
642
+ const settings = { gatewayEndpoints: undefined };
643
+ gatewayManager.init(settings);
644
+
645
+ const enabled = gatewayManager.getEnabledEndpoints();
646
+ expect(enabled.length).toBeGreaterThan(0);
647
+ expect(enabled[0]).toHaveProperty('host');
648
+ expect(enabled[0]).toHaveProperty('port');
649
+ expect(enabled[0]).toHaveProperty('enabled', true);
650
+ });
651
+
652
+ test('gateway manager respects custom endpoint configuration', () => {
653
+ const customEndpoints = [
654
+ { name: 'local', host: 'localhost', port: 3000, enabled: true },
655
+ { name: 'remote', host: '192.168.1.100', port: 3001, enabled: false },
656
+ ];
657
+
658
+ gatewayManager.init({ gatewayEndpoints: customEndpoints });
659
+
660
+ const enabled = gatewayManager.getEnabledEndpoints();
661
+ const all = gatewayManager.getAllEndpoints();
662
+
663
+ expect(all.length).toBe(2);
664
+ expect(enabled.length).toBe(1);
665
+ expect(enabled[0].name).toBe('local');
666
+ expect(enabled[0].port).toBe(3000);
667
+ });
668
+
669
+ test('gateway manager enforces endpoint limits', () => {
670
+ gatewayManager.init({ gatewayEndpoints: [] });
671
+
672
+ // Add endpoints up to limit
673
+ const maxEndpoints = config.GATEWAY.MAX_ENDPOINTS;
674
+ for (let i = 0; i < maxEndpoints; i++) {
675
+ const result = gatewayManager.addEndpoint({
676
+ name: `endpoint-${i}`,
677
+ host: 'localhost',
678
+ port: 3000 + i,
679
+ });
680
+ expect(result).not.toBeNull();
681
+ }
682
+
683
+ // Try to add one more - should fail
684
+ const overflowResult = gatewayManager.addEndpoint({
685
+ name: 'overflow',
686
+ host: 'localhost',
687
+ port: 9999,
688
+ });
689
+ expect(overflowResult).toBeNull();
690
+ });
691
+
692
+ test('gateway manager handles duplicate endpoint names', () => {
693
+ gatewayManager.init({ gatewayEndpoints: [] });
694
+
695
+ const first = gatewayManager.addEndpoint({
696
+ name: 'duplicate',
697
+ host: 'localhost',
698
+ port: 3000,
699
+ });
700
+ expect(first).not.toBeNull();
701
+
702
+ // Try to add duplicate - should fail
703
+ const duplicate = gatewayManager.addEndpoint({
704
+ name: 'duplicate',
705
+ host: 'localhost',
706
+ port: 3001,
707
+ });
708
+ expect(duplicate).toBeNull();
709
+ });
710
+
711
+ test('gateway manager endpoint removal works correctly', () => {
712
+ gatewayManager.init({ gatewayEndpoints: [] });
713
+
714
+ gatewayManager.addEndpoint({ name: 'endpoint-1', host: 'localhost', port: 3000 });
715
+ gatewayManager.addEndpoint({ name: 'endpoint-2', host: 'localhost', port: 3001 });
716
+
717
+ // Remove one endpoint
718
+ const removed = gatewayManager.removeEndpoint('endpoint-1');
719
+ expect(removed).toBe(true);
720
+
721
+ // Verify remaining endpoints
722
+ const all = gatewayManager.getAllEndpoints();
723
+ expect(all.length).toBe(1);
724
+ expect(all[0].name).toBe('endpoint-2');
725
+
726
+ // Cannot remove non-existent endpoint
727
+ const notRemoved = gatewayManager.removeEndpoint('nonexistent');
728
+ expect(notRemoved).toBe(false);
729
+ });
730
+
731
+ test('gateway manager prevents removing last endpoint', () => {
732
+ gatewayManager.init({ gatewayEndpoints: [] });
733
+ gatewayManager.addEndpoint({ name: 'only', host: 'localhost', port: 3000 });
734
+
735
+ // Should not be able to remove the last endpoint
736
+ const removed = gatewayManager.removeEndpoint('only');
737
+ expect(removed).toBe(false);
738
+ });
739
+
740
+ test('cache integration with gateway endpoint data', async () => {
741
+ gatewayManager.init({ gatewayEndpoints: [] });
742
+ gatewayManager.addEndpoint({ name: 'test-endpoint', host: 'localhost', port: 3000 });
743
+
744
+ // Simulate caching endpoint status
745
+ const endpointStatus = { reachable: true, latency: 45, lastSeen: Date.now() };
746
+ cache.set('gateway-status-test-endpoint', endpointStatus, config.CACHE_TTL.DEFAULT);
747
+
748
+ // Verify cached data
749
+ const cached = cache.get('gateway-status-test-endpoint');
750
+ expect(cached).toEqual(endpointStatus);
751
+
752
+ // Verify cache expiration
753
+ cache.set('gateway-expiring', { test: true }, 50);
754
+ await new Promise(resolve => setTimeout(resolve, 60));
755
+ expect(cache.get('gateway-expiring')).toBeNull();
756
+ });
757
+ });
758
+
759
+ describe('Integration: Database + Cache Workflow', () => {
760
+ let database;
761
+
762
+ beforeEach(async () => {
763
+ database = await import('../src/database.js');
764
+ cache.clear();
765
+ });
766
+
767
+ afterEach(async () => {
768
+ // Clean up any intervals
769
+ if (database.saveInterval) clearInterval(database.saveInterval);
770
+ if (database.cleanupInterval) clearInterval(database.cleanupInterval);
771
+ });
772
+
773
+ test('database module exports expected functions', () => {
774
+ expect(database.initDatabase).toBeDefined();
775
+ expect(typeof database.initDatabase).toBe('function');
776
+ });
777
+
778
+ test('database initialization creates required tables', async () => {
779
+ // Note: This test verifies the module structure
780
+ // Full database tests would require sql.js initialization
781
+ const result = await database.initDatabase();
782
+
783
+ // Should return true on successful init
784
+ expect(typeof result).toBe('boolean');
785
+ });
786
+
787
+ test('cache database key follows naming convention', () => {
788
+ // Verify cache key naming for database-related data
789
+ const sessionKey = 'db-session-snapshot';
790
+ const metricsKey = 'db-metrics-history';
791
+
792
+ cache.set(sessionKey, { test: 'session' }, config.CACHE_TTL.DEFAULT);
793
+ cache.set(metricsKey, { test: 'metrics' }, config.CACHE_TTL.DEFAULT);
794
+
795
+ expect(cache.get(sessionKey)).toBeDefined();
796
+ expect(cache.get(metricsKey)).toBeDefined();
797
+ });
798
+ });
799
+
800
+ describe('Integration: Full System Metrics Collection Workflow', () => {
801
+ beforeEach(() => {
802
+ cache.clear();
803
+ alerts.resetThresholds();
804
+ alerts.clearAllAlerts();
805
+ performanceMonitor.reset();
806
+ performanceMonitor.stop();
807
+ });
808
+
809
+ afterEach(() => {
810
+ performanceMonitor.stop();
811
+ });
812
+
813
+ test('complete metrics collection and alert cycle', async () => {
814
+ // Phase 1: Start performance monitoring
815
+ performanceMonitor.start();
816
+
817
+ // Phase 2: Simulate system metrics collection
818
+ const systemMetrics = {
819
+ cpu: 45,
820
+ memory: 65,
821
+ disk: 55,
822
+ };
823
+
824
+ // Phase 3: Cache the metrics
825
+ cache.set('system-metrics-latest', systemMetrics, config.CACHE_TTL.CPU);
826
+
827
+ // Phase 4: Record performance snapshot
828
+ const snapshot = await performanceMonitor.record(100);
829
+ expect(snapshot).toBeDefined();
830
+ expect(snapshot.cpuPercent).toBeDefined();
831
+ expect(snapshot.memoryPercent).toBeDefined();
832
+
833
+ // Phase 5: Check alerts
834
+ const newAlerts = alerts.checkAllMetrics(systemMetrics);
835
+ expect(newAlerts.length).toBe(0); // Normal values, no alerts
836
+
837
+ // Phase 6: Simulate high load scenario
838
+ const highLoadMetrics = {
839
+ cpu: 92,
840
+ memory: 88,
841
+ disk: 55,
842
+ };
843
+
844
+ cache.set('system-metrics-latest', highLoadMetrics, config.CACHE_TTL.CPU);
845
+ const highLoadAlerts = alerts.checkAllMetrics(highLoadMetrics);
846
+
847
+ // Should trigger CPU and memory alerts
848
+ expect(highLoadAlerts.length).toBeGreaterThanOrEqual(2);
849
+ const alertTypes = highLoadAlerts.map(a => a.type);
850
+ expect(alertTypes).toContain('cpu');
851
+ expect(alertTypes).toContain('memory');
852
+
853
+ // Phase 7: Verify health check
854
+ const health = performanceMonitor.checkHealth();
855
+ expect(health).toHaveProperty('degraded');
856
+ expect(health).toHaveProperty('reasons');
857
+ });
858
+
859
+ test('metrics history persists through multiple collection cycles', async () => {
860
+ performanceMonitor.start();
861
+ performanceMonitor.maxHistory = 20;
862
+
863
+ // Simulate multiple collection cycles
864
+ const cycles = 25;
865
+ for (let i = 0; i < cycles; i++) {
866
+ // Simulate varying metrics
867
+ const metrics = {
868
+ cpu: 40 + (i % 30),
869
+ memory: 50 + (i % 25),
870
+ disk: 50,
871
+ };
872
+
873
+ cache.set('system-metrics-cycle', metrics, config.CACHE_TTL.CPU);
874
+ performanceMonitor.record(50);
875
+ }
876
+
877
+ // Verify history is capped
878
+ expect(performanceMonitor.history.length).toBeLessThanOrEqual(20);
879
+
880
+ // Verify aggregates are calculated
881
+ const metrics = performanceMonitor.getMetrics();
882
+ expect(metrics.aggregates).toBeDefined();
883
+ expect(metrics.aggregates.avgMemoryUsed).toBeDefined();
884
+ expect(metrics.aggregates.peakMemoryUsed).toBeDefined();
885
+
886
+ // Verify sparkline data is available
887
+ const memorySparkline = performanceMonitor.getMemorySparkline();
888
+ expect(Array.isArray(memorySparkline)).toBe(true);
889
+ expect(memorySparkline.length).toBeGreaterThan(0);
890
+ });
891
+
892
+ test('cache prevents redundant system calls during rapid refresh', async () => {
893
+ let fetchCount = 0;
894
+
895
+ const fetchSystemMetrics = async () => {
896
+ fetchCount++;
897
+ return {
898
+ cpu: 50 + fetchCount,
899
+ memory: 60 + fetchCount,
900
+ disk: 55,
901
+ };
902
+ };
903
+
904
+ const cacheKey = 'rapid-refresh-test';
905
+ const ttl = 100; // 100ms TTL
906
+
907
+ // First fetch - cache miss
908
+ let metrics = cache.get(cacheKey);
909
+ if (metrics === null) {
910
+ metrics = await fetchSystemMetrics();
911
+ cache.set(cacheKey, metrics, ttl);
912
+ }
913
+ expect(fetchCount).toBe(1);
914
+
915
+ // Second fetch within TTL - cache hit
916
+ metrics = cache.get(cacheKey);
917
+ expect(metrics).toBeDefined();
918
+ expect(fetchCount).toBe(1); // Should not have fetched again
919
+
920
+ // Wait for TTL to expire
921
+ await new Promise(resolve => setTimeout(resolve, ttl + 10));
922
+
923
+ // Third fetch after TTL - cache miss, should fetch again
924
+ metrics = cache.get(cacheKey);
925
+ if (metrics === null) {
926
+ metrics = await fetchSystemMetrics();
927
+ cache.set(cacheKey, metrics, ttl);
928
+ }
929
+ expect(fetchCount).toBe(2);
930
+ });
931
+ });
932
+
933
+ describe('Integration: Settings Validation + Config Workflow', () => {
934
+ let validateSettings, validateGatewayEndpoint, getDefaultSettings;
935
+
936
+ beforeEach(async () => {
937
+ const validation = await import('../src/validation.js');
938
+ validateSettings = validation.validateSettings;
939
+ validateGatewayEndpoint = validation.validateGatewayEndpoint;
940
+ getDefaultSettings = validation.getDefaultSettings;
941
+ });
942
+
943
+ test('default settings pass validation', () => {
944
+ const defaultSettings = getDefaultSettings();
945
+ const result = validateSettings(defaultSettings);
946
+
947
+ expect(result.valid).toBe(true);
948
+ expect(result.value).toBeDefined();
949
+ expect(result.value.refreshInterval).toBe(config.REFRESH_INTERVALS.DEFAULT);
950
+ });
951
+
952
+ test('invalid refresh interval is corrected', () => {
953
+ const invalidSettings = {
954
+ ...getDefaultSettings(),
955
+ refreshInterval: 100, // Below minimum
956
+ };
957
+
958
+ const result = validateSettings(invalidSettings);
959
+
960
+ expect(result.valid).toBe(true); // Should auto-correct
961
+ expect(result.value.refreshInterval).toBeGreaterThanOrEqual(config.VALIDATION.REFRESH_INTERVAL.MIN);
962
+ });
963
+
964
+ test('invalid theme is corrected to default', () => {
965
+ const invalidSettings = {
966
+ ...getDefaultSettings(),
967
+ theme: 'nonexistent-theme',
968
+ };
969
+
970
+ const result = validateSettings(invalidSettings);
971
+
972
+ expect(result.valid).toBe(true);
973
+ expect(config.VALIDATION.VALID_THEMES).toContain(result.value.theme);
974
+ });
975
+
976
+ test('gateway endpoint validation catches missing fields', () => {
977
+ const invalidEndpoints = [
978
+ { name: '', host: 'localhost', port: 3000 }, // Empty name
979
+ { name: 'test', host: '', port: 3000 }, // Empty host
980
+ ];
981
+
982
+ invalidEndpoints.forEach(endpoint => {
983
+ const result = validateGatewayEndpoint(endpoint);
984
+ expect(result.valid).toBe(false);
985
+ });
986
+ });
987
+
988
+ test('valid gateway endpoint passes validation', () => {
989
+ const validEndpoint = {
990
+ name: 'test-endpoint',
991
+ host: 'localhost',
992
+ port: 3000,
993
+ token: 'test-token',
994
+ enabled: true,
995
+ type: 'local',
996
+ };
997
+
998
+ const result = validateGatewayEndpoint(validEndpoint);
999
+ expect(result.valid).toBe(true);
1000
+ });
1001
+
1002
+ test('settings export format validation', () => {
1003
+ const validFormats = ['json', 'yaml'];
1004
+ const invalidFormat = 'xml';
1005
+
1006
+ // Valid format
1007
+ const validSettings = { ...getDefaultSettings(), exportFormat: 'json' };
1008
+ let result = validateSettings(validSettings);
1009
+ expect(result.valid).toBe(true);
1010
+
1011
+ // Invalid format should be corrected
1012
+ const invalidSettings = { ...getDefaultSettings(), exportFormat: invalidFormat };
1013
+ result = validateSettings(invalidSettings);
1014
+ expect(result.value.exportFormat).not.toBe(invalidFormat);
1015
+ });
1016
+ });
1017
+
1018
+ describe('Integration: Error Recovery Workflow', () => {
1019
+ beforeEach(() => {
1020
+ cache.clear();
1021
+ alerts.clearAllAlerts();
1022
+ });
1023
+
1024
+ test('graceful degradation when cache fails', async () => {
1025
+ // Simulate cache miss with retry fallback
1026
+ const cacheKey = 'degradation-test';
1027
+
1028
+ // Ensure cache miss
1029
+ expect(cache.get(cacheKey)).toBeNull();
1030
+
1031
+ // Simulated fetch that might fail
1032
+ let fetchAttempts = 0;
1033
+ const unreliableFetch = async () => {
1034
+ fetchAttempts++;
1035
+ if (fetchAttempts === 1) {
1036
+ throw new Error('Temporary failure');
1037
+ }
1038
+ return { data: 'recovered' };
1039
+ };
1040
+
1041
+ // Use retry module for resilience
1042
+ const wrappedFetch = retry.withRetry(unreliableFetch, {
1043
+ maxRetries: 2,
1044
+ initialDelay: 10,
1045
+ retryableErrors: ['Temporary failure'],
1046
+ });
1047
+
1048
+ const result = await wrappedFetch();
1049
+ expect(result.data).toBe('recovered');
1050
+ expect(fetchAttempts).toBe(2);
1051
+ });
1052
+
1053
+ test('alert system continues working after module errors', () => {
1054
+ // Use all valid metric values that will generate alerts
1055
+ const metricsWithAllValid = {
1056
+ cpu: 95, // Above critical threshold (90)
1057
+ memory: 85, // Above warning threshold (75)
1058
+ disk: 96, // Above critical threshold (95)
1059
+ };
1060
+
1061
+ // Alert system should process all valid metrics
1062
+ const results = alerts.checkAllMetrics(metricsWithAllValid);
1063
+
1064
+ // Should generate alerts for all three metrics
1065
+ expect(results.length).toBeGreaterThanOrEqual(2);
1066
+ const alertTypes = results.map(a => a.type);
1067
+ expect(alertTypes).toContain('cpu');
1068
+ expect(alertTypes).toContain('disk');
1069
+ });
1070
+
1071
+ test('retry module handles transient network errors', async () => {
1072
+ let attemptCount = 0;
1073
+ const networkErrors = ['ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET'];
1074
+
1075
+ const flakyNetworkFn = async () => {
1076
+ attemptCount++;
1077
+ if (attemptCount < 3) {
1078
+ const error = new Error(networkErrors[attemptCount - 1]);
1079
+ error.code = networkErrors[attemptCount - 1];
1080
+ throw error;
1081
+ }
1082
+ return { success: true };
1083
+ };
1084
+
1085
+ const wrappedFn = retry.withRetry(flakyNetworkFn, {
1086
+ maxRetries: 3,
1087
+ initialDelay: 10,
1088
+ maxDelay: 50,
1089
+ retryableErrors: networkErrors,
1090
+ });
1091
+
1092
+ const result = await wrappedFn();
1093
+ expect(result.success).toBe(true);
1094
+ expect(attemptCount).toBe(3);
1095
+ });
1096
+
1097
+ test('retry gives up on non-retryable errors', async () => {
1098
+ const nonRetryableErrors = ['EINVAL', 'ENOENT', 'EACCES'];
1099
+
1100
+ for (const errorCode of nonRetryableErrors) {
1101
+ let attemptCount = 0;
1102
+
1103
+ const failingFn = async () => {
1104
+ attemptCount++;
1105
+ const error = new Error(`Non-retryable: ${errorCode}`);
1106
+ error.code = errorCode;
1107
+ throw error;
1108
+ };
1109
+
1110
+ const wrappedFn = retry.withRetry(failingFn, {
1111
+ maxRetries: 3,
1112
+ initialDelay: 10,
1113
+ retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT'],
1114
+ });
1115
+
1116
+ await expect(wrappedFn()).rejects.toThrow();
1117
+ expect(attemptCount).toBe(1); // Should not retry
1118
+ }
1119
+ });
1120
+ });
1121
+
1122
+ describe('Integration: Multi-Module Dashboard Refresh Simulation', () => {
1123
+ beforeEach(() => {
1124
+ cache.clear();
1125
+ alerts.resetThresholds();
1126
+ alerts.clearAllAlerts();
1127
+ alerts.resetRateLimit();
1128
+ performanceMonitor.reset();
1129
+ performanceMonitor.stop();
1130
+ });
1131
+
1132
+ afterEach(() => {
1133
+ performanceMonitor.stop();
1134
+ });
1135
+
1136
+ test('simulates complete dashboard refresh with all modules', async () => {
1137
+ // Initialize all modules
1138
+ performanceMonitor.start();
1139
+
1140
+ // Simulate a complete dashboard refresh cycle
1141
+ const refreshCycle = async () => {
1142
+ // 1. Fetch system metrics (simulated)
1143
+ const systemMetrics = {
1144
+ cpu: Math.random() * 100,
1145
+ memory: Math.random() * 100,
1146
+ disk: 50 + Math.random() * 10,
1147
+ };
1148
+
1149
+ // 2. Cache metrics
1150
+ cache.set('dashboard-system-metrics', systemMetrics, config.CACHE_TTL.CPU);
1151
+
1152
+ // 3. Record performance snapshot
1153
+ await performanceMonitor.record(100);
1154
+
1155
+ // 4. Check alerts
1156
+ const newAlerts = alerts.checkAllMetrics(systemMetrics);
1157
+
1158
+ // 5. Get health status
1159
+ const health = performanceMonitor.checkHealth();
1160
+
1161
+ return { systemMetrics, newAlerts, health };
1162
+ };
1163
+
1164
+ // Run multiple refresh cycles
1165
+ const results = [];
1166
+ for (let i = 0; i < 5; i++) {
1167
+ const result = await refreshCycle();
1168
+ results.push(result);
1169
+
1170
+ // Small delay between cycles
1171
+ await new Promise(resolve => setTimeout(resolve, 50));
1172
+ }
1173
+
1174
+ // Verify all cycles completed
1175
+ expect(results.length).toBe(5);
1176
+
1177
+ // Verify performance history was recorded
1178
+ expect(performanceMonitor.history.length).toBeGreaterThan(0);
1179
+
1180
+ // Verify cache was used
1181
+ const cachedMetrics = cache.get('dashboard-system-metrics');
1182
+ expect(cachedMetrics).toBeDefined();
1183
+ });
1184
+
1185
+ test('handles rapid refresh requests with debouncing', async () => {
1186
+ let refreshCount = 0;
1187
+
1188
+ const doRefresh = async () => {
1189
+ refreshCount++;
1190
+ return { timestamp: Date.now() };
1191
+ };
1192
+
1193
+ // Wrap with debounce
1194
+ const debouncedRefresh = cache.debounce(doRefresh, 100);
1195
+
1196
+ // Trigger multiple rapid refreshes
1197
+ debouncedRefresh();
1198
+ debouncedRefresh();
1199
+ debouncedRefresh();
1200
+ debouncedRefresh();
1201
+
1202
+ // Wait for debounce to settle
1203
+ await new Promise(resolve => setTimeout(resolve, 150));
1204
+
1205
+ // Should only execute once due to debouncing
1206
+ expect(refreshCount).toBe(1);
1207
+ });
1208
+
1209
+ test('handles rapid refresh requests with throttling', async () => {
1210
+ let refreshCount = 0;
1211
+ const results = [];
1212
+
1213
+ const doRefresh = async () => {
1214
+ refreshCount++;
1215
+ const result = { timestamp: Date.now(), count: refreshCount };
1216
+ results.push(result);
1217
+ return result;
1218
+ };
1219
+
1220
+ // Wrap with throttle
1221
+ const throttledRefresh = cache.throttle(doRefresh, 100);
1222
+
1223
+ // Trigger multiple rapid refreshes
1224
+ await throttledRefresh();
1225
+ await throttledRefresh();
1226
+ await throttledRefresh();
1227
+
1228
+ // Should only execute once within throttle window
1229
+ expect(refreshCount).toBe(1);
1230
+ });
1231
+
1232
+ test('alert escalation and recovery workflow', async () => {
1233
+ performanceMonitor.start();
1234
+
1235
+ // Phase 1: Normal state
1236
+ let newAlerts = alerts.checkAllMetrics({ cpu: 40, memory: 50, disk: 50 });
1237
+ expect(newAlerts.length).toBe(0);
1238
+
1239
+ // Phase 2: Warning state
1240
+ newAlerts = alerts.checkAllMetrics({ cpu: 75, memory: 50, disk: 50 });
1241
+ expect(newAlerts.length).toBe(1);
1242
+ expect(newAlerts[0].level).toBe(alerts.AlertLevel.WARNING);
1243
+
1244
+ // Phase 3: Critical escalation
1245
+ newAlerts = alerts.checkAllMetrics({ cpu: 95, memory: 50, disk: 50 });
1246
+ // Should update existing alert (returns null for updates)
1247
+ const activeAlerts = alerts.getActiveAlerts();
1248
+ const cpuAlert = activeAlerts.find(a => a.type === 'cpu');
1249
+ expect(cpuAlert.level).toBe(alerts.AlertLevel.CRITICAL);
1250
+
1251
+ // Phase 4: Recovery
1252
+ newAlerts = alerts.checkAllMetrics({ cpu: 40, memory: 50, disk: 50 });
1253
+ expect(newAlerts.length).toBe(1);
1254
+ expect(newAlerts[0].level).toBe(alerts.AlertLevel.CLEARED);
1255
+ expect(newAlerts[0].message).toContain('normalized');
1256
+
1257
+ // Verify alert history
1258
+ const history = alerts.getAlertHistory();
1259
+ expect(history.length).toBeGreaterThan(0);
1260
+ });
1261
+
1262
+ test('concurrent metric updates maintain consistency', async () => {
1263
+ performanceMonitor.start();
1264
+
1265
+ // Simulate concurrent metric updates
1266
+ const concurrentUpdates = async () => {
1267
+ const promises = [];
1268
+
1269
+ for (let i = 0; i < 10; i++) {
1270
+ promises.push(
1271
+ Promise.resolve().then(() => {
1272
+ const metrics = {
1273
+ cpu: 50 + Math.random() * 20,
1274
+ memory: 50 + Math.random() * 20,
1275
+ disk: 50,
1276
+ };
1277
+ cache.set('concurrent-metrics', metrics, config.CACHE_TTL.CPU);
1278
+ performanceMonitor.record(50);
1279
+ return metrics;
1280
+ })
1281
+ );
1282
+ }
1283
+
1284
+ return Promise.all(promises);
1285
+ };
1286
+
1287
+ const results = await concurrentUpdates();
1288
+ expect(results.length).toBe(10);
1289
+
1290
+ // Verify final cached state is consistent
1291
+ const finalMetrics = cache.get('concurrent-metrics');
1292
+ expect(finalMetrics).toBeDefined();
1293
+ expect(finalMetrics.cpu).toBeGreaterThan(0);
1294
+ expect(finalMetrics.memory).toBeGreaterThan(0);
1295
+ });
1296
+ });
1297
+
1298
+ describe('Integration: Container Detection + System Info Workflow', () => {
1299
+ let containerDetector;
1300
+
1301
+ beforeEach(async () => {
1302
+ containerDetector = await import('../src/container-detector.js');
1303
+ });
1304
+
1305
+ test('container detector module exports expected functions', () => {
1306
+ expect(containerDetector.detectContainerEnv).toBeDefined();
1307
+ expect(typeof containerDetector.detectContainerEnv).toBe('function');
1308
+ });
1309
+
1310
+ test('container detection integrates with system info', async () => {
1311
+ // This test verifies the module integration
1312
+ // Full container detection would require actual container environment
1313
+ const env = await containerDetector.detectContainerEnv();
1314
+
1315
+ // Should return an object with expected properties
1316
+ expect(env).toHaveProperty('isContainer');
1317
+ expect(env).toHaveProperty('isWSL');
1318
+ expect(typeof env.isContainer).toBe('boolean');
1319
+ expect(typeof env.isWSL).toBe('boolean');
1320
+ });
1321
+ });