claw-dashboard 2.1.0 → 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 (90) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/API.md +1 -2
  3. package/index.js +32 -42
  4. package/package.json +23 -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/src/widgets/builtin-widgets.js +0 -94
  14. package/src/widgets/index.js +0 -1
  15. package/.c8rc.json +0 -38
  16. package/.dockerignore +0 -68
  17. package/.github/dependabot.yml +0 -45
  18. package/.github/pull_request_template.md +0 -39
  19. package/.github/workflows/ci.yml +0 -147
  20. package/.github/workflows/release.yml +0 -40
  21. package/.github/workflows/security.yml +0 -84
  22. package/.husky/pre-commit +0 -1
  23. package/.planning/codebase/ARCHITECTURE.md +0 -206
  24. package/.planning/codebase/INTEGRATIONS.md +0 -150
  25. package/.planning/codebase/STACK.md +0 -122
  26. package/.planning/codebase/STRUCTURE.md +0 -201
  27. package/CONTRIBUTING.md +0 -378
  28. package/Dockerfile +0 -54
  29. package/FEATURES.md +0 -307
  30. package/TODO.md +0 -28
  31. package/ai.openclaw.dashboard.plist +0 -35
  32. package/build-cjs.js +0 -127
  33. package/cjs-shim.js +0 -13
  34. package/dist/clawdash +0 -1729
  35. package/dist/clawdash.meta.json +0 -2236
  36. package/dist/widgets.cjs +0 -6511
  37. package/docker-compose.yml +0 -67
  38. package/esbuild.config.js +0 -158
  39. package/eslint.config.js +0 -56
  40. package/examples/plugins/README.md +0 -122
  41. package/examples/plugins/api-status/index.js +0 -294
  42. package/examples/plugins/api-status/plugin.json +0 -19
  43. package/examples/plugins/hello-world/index.js +0 -104
  44. package/examples/plugins/hello-world/plugin.json +0 -15
  45. package/examples/plugins/system-metrics-chart/index.js +0 -339
  46. package/examples/plugins/system-metrics-chart/plugin.json +0 -18
  47. package/examples/plugins/weather-widget/index.js +0 -136
  48. package/examples/plugins/weather-widget/plugin.json +0 -19
  49. package/index.cjs +0 -23005
  50. package/jest.config.js +0 -11
  51. package/scripts/release.js +0 -595
  52. package/src/database.js +0 -734
  53. package/tests/alerts.test.js +0 -437
  54. package/tests/auto-save.test.js +0 -529
  55. package/tests/cache.test.js +0 -317
  56. package/tests/cli.test.js +0 -351
  57. package/tests/command-palette.test.js +0 -320
  58. package/tests/config-processor.test.js +0 -452
  59. package/tests/config-validator.test.js +0 -710
  60. package/tests/config-watcher.test.js +0 -594
  61. package/tests/config.test.js +0 -755
  62. package/tests/database.test.js +0 -438
  63. package/tests/errors.test.js +0 -189
  64. package/tests/example-plugins.test.js +0 -624
  65. package/tests/integration.test.js +0 -1321
  66. package/tests/loading-states.test.js +0 -300
  67. package/tests/manifest-validation-on-load.test.js +0 -671
  68. package/tests/performance-monitor.test.js +0 -190
  69. package/tests/plugin-api-rate-limit.test.js +0 -302
  70. package/tests/plugin-errors.test.js +0 -311
  71. package/tests/plugin-lifecycle-e2e.test.js +0 -1036
  72. package/tests/plugin-reload.test.js +0 -764
  73. package/tests/rate-limiter.test.js +0 -539
  74. package/tests/retry.test.js +0 -308
  75. package/tests/security.test.js +0 -411
  76. package/tests/settings-modal.test.js +0 -226
  77. package/tests/theme-selector.test.js +0 -57
  78. package/tests/utils.js +0 -242
  79. package/tests/utils.test.js +0 -317
  80. package/tests/validate-plugin-cli.test.js +0 -359
  81. package/tests/validation.test.js +0 -837
  82. package/tests/web-server.test.js +0 -646
  83. package/tests/widget-arrange-mode.test.js +0 -183
  84. package/tests/widget-config-hot-reload.test.js +0 -465
  85. package/tests/widget-dependency.test.js +0 -591
  86. package/tests/widget-error-boundary.test.js +0 -749
  87. package/tests/widget-error-isolation.test.js +0 -469
  88. package/tests/widget-integration.test.js +0 -1147
  89. package/tests/widget-refresh-intervals.test.js +0 -284
  90. package/tests/worker-pool.test.js +0 -522
@@ -1,764 +0,0 @@
1
- /**
2
- * Tests for plugin-reload module
3
- * Hot-reload functionality for plugin development
4
- */
5
-
6
- import { jest } from '@jest/globals';
7
- import {
8
- PluginReloadManager,
9
- createPluginReloadManager,
10
- getPluginReloadManager,
11
- } from '../src/plugin-reload.js';
12
-
13
- import { WidgetLoader } from '../src/widgets/widget-loader.js';
14
- import { ConfigWatcher } from '../src/config-watcher.js';
15
-
16
- import { writeFileSync, mkdirSync, unlinkSync, rmdirSync, existsSync, readdirSync, readFileSync } from 'fs';
17
- import { join } from 'path';
18
- import { mkdtempSync } from 'fs';
19
- import { tmpdir } from 'os';
20
-
21
- // Mock logger to avoid noise in tests
22
- jest.mock('../src/logger.js', () => ({
23
- __esModule: true,
24
- default: {
25
- info: jest.fn(),
26
- warn: jest.fn(),
27
- error: jest.fn(),
28
- debug: jest.fn(),
29
- },
30
- }));
31
-
32
- // Helper to create a test plugin
33
- function createTestPlugin(pluginsDir, id, version = '1.0.0') {
34
- const pluginDir = join(pluginsDir, id);
35
- mkdirSync(pluginDir, { recursive: true });
36
-
37
- const manifest = {
38
- id,
39
- name: `Test Plugin ${id}`,
40
- version,
41
- type: 'widget',
42
- description: 'Test plugin for hot-reload',
43
- author: 'Test',
44
- };
45
-
46
- writeFileSync(join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2));
47
-
48
- const code = `
49
- export default class TestWidget {
50
- constructor(config) {
51
- this.config = config;
52
- this.version = '${version}';
53
- }
54
- render() { return 'render-${version}'; }
55
- getData() { return { version: '${version}' }; }
56
- destroy() {}
57
- }
58
- `;
59
-
60
- writeFileSync(join(pluginDir, 'index.js'), code);
61
-
62
- return { pluginDir, manifest };
63
- }
64
-
65
- describe('plugin-reload', () => {
66
- let testDir;
67
- let pluginsDir;
68
- let widgetLoader;
69
-
70
- beforeEach(() => {
71
- // Create temporary directories
72
- testDir = mkdtempSync(join(tmpdir(), 'plugin-reload-test-'));
73
- pluginsDir = join(testDir, 'plugins');
74
- mkdirSync(pluginsDir, { recursive: true });
75
-
76
- // Create widget loader
77
- widgetLoader = new WidgetLoader({
78
- pluginsDir,
79
- });
80
- });
81
-
82
- afterEach(async () => {
83
- // Clean up
84
- try {
85
- if (existsSync(testDir)) {
86
- const rmDir = (dir) => {
87
- const files = readdirSync(dir, { withFileTypes: true });
88
- for (const file of files) {
89
- const fullPath = join(dir, file.name);
90
- if (file.isDirectory()) {
91
- rmDir(fullPath);
92
- } else {
93
- try { unlinkSync(fullPath); } catch (e) {}
94
- }
95
- }
96
- try { rmdirSync(dir); } catch (e) {}
97
- };
98
- rmDir(testDir);
99
- }
100
- } catch (e) {
101
- // Ignore cleanup errors
102
- }
103
- });
104
-
105
- describe('PluginReloadManager constructor', () => {
106
- test('should create instance with default options', () => {
107
- const manager = new PluginReloadManager();
108
-
109
- expect(manager.options.debounceMs).toBe(300);
110
- expect(manager.options.autoReload).toBe(true);
111
- expect(manager.options.showNotifications).toBe(true);
112
- expect(manager.isRunning).toBe(false);
113
- expect(manager.watchedPlugins.size).toBe(0);
114
- expect(manager.widgetLoader).toBeNull();
115
- });
116
-
117
- test('should merge custom options with defaults', () => {
118
- const manager = new PluginReloadManager({
119
- debounceMs: 500,
120
- autoReload: false,
121
- customOption: 'test',
122
- });
123
-
124
- expect(manager.options.debounceMs).toBe(500);
125
- expect(manager.options.autoReload).toBe(false);
126
- expect(manager.options.showNotifications).toBe(true); // default preserved
127
- expect(manager.options.customOption).toBe('test');
128
- });
129
-
130
- test('should accept widget loader in constructor', () => {
131
- const manager = new PluginReloadManager({ widgetLoader });
132
- expect(manager.widgetLoader).toBe(widgetLoader);
133
- });
134
- });
135
-
136
- describe('setWidgetLoader', () => {
137
- test('should set widget loader', () => {
138
- const manager = new PluginReloadManager();
139
- expect(manager.widgetLoader).toBeNull();
140
-
141
- manager.setWidgetLoader(widgetLoader);
142
- expect(manager.widgetLoader).toBe(widgetLoader);
143
- });
144
- });
145
-
146
- describe('start', () => {
147
- test('should fail if no widget loader is set', () => {
148
- const manager = new PluginReloadManager();
149
- const result = manager.start();
150
-
151
- expect(result).toBe(false);
152
- expect(manager.isRunning).toBe(false);
153
- });
154
-
155
- test('should start watching plugins directory', () => {
156
- // Create a test plugin
157
- createTestPlugin(pluginsDir, 'test-plugin');
158
-
159
- const manager = new PluginReloadManager({
160
- widgetLoader,
161
- pluginsDir,
162
- });
163
-
164
- const result = manager.start();
165
-
166
- expect(result).toBe(true);
167
- expect(manager.isRunning).toBe(true);
168
- expect(manager.watcher).toBeInstanceOf(ConfigWatcher);
169
- expect(manager.watchedPlugins.has('test-plugin')).toBe(true);
170
-
171
- manager.stop();
172
- });
173
-
174
- test('should return true if already running', () => {
175
- createTestPlugin(pluginsDir, 'test-plugin');
176
-
177
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
178
- manager.start();
179
-
180
- const result = manager.start();
181
-
182
- expect(result).toBe(true);
183
- expect(manager.isRunning).toBe(true);
184
-
185
- manager.stop();
186
- });
187
-
188
- test('should handle missing plugins directory gracefully', () => {
189
- const nonExistentDir = join(testDir, 'non-existent', 'plugins');
190
-
191
- const manager = new PluginReloadManager({
192
- widgetLoader,
193
- pluginsDir: nonExistentDir,
194
- });
195
-
196
- const result = manager.start();
197
-
198
- expect(result).toBe(true); // Still starts, just logs warning
199
- expect(manager.isRunning).toBe(true);
200
-
201
- manager.stop();
202
- });
203
- });
204
-
205
- describe('stop', () => {
206
- test('should stop watching and clean up', () => {
207
- createTestPlugin(pluginsDir, 'test-plugin');
208
-
209
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
210
- manager.start();
211
-
212
- expect(manager.isRunning).toBe(true);
213
- expect(manager.watchedPlugins.size).toBeGreaterThan(0);
214
-
215
- manager.stop();
216
-
217
- expect(manager.isRunning).toBe(false);
218
- expect(manager.watchedPlugins.size).toBe(0);
219
- expect(manager.watcher).toBeNull();
220
- });
221
-
222
- test('should handle stop when not running', () => {
223
- const manager = new PluginReloadManager();
224
-
225
- expect(() => manager.stop()).not.toThrow();
226
- expect(manager.isRunning).toBe(false);
227
- });
228
- });
229
-
230
- describe('addHook', () => {
231
- test('should add hooks for valid types', () => {
232
- const manager = new PluginReloadManager();
233
- const handler = jest.fn();
234
-
235
- manager.addHook('beforeReload', handler);
236
- manager.addHook('afterReload', handler);
237
- manager.addHook('onError', handler);
238
-
239
- expect(manager.hooks.beforeReload).toContain(handler);
240
- expect(manager.hooks.afterReload).toContain(handler);
241
- expect(manager.hooks.onError).toContain(handler);
242
- });
243
-
244
- test('should throw for unknown hook types', () => {
245
- const manager = new PluginReloadManager();
246
-
247
- expect(() => manager.addHook('unknownHook', jest.fn())).toThrow('Unknown hook type');
248
- });
249
- });
250
-
251
- describe('reloadPlugin', () => {
252
- test('should call widgetLoader.unregister for existing plugin', async () => {
253
- createTestPlugin(pluginsDir, 'test-plugin', '1.0.0');
254
-
255
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
256
- manager.start();
257
-
258
- // Mock the widgetLoader methods
259
- const unregisterSpy = jest.spyOn(widgetLoader, 'unregister').mockResolvedValue(true);
260
- const loadPluginSpy = jest.spyOn(widgetLoader, 'loadPlugin').mockResolvedValue('test-plugin');
261
- jest.spyOn(widgetLoader, 'isLoaded').mockReturnValue(true);
262
- jest.spyOn(widgetLoader.widgetRegistry, 'has').mockReturnValue(true);
263
-
264
- const { pluginDir } = createTestPlugin(pluginsDir, 'test-plugin');
265
-
266
- const result = await manager.reloadPlugin(
267
- 'test-plugin',
268
- pluginDir,
269
- join(pluginDir, 'plugin.json'),
270
- join(pluginDir, 'index.js')
271
- );
272
-
273
- expect(result.success).toBe(true);
274
- expect(unregisterSpy).toHaveBeenCalledWith('test-plugin');
275
- expect(loadPluginSpy).toHaveBeenCalled();
276
-
277
- manager.stop();
278
- });
279
-
280
- test('should load new plugin when not previously registered', async () => {
281
- createTestPlugin(pluginsDir, 'new-plugin');
282
-
283
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
284
- manager.start();
285
-
286
- // Mock widgetLoader to simulate new plugin
287
- jest.spyOn(widgetLoader, 'isLoaded').mockReturnValue(false);
288
- jest.spyOn(widgetLoader.widgetRegistry, 'has').mockReturnValue(false);
289
- const loadPluginSpy = jest.spyOn(widgetLoader, 'loadPlugin').mockResolvedValue('new-plugin');
290
-
291
- const pluginDir = join(pluginsDir, 'new-plugin');
292
-
293
- const result = await manager.reloadPlugin(
294
- 'new-plugin',
295
- pluginDir,
296
- join(pluginDir, 'plugin.json'),
297
- join(pluginDir, 'index.js')
298
- );
299
-
300
- expect(result.success).toBe(true);
301
- expect(result.isNew).toBe(true);
302
- expect(loadPluginSpy).toHaveBeenCalled();
303
-
304
- manager.stop();
305
- });
306
-
307
- test('should handle errors from widgetLoader.loadPlugin', async () => {
308
- createTestPlugin(pluginsDir, 'error-plugin');
309
-
310
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
311
- manager.start();
312
-
313
- // Mock widgetLoader to throw error
314
- jest.spyOn(widgetLoader.widgetRegistry, 'has').mockReturnValue(false);
315
- jest.spyOn(widgetLoader, 'loadPlugin').mockRejectedValue(new Error('Failed to load plugin'));
316
-
317
- const pluginDir = join(pluginsDir, 'error-plugin');
318
-
319
- const result = await manager.reloadPlugin(
320
- 'error-plugin',
321
- pluginDir,
322
- join(pluginDir, 'plugin.json'),
323
- join(pluginDir, 'index.js')
324
- );
325
-
326
- expect(result.success).toBe(false);
327
- expect(result.error).toBe('Failed to load plugin');
328
-
329
- manager.stop();
330
- });
331
-
332
- test('should call beforeReload and afterReload hooks', async () => {
333
- createTestPlugin(pluginsDir, 'test-plugin');
334
-
335
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
336
- const beforeHook = jest.fn();
337
- const afterHook = jest.fn();
338
-
339
- manager.addHook('beforeReload', beforeHook);
340
- manager.addHook('afterReload', afterHook);
341
- manager.start();
342
-
343
- // Mock widgetLoader
344
- jest.spyOn(widgetLoader.widgetRegistry, 'has').mockReturnValue(false);
345
- jest.spyOn(widgetLoader, 'loadPlugin').mockResolvedValue('test-plugin');
346
-
347
- const pluginDir = join(pluginsDir, 'test-plugin');
348
-
349
- await manager.reloadPlugin(
350
- 'test-plugin',
351
- pluginDir,
352
- join(pluginDir, 'plugin.json'),
353
- join(pluginDir, 'index.js')
354
- );
355
-
356
- expect(beforeHook).toHaveBeenCalled();
357
- expect(afterHook).toHaveBeenCalled();
358
- expect(beforeHook.mock.calls[0][0].id).toBe('test-plugin');
359
- expect(afterHook.mock.calls[0][0].id).toBe('test-plugin');
360
- expect(afterHook.mock.calls[0][0].loadTime).toBeDefined();
361
-
362
- manager.stop();
363
- });
364
-
365
- test('should call onError hook when reload fails', async () => {
366
- createTestPlugin(pluginsDir, 'bad-plugin');
367
-
368
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
369
- const errorHook = jest.fn();
370
-
371
- manager.addHook('onError', errorHook);
372
- manager.start();
373
-
374
- // Mock widgetLoader to throw
375
- jest.spyOn(widgetLoader.widgetRegistry, 'has').mockReturnValue(false);
376
- jest.spyOn(widgetLoader, 'loadPlugin').mockRejectedValue(new Error('Syntax error'));
377
-
378
- const pluginDir = join(pluginsDir, 'bad-plugin');
379
-
380
- await manager.reloadPlugin(
381
- 'bad-plugin',
382
- pluginDir,
383
- join(pluginDir, 'plugin.json'),
384
- join(pluginDir, 'index.js')
385
- );
386
-
387
- expect(errorHook).toHaveBeenCalled();
388
- expect(errorHook.mock.calls[0][0].error).toBeDefined();
389
- expect(errorHook.mock.calls[0][0].type).toBe('reload');
390
-
391
- manager.stop();
392
- });
393
- });
394
-
395
- describe('reload', () => {
396
- test('should reload a watched plugin by id', async () => {
397
- createTestPlugin(pluginsDir, 'test-plugin');
398
-
399
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
400
- manager.start();
401
-
402
- // Mock the reloadPlugin method
403
- const reloadSpy = jest.spyOn(manager, 'reloadPlugin').mockResolvedValue({
404
- success: true,
405
- id: 'test-plugin',
406
- });
407
-
408
- const result = await manager.reload('test-plugin');
409
-
410
- expect(result.success).toBe(true);
411
- expect(result.id).toBe('test-plugin');
412
- expect(reloadSpy).toHaveBeenCalled();
413
-
414
- manager.stop();
415
- });
416
-
417
- test('should throw if plugin is not being watched', async () => {
418
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
419
-
420
- await expect(manager.reload('non-existent')).rejects.toThrow('not being watched');
421
- });
422
- });
423
-
424
- describe('addPlugin', () => {
425
- test('should add a new plugin to watch list', async () => {
426
- // Create manager first
427
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
428
- manager.start();
429
-
430
- // Create plugin after manager started
431
- const { pluginDir } = createTestPlugin(pluginsDir, 'added-plugin');
432
-
433
- expect(manager.isWatching('added-plugin')).toBe(false);
434
-
435
- await manager.addPlugin(pluginDir);
436
-
437
- expect(manager.isWatching('added-plugin')).toBe(true);
438
-
439
- manager.stop();
440
- });
441
-
442
- test('should throw if plugin manifest not found', async () => {
443
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
444
- const fakeDir = join(pluginsDir, 'fake-plugin');
445
- mkdirSync(fakeDir);
446
-
447
- await expect(manager.addPlugin(fakeDir)).rejects.toThrow('manifest not found');
448
- });
449
- });
450
-
451
- describe('removePlugin', () => {
452
- test('should remove a plugin from watch list', () => {
453
- createTestPlugin(pluginsDir, 'test-plugin');
454
-
455
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
456
- manager.start();
457
-
458
- expect(manager.isWatching('test-plugin')).toBe(true);
459
-
460
- const result = manager.removePlugin('test-plugin');
461
-
462
- expect(result).toBe(true);
463
- expect(manager.isWatching('test-plugin')).toBe(false);
464
-
465
- manager.stop();
466
- });
467
-
468
- test('should return false if plugin not being watched', () => {
469
- const manager = new PluginReloadManager();
470
-
471
- const result = manager.removePlugin('non-existent');
472
-
473
- expect(result).toBe(false);
474
- });
475
- });
476
-
477
- describe('getWatchedPlugins', () => {
478
- test('should return list of watched plugin IDs', () => {
479
- createTestPlugin(pluginsDir, 'plugin-a');
480
- createTestPlugin(pluginsDir, 'plugin-b');
481
-
482
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
483
- manager.start();
484
-
485
- const plugins = manager.getWatchedPlugins();
486
-
487
- expect(plugins).toContain('plugin-a');
488
- expect(plugins).toContain('plugin-b');
489
- expect(plugins).toHaveLength(2);
490
-
491
- manager.stop();
492
- });
493
-
494
- test('should return empty array when no plugins watched', () => {
495
- const manager = new PluginReloadManager();
496
-
497
- const plugins = manager.getWatchedPlugins();
498
-
499
- expect(plugins).toEqual([]);
500
- });
501
- });
502
-
503
- describe('isWatching', () => {
504
- test('should return true for watched plugin', () => {
505
- createTestPlugin(pluginsDir, 'test-plugin');
506
-
507
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
508
- manager.start();
509
-
510
- expect(manager.isWatching('test-plugin')).toBe(true);
511
-
512
- manager.stop();
513
- });
514
-
515
- test('should return false for unwatched plugin', () => {
516
- createTestPlugin(pluginsDir, 'test-plugin');
517
-
518
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
519
- manager.start();
520
-
521
- expect(manager.isWatching('non-existent')).toBe(false);
522
-
523
- manager.stop();
524
- });
525
- });
526
-
527
- describe('getStats', () => {
528
- test('should return stats object', () => {
529
- createTestPlugin(pluginsDir, 'test-plugin');
530
-
531
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
532
-
533
- const stats = manager.getStats();
534
-
535
- expect(stats).toEqual({
536
- isRunning: false,
537
- watchedPlugins: 0,
538
- watchedFiles: 0,
539
- autoReload: true,
540
- });
541
- });
542
-
543
- test('should reflect running state', () => {
544
- createTestPlugin(pluginsDir, 'test-plugin');
545
-
546
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
547
- manager.start();
548
-
549
- const stats = manager.getStats();
550
-
551
- expect(stats.isRunning).toBe(true);
552
- expect(stats.watchedPlugins).toBe(1);
553
- expect(stats.watchedFiles).toBeGreaterThan(0);
554
-
555
- manager.stop();
556
- });
557
- });
558
-
559
- describe('file change detection', () => {
560
- test('should detect plugin.json changes and reload', async () => {
561
- const { pluginDir } = createTestPlugin(pluginsDir, 'test-plugin', '1.0.0');
562
-
563
- const manager = new PluginReloadManager({
564
- widgetLoader,
565
- pluginsDir,
566
- debounceMs: 50,
567
- });
568
-
569
- const reloadSpy = jest.spyOn(manager, 'reloadPlugin').mockResolvedValue({
570
- success: true,
571
- id: 'test-plugin',
572
- });
573
-
574
- manager.start();
575
-
576
- // Simulate file change by triggering reload handler
577
- const manifestPath = join(pluginDir, 'plugin.json');
578
- await manager._handleFileChange(manifestPath);
579
-
580
- expect(reloadSpy).toHaveBeenCalled();
581
-
582
- manager.stop();
583
- });
584
-
585
- test('should detect index.js changes and reload', async () => {
586
- const { pluginDir } = createTestPlugin(pluginsDir, 'test-plugin', '1.0.0');
587
-
588
- const manager = new PluginReloadManager({
589
- widgetLoader,
590
- pluginsDir,
591
- debounceMs: 50,
592
- });
593
-
594
- const reloadSpy = jest.spyOn(manager, 'reloadPlugin').mockResolvedValue({
595
- success: true,
596
- id: 'test-plugin',
597
- });
598
-
599
- manager.start();
600
-
601
- const indexPath = join(pluginDir, 'index.js');
602
- await manager._handleFileChange(indexPath);
603
-
604
- expect(reloadSpy).toHaveBeenCalled();
605
-
606
- manager.stop();
607
- });
608
-
609
- test('should ignore changes to non-plugin files', async () => {
610
- createTestPlugin(pluginsDir, 'test-plugin');
611
-
612
- const manager = new PluginReloadManager({
613
- widgetLoader,
614
- pluginsDir,
615
- debounceMs: 50,
616
- });
617
-
618
- const reloadSpy = jest.spyOn(manager, 'reloadPlugin').mockResolvedValue({
619
- success: true,
620
- id: 'test-plugin',
621
- });
622
-
623
- manager.start();
624
-
625
- // Try to handle change for unknown file
626
- await manager._handleFileChange('/random/path/file.js');
627
-
628
- expect(reloadSpy).not.toHaveBeenCalled();
629
-
630
- manager.stop();
631
- });
632
-
633
- test('should skip reload when autoReload is disabled', async () => {
634
- const { pluginDir } = createTestPlugin(pluginsDir, 'test-plugin', '1.0.0');
635
-
636
- const manager = new PluginReloadManager({
637
- widgetLoader,
638
- pluginsDir,
639
- debounceMs: 50,
640
- autoReload: false,
641
- });
642
-
643
- const reloadSpy = jest.spyOn(manager, 'reloadPlugin').mockResolvedValue({
644
- success: true,
645
- id: 'test-plugin',
646
- });
647
-
648
- manager.start();
649
-
650
- const indexPath = join(pluginDir, 'index.js');
651
- await manager._handleFileChange(indexPath);
652
-
653
- expect(reloadSpy).not.toHaveBeenCalled();
654
-
655
- manager.stop();
656
- });
657
- });
658
-
659
- describe('createPluginReloadManager', () => {
660
- test('should create a new manager instance', () => {
661
- const manager = createPluginReloadManager({ debounceMs: 500 });
662
-
663
- expect(manager).toBeInstanceOf(PluginReloadManager);
664
- expect(manager.options.debounceMs).toBe(500);
665
- });
666
- });
667
-
668
- describe('getPluginReloadManager', () => {
669
- test('should return singleton instance', () => {
670
- const manager1 = getPluginReloadManager();
671
- const manager2 = getPluginReloadManager();
672
-
673
- expect(manager1).toBe(manager2);
674
- });
675
-
676
- test('should create instance with options if none exists', () => {
677
- // Note: This may interact with other tests
678
- const manager = getPluginReloadManager({ debounceMs: 400 });
679
-
680
- expect(manager).toBeInstanceOf(PluginReloadManager);
681
- });
682
- });
683
-
684
- describe('_findPluginByPath', () => {
685
- test('should find plugin by manifest path', () => {
686
- createTestPlugin(pluginsDir, 'test-plugin');
687
-
688
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
689
- manager.start();
690
-
691
- const pluginDir = join(pluginsDir, 'test-plugin');
692
- const manifestPath = join(pluginDir, 'plugin.json');
693
-
694
- const result = manager._findPluginByPath(manifestPath);
695
-
696
- expect(result).not.toBeNull();
697
- expect(result.id).toBe('test-plugin');
698
-
699
- manager.stop();
700
- });
701
-
702
- test('should find plugin by index.js path', () => {
703
- createTestPlugin(pluginsDir, 'test-plugin');
704
-
705
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
706
- manager.start();
707
-
708
- const pluginDir = join(pluginsDir, 'test-plugin');
709
- const indexPath = join(pluginDir, 'index.js');
710
-
711
- const result = manager._findPluginByPath(indexPath);
712
-
713
- expect(result).not.toBeNull();
714
- expect(result.id).toBe('test-plugin');
715
-
716
- manager.stop();
717
- });
718
-
719
- test('should return null for unknown path', () => {
720
- createTestPlugin(pluginsDir, 'test-plugin');
721
-
722
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
723
- manager.start();
724
-
725
- const result = manager._findPluginByPath('/unknown/path/file.js');
726
-
727
- expect(result).toBeNull();
728
-
729
- manager.stop();
730
- });
731
- });
732
-
733
- describe('_clearModuleCache', () => {
734
- test('should not throw for valid path', () => {
735
- const manager = new PluginReloadManager();
736
-
737
- expect(() => {
738
- manager._clearModuleCache('/some/path/file.js');
739
- }).not.toThrow();
740
- });
741
- });
742
-
743
- describe('_updateWatchedFiles', () => {
744
- test('should update watched plugin info', async () => {
745
- createTestPlugin(pluginsDir, 'test-plugin');
746
-
747
- const manager = new PluginReloadManager({ widgetLoader, pluginsDir });
748
- manager.start();
749
-
750
- const pluginDir = join(pluginsDir, 'test-plugin');
751
- const manifestPath = join(pluginDir, 'plugin.json');
752
- const indexPath = join(pluginDir, 'index.js');
753
-
754
- // Update with slightly different info
755
- await manager._updateWatchedFiles('test-plugin', pluginDir, manifestPath, indexPath);
756
-
757
- const info = manager.watchedPlugins.get('test-plugin');
758
- expect(info).toBeDefined();
759
- expect(info.pluginPath).toBe(pluginDir);
760
-
761
- manager.stop();
762
- });
763
- });
764
- });