claw-dashboard 2.1.1 → 2.2.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 (89) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/API.md +1 -2
  3. package/index.js +31 -41
  4. package/package.json +22 -12
  5. package/src/auto-save.js +11 -5
  6. package/src/cli/export-snapshot.js +3 -1
  7. package/src/cli/import-snapshot.js +3 -1
  8. package/src/config.js +9 -15
  9. package/src/errors.js +0 -9
  10. package/src/security.js +6 -2
  11. package/src/snapshot.js +73 -26
  12. package/src/web-server.js +7 -10
  13. package/.c8rc.json +0 -38
  14. package/.dockerignore +0 -68
  15. package/.github/dependabot.yml +0 -45
  16. package/.github/pull_request_template.md +0 -39
  17. package/.github/workflows/ci.yml +0 -147
  18. package/.github/workflows/release.yml +0 -40
  19. package/.github/workflows/security.yml +0 -84
  20. package/.husky/pre-commit +0 -1
  21. package/.planning/codebase/ARCHITECTURE.md +0 -206
  22. package/.planning/codebase/INTEGRATIONS.md +0 -150
  23. package/.planning/codebase/STACK.md +0 -122
  24. package/.planning/codebase/STRUCTURE.md +0 -201
  25. package/CONTRIBUTING.md +0 -378
  26. package/Dockerfile +0 -54
  27. package/FEATURES.md +0 -307
  28. package/TODO.md +0 -28
  29. package/ai.openclaw.dashboard.plist +0 -35
  30. package/build-cjs.js +0 -127
  31. package/cjs-shim.js +0 -13
  32. package/dist/clawdash +0 -1729
  33. package/dist/clawdash.meta.json +0 -2236
  34. package/dist/widgets.cjs +0 -6511
  35. package/docker-compose.yml +0 -67
  36. package/esbuild.config.js +0 -158
  37. package/eslint.config.js +0 -56
  38. package/examples/plugins/README.md +0 -122
  39. package/examples/plugins/api-status/index.js +0 -294
  40. package/examples/plugins/api-status/plugin.json +0 -19
  41. package/examples/plugins/hello-world/index.js +0 -104
  42. package/examples/plugins/hello-world/plugin.json +0 -15
  43. package/examples/plugins/system-metrics-chart/index.js +0 -339
  44. package/examples/plugins/system-metrics-chart/plugin.json +0 -18
  45. package/examples/plugins/weather-widget/index.js +0 -136
  46. package/examples/plugins/weather-widget/plugin.json +0 -19
  47. package/index.cjs +0 -23005
  48. package/jest.config.js +0 -11
  49. package/scripts/release.js +0 -595
  50. package/src/database.js +0 -734
  51. package/tests/alerts.test.js +0 -437
  52. package/tests/auto-save.test.js +0 -529
  53. package/tests/cache.test.js +0 -317
  54. package/tests/cli.test.js +0 -351
  55. package/tests/command-palette.test.js +0 -320
  56. package/tests/config-processor.test.js +0 -452
  57. package/tests/config-validator.test.js +0 -710
  58. package/tests/config-watcher.test.js +0 -594
  59. package/tests/config.test.js +0 -755
  60. package/tests/database.test.js +0 -438
  61. package/tests/errors.test.js +0 -189
  62. package/tests/example-plugins.test.js +0 -624
  63. package/tests/integration.test.js +0 -1321
  64. package/tests/loading-states.test.js +0 -300
  65. package/tests/manifest-validation-on-load.test.js +0 -671
  66. package/tests/performance-monitor.test.js +0 -190
  67. package/tests/plugin-api-rate-limit.test.js +0 -302
  68. package/tests/plugin-errors.test.js +0 -311
  69. package/tests/plugin-lifecycle-e2e.test.js +0 -1036
  70. package/tests/plugin-reload.test.js +0 -764
  71. package/tests/rate-limiter.test.js +0 -539
  72. package/tests/removed-dependencies.test.js +0 -74
  73. package/tests/retry.test.js +0 -308
  74. package/tests/security.test.js +0 -411
  75. package/tests/settings-modal.test.js +0 -226
  76. package/tests/theme-selector.test.js +0 -57
  77. package/tests/utils.js +0 -242
  78. package/tests/utils.test.js +0 -317
  79. package/tests/validate-plugin-cli.test.js +0 -359
  80. package/tests/validation.test.js +0 -837
  81. package/tests/web-server.test.js +0 -646
  82. package/tests/widget-arrange-mode.test.js +0 -183
  83. package/tests/widget-config-hot-reload.test.js +0 -465
  84. package/tests/widget-dependency.test.js +0 -591
  85. package/tests/widget-error-boundary.test.js +0 -749
  86. package/tests/widget-error-isolation.test.js +0 -469
  87. package/tests/widget-integration.test.js +0 -1147
  88. package/tests/widget-refresh-intervals.test.js +0 -284
  89. package/tests/worker-pool.test.js +0 -522
@@ -1,522 +0,0 @@
1
- /**
2
- * Tests for WorkerPool - task execution, timeout handling, worker recovery, error propagation
3
- */
4
-
5
- import { jest } from '@jest/globals';
6
- import { WorkerPool } from '../src/workers/worker-pool.js';
7
-
8
- // Mock systeminformation for fallback tests
9
- const mockSiData = {
10
- currentLoad: { avgLoad: 1.5, currentLoad: 25 },
11
- mem: { total: 16000000000, used: 8000000000 },
12
- graphics: { controllers: [] },
13
- networkStats: [{ iface: 'eth0', rx_bytes: 1000 }],
14
- fsSize: [{ fs: '/', size: 1000000000 }],
15
- osInfo: { platform: 'darwin', hostname: 'test-host' },
16
- versions: { node: '18.0.0' },
17
- time: { uptime: 3600 },
18
- };
19
-
20
- jest.unstable_mockModule('systeminformation', () => ({
21
- default: {
22
- currentLoad: jest.fn().mockResolvedValue(mockSiData.currentLoad),
23
- mem: jest.fn().mockResolvedValue(mockSiData.mem),
24
- graphics: jest.fn().mockResolvedValue(mockSiData.graphics),
25
- networkStats: jest.fn().mockResolvedValue(mockSiData.networkStats),
26
- fsSize: jest.fn().mockResolvedValue(mockSiData.fsSize),
27
- osInfo: jest.fn().mockResolvedValue(mockSiData.osInfo),
28
- versions: jest.fn().mockResolvedValue(mockSiData.versions),
29
- time: jest.fn().mockResolvedValue(mockSiData.time),
30
- },
31
- }));
32
-
33
- // Mock logger
34
- jest.unstable_mockModule('../src/logger.js', () => ({
35
- default: {
36
- info: jest.fn(),
37
- warn: jest.fn(),
38
- error: jest.fn(),
39
- debug: jest.fn(),
40
- },
41
- }));
42
-
43
- describe('WorkerPool', () => {
44
- let pool;
45
-
46
- afterEach(async () => {
47
- if (pool) {
48
- await pool.shutdown();
49
- pool = null;
50
- }
51
- });
52
-
53
- describe('constructor', () => {
54
- test('should create WorkerPool with default options from config', async () => {
55
- pool = new WorkerPool();
56
-
57
- // Uses config.js defaults since we can't mock it effectively in ES modules
58
- expect(pool.taskTimeout).toBe(10000); // Default from worker-pool.js
59
- expect(pool.enableWorkers).toBe(true);
60
- expect(pool.workersSupported).toBe(true);
61
- });
62
-
63
- test('should create WorkerPool with custom options', async () => {
64
- pool = new WorkerPool({
65
- maxWorkers: 4,
66
- taskTimeout: 5000,
67
- enableWorkers: false,
68
- });
69
-
70
- expect(pool.maxWorkers).toBe(4);
71
- expect(pool.taskTimeout).toBe(5000);
72
- expect(pool.enableWorkers).toBe(false);
73
- expect(pool.workers.length).toBe(0);
74
- });
75
-
76
- test('should respect workersSupported flag when disabled', async () => {
77
- pool = new WorkerPool({ enableWorkers: false });
78
- expect(pool.workersSupported).toBe(true);
79
- expect(pool.workers.length).toBe(0);
80
- });
81
- });
82
-
83
- describe('task execution', () => {
84
- test('should execute task and return result via fallback', async () => {
85
- pool = new WorkerPool({ enableWorkers: false });
86
-
87
- const result = await pool.execute('currentLoad');
88
-
89
- expect(result).toEqual(mockSiData.currentLoad);
90
- });
91
-
92
- test('should execute multiple commands', async () => {
93
- pool = new WorkerPool({ enableWorkers: false });
94
-
95
- const result1 = await pool.execute('mem');
96
- const result2 = await pool.execute('fsSize');
97
-
98
- expect(result1).toEqual(mockSiData.mem);
99
- expect(result2).toEqual(mockSiData.fsSize);
100
- });
101
- });
102
-
103
- describe('fallback execution', () => {
104
- test('should use fallback when workers are disabled', async () => {
105
- pool = new WorkerPool({ enableWorkers: false });
106
-
107
- const result = await pool.execute('currentLoad');
108
-
109
- expect(result).toEqual(mockSiData.currentLoad);
110
- });
111
-
112
- test('should use fallback when workers are not supported', async () => {
113
- pool = new WorkerPool({ enableWorkers: false });
114
- pool.workersSupported = false;
115
-
116
- const result = await pool.execute('mem');
117
-
118
- expect(result).toEqual(mockSiData.mem);
119
- });
120
-
121
- test('should fallback execute currentLoad command', async () => {
122
- pool = new WorkerPool({ enableWorkers: false });
123
-
124
- const result = await pool.execute('currentLoad');
125
- expect(result).toEqual(mockSiData.currentLoad);
126
- });
127
-
128
- test('should fallback execute mem command', async () => {
129
- pool = new WorkerPool({ enableWorkers: false });
130
-
131
- const result = await pool.execute('mem');
132
- expect(result).toEqual(mockSiData.mem);
133
- });
134
-
135
- test('should fallback execute graphics command', async () => {
136
- pool = new WorkerPool({ enableWorkers: false });
137
-
138
- const result = await pool.execute('graphics');
139
- expect(result).toEqual(mockSiData.graphics);
140
- });
141
-
142
- test('should fallback execute networkStats command', async () => {
143
- pool = new WorkerPool({ enableWorkers: false });
144
-
145
- const result = await pool.execute('networkStats');
146
- expect(result).toEqual(mockSiData.networkStats);
147
- });
148
-
149
- test('should fallback execute fsSize command', async () => {
150
- pool = new WorkerPool({ enableWorkers: false });
151
-
152
- const result = await pool.execute('fsSize');
153
- expect(result).toEqual(mockSiData.fsSize);
154
- });
155
-
156
- test('should fallback execute systemData command', async () => {
157
- pool = new WorkerPool({ enableWorkers: false });
158
-
159
- const result = await pool.execute('systemData');
160
-
161
- expect(result).toEqual({
162
- os: mockSiData.osInfo,
163
- ver: mockSiData.versions,
164
- time: mockSiData.time,
165
- });
166
- });
167
-
168
- test('should throw for unknown command in fallback', async () => {
169
- pool = new WorkerPool({ enableWorkers: false });
170
-
171
- await expect(pool.execute('unknownCommand')).rejects.toThrow('Unknown command: unknownCommand');
172
- });
173
-
174
- test('should propagate fallback execution errors', async () => {
175
- const { default: systemInfo } = await import('systeminformation');
176
- pool = new WorkerPool({ enableWorkers: false });
177
-
178
- systemInfo.currentLoad.mockRejectedValueOnce(new Error('SI Error'));
179
-
180
- await expect(pool.execute('currentLoad')).rejects.toThrow('SI Error');
181
- });
182
- });
183
-
184
- describe('getStatus', () => {
185
- test('should return pool status with workers disabled', async () => {
186
- pool = new WorkerPool({ enableWorkers: false });
187
-
188
- const status = pool.getStatus();
189
-
190
- expect(status.enabled).toBe(false);
191
- expect(status.supported).toBe(true);
192
- expect(status.totalWorkers).toBe(0);
193
- expect(status.busyWorkers).toBe(0);
194
- expect(status.readyWorkers).toBe(0);
195
- expect(status.pendingTasks).toBe(0);
196
- expect(status.queuedTasks).toBe(0);
197
- });
198
-
199
- test('should count pending and queued tasks correctly', async () => {
200
- pool = new WorkerPool({ enableWorkers: false });
201
-
202
- // Manually add some pending and queued tasks
203
- pool.pendingTasks.set(1, {
204
- id: 1,
205
- command: 'test',
206
- resolve: () => {},
207
- reject: () => {},
208
- timeout: null,
209
- });
210
- pool.taskQueue.push({ id: 2, command: 'test2' });
211
-
212
- const status = pool.getStatus();
213
- expect(status.pendingTasks).toBe(1);
214
- expect(status.queuedTasks).toBe(1);
215
- });
216
- });
217
-
218
- describe('shutdown', () => {
219
- test('should set shutdown flag', async () => {
220
- pool = new WorkerPool({ enableWorkers: false });
221
-
222
- expect(pool.isShutdown).toBe(false);
223
- await pool.shutdown();
224
-
225
- expect(pool.isShutdown).toBe(true);
226
- });
227
-
228
- test('should reject pending tasks on shutdown', async () => {
229
- pool = new WorkerPool({ enableWorkers: false, taskTimeout: 10000 });
230
-
231
- // Add a pending task with proper reject function
232
- const rejectFn = jest.fn();
233
- pool.pendingTasks.set(1, {
234
- id: 1,
235
- command: 'test',
236
- resolve: () => {},
237
- reject: rejectFn,
238
- timeout: null,
239
- });
240
-
241
- await pool.shutdown();
242
-
243
- expect(rejectFn).toHaveBeenCalledWith(new Error('Worker pool shutting down'));
244
- expect(pool.pendingTasks.size).toBe(0);
245
- });
246
-
247
- test('should clear task queue on shutdown', async () => {
248
- pool = new WorkerPool({ enableWorkers: false });
249
-
250
- // Add tasks to queue
251
- pool.taskQueue.push({ id: 1, command: 'test' });
252
- pool.taskQueue.push({ id: 2, command: 'test2' });
253
-
254
- await pool.shutdown();
255
-
256
- expect(pool.taskQueue.length).toBe(0);
257
- });
258
-
259
- test('should be safe to call shutdown multiple times', async () => {
260
- pool = new WorkerPool({ enableWorkers: false });
261
-
262
- await pool.shutdown();
263
- await pool.shutdown(); // Should not throw
264
-
265
- expect(pool.isShutdown).toBe(true);
266
- });
267
- });
268
-
269
- describe('checkWorkerSupport', () => {
270
- test('should return true for Node.js 12+', () => {
271
- pool = new WorkerPool();
272
- expect(pool.checkWorkerSupport()).toBe(true);
273
- });
274
- });
275
-
276
- describe('task ID generation', () => {
277
- test('taskId starts at 0', () => {
278
- pool = new WorkerPool({ enableWorkers: false });
279
- expect(pool.taskId).toBe(0);
280
- });
281
- });
282
-
283
- describe('processQueue edge cases', () => {
284
- test('should handle empty queue', async () => {
285
- pool = new WorkerPool({ enableWorkers: false });
286
-
287
- // Should not throw
288
- pool.processQueue();
289
- expect(pool.taskQueue.length).toBe(0);
290
- });
291
-
292
- test('should not process queue when workers are disabled', async () => {
293
- pool = new WorkerPool({ enableWorkers: false });
294
-
295
- // Add task to queue
296
- pool.taskQueue.push({ id: 1, command: 'test', options: {} });
297
-
298
- // Try to process
299
- pool.processQueue();
300
-
301
- // Task should remain in queue since no workers
302
- expect(pool.taskQueue.length).toBe(1);
303
- });
304
- });
305
-
306
- describe('worker management', () => {
307
- test('should create and remove workers', async () => {
308
- pool = new WorkerPool({ enableWorkers: false });
309
-
310
- // Mock worker for testing
311
- const mockWorker = { id: 99, terminate: jest.fn().mockResolvedValue(0) };
312
- pool.workers.push(mockWorker);
313
-
314
- expect(pool.workers.length).toBe(1);
315
-
316
- pool.removeWorker(99);
317
-
318
- expect(pool.workers.length).toBe(0);
319
- });
320
-
321
- test('should handle removing non-existent worker', async () => {
322
- pool = new WorkerPool({ enableWorkers: false });
323
-
324
- // Should not throw
325
- pool.removeWorker(999);
326
- expect(pool.workers.length).toBe(0);
327
- });
328
-
329
- test('should restart worker', async () => {
330
- pool = new WorkerPool({ enableWorkers: false });
331
-
332
- // Mock worker for testing
333
- const mockWorker = { id: 99, terminate: jest.fn().mockResolvedValue(0) };
334
- pool.workers.push(mockWorker);
335
-
336
- // Restart worker
337
- pool.restartWorker(99);
338
-
339
- // Should have called terminate
340
- expect(mockWorker.terminate).toHaveBeenCalled();
341
- });
342
- });
343
-
344
- describe('error handling in fallbackExecute', () => {
345
- test('should propagate fallback execution errors', async () => {
346
- const { default: systemInfo } = await import('systeminformation');
347
-
348
- pool = new WorkerPool({ enableWorkers: false });
349
-
350
- systemInfo.currentLoad.mockRejectedValueOnce(new Error('Test error'));
351
-
352
- await expect(pool.execute('currentLoad')).rejects.toThrow('Test error');
353
- });
354
- });
355
-
356
- describe('handleWorkerMessage', () => {
357
- test('should handle ready signal', async () => {
358
- pool = new WorkerPool({ enableWorkers: false });
359
-
360
- const mockWorker = { id: 1, isReady: false, isBusy: false };
361
-
362
- pool.handleWorkerMessage(mockWorker, { type: 'ready' });
363
-
364
- expect(mockWorker.isReady).toBe(true);
365
- });
366
-
367
- test('should handle successful task completion', async () => {
368
- pool = new WorkerPool({ enableWorkers: false });
369
-
370
- const resolve = jest.fn();
371
- const reject = jest.fn();
372
-
373
- pool.pendingTasks.set(1, {
374
- id: 1,
375
- command: 'test',
376
- resolve,
377
- reject,
378
- timeout: null,
379
- });
380
-
381
- const mockWorker = { id: 1, isBusy: true };
382
-
383
- pool.handleWorkerMessage(mockWorker, {
384
- id: 1,
385
- success: true,
386
- data: { result: 'test-data' },
387
- });
388
-
389
- expect(resolve).toHaveBeenCalledWith({ result: 'test-data' });
390
- expect(mockWorker.isBusy).toBe(false);
391
- expect(pool.pendingTasks.has(1)).toBe(false);
392
- });
393
-
394
- test('should handle failed task completion', async () => {
395
- pool = new WorkerPool({ enableWorkers: false });
396
-
397
- const resolve = jest.fn();
398
- const reject = jest.fn();
399
-
400
- pool.pendingTasks.set(1, {
401
- id: 1,
402
- command: 'test',
403
- resolve,
404
- reject,
405
- timeout: null,
406
- });
407
-
408
- const mockWorker = { id: 1, isBusy: true };
409
-
410
- pool.handleWorkerMessage(mockWorker, {
411
- id: 1,
412
- success: false,
413
- error: 'Task failed',
414
- stack: 'Error: Task failed\n at test',
415
- });
416
-
417
- expect(reject).toHaveBeenCalled();
418
- const errorArg = reject.mock.calls[0][0];
419
- expect(errorArg.message).toBe('Task failed');
420
- expect(errorArg.stack).toBe('Error: Task failed\n at test');
421
- expect(mockWorker.isBusy).toBe(false);
422
- });
423
-
424
- test('should clear timeout on task completion', async () => {
425
- pool = new WorkerPool({ enableWorkers: false });
426
-
427
- const timeout = setTimeout(() => {}, 10000);
428
- const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
429
-
430
- const resolve = jest.fn();
431
-
432
- pool.pendingTasks.set(1, {
433
- id: 1,
434
- command: 'test',
435
- resolve,
436
- reject: jest.fn(),
437
- timeout,
438
- });
439
-
440
- const mockWorker = { id: 1, isBusy: true };
441
-
442
- pool.handleWorkerMessage(mockWorker, {
443
- id: 1,
444
- success: true,
445
- data: { result: 'test' },
446
- });
447
-
448
- expect(clearTimeoutSpy).toHaveBeenCalledWith(timeout);
449
-
450
- clearTimeoutSpy.mockRestore();
451
- });
452
-
453
- test('should ignore messages for unknown task IDs', async () => {
454
- pool = new WorkerPool({ enableWorkers: false });
455
-
456
- const mockWorker = { id: 1, isBusy: true };
457
-
458
- // Should not throw
459
- pool.handleWorkerMessage(mockWorker, {
460
- id: 999,
461
- success: true,
462
- data: { result: 'test' },
463
- });
464
-
465
- // Worker should remain busy since task wasn't found
466
- expect(mockWorker.isBusy).toBe(true);
467
- });
468
-
469
- test('should handle failed task with default error message', async () => {
470
- pool = new WorkerPool({ enableWorkers: false });
471
-
472
- const reject = jest.fn();
473
-
474
- pool.pendingTasks.set(1, {
475
- id: 1,
476
- command: 'test',
477
- resolve: jest.fn(),
478
- reject,
479
- timeout: null,
480
- });
481
-
482
- const mockWorker = { id: 1, isBusy: true };
483
-
484
- pool.handleWorkerMessage(mockWorker, {
485
- id: 1,
486
- success: false,
487
- // No error message provided
488
- });
489
-
490
- expect(reject).toHaveBeenCalled();
491
- const errorArg = reject.mock.calls[0][0];
492
- expect(errorArg.message).toBe('Worker task failed');
493
- });
494
- });
495
-
496
- describe('execute method', () => {
497
- test('should not set up timeout in fallback mode', async () => {
498
- pool = new WorkerPool({
499
- enableWorkers: false,
500
- taskTimeout: 5000,
501
- });
502
-
503
- // Mock setTimeout to verify it's not called in fallback mode
504
- const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
505
-
506
- await pool.execute('currentLoad');
507
-
508
- // In fallback mode, setTimeout should not be called
509
- expect(setTimeoutSpy).not.toHaveBeenCalled();
510
-
511
- setTimeoutSpy.mockRestore();
512
- });
513
-
514
- test('should use direct execution in fallback mode', async () => {
515
- pool = new WorkerPool({ enableWorkers: false });
516
-
517
- const result = await pool.execute('currentLoad');
518
-
519
- expect(result).toEqual(mockSiData.currentLoad);
520
- });
521
- });
522
- });