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,591 +0,0 @@
1
- /**
2
- * Widget Dependency Resolver Tests
3
- * Tests for dependency resolution, topological sorting, and constraint checking
4
- */
5
-
6
- import { jest } from '@jest/globals';
7
- import {
8
- parseDependency,
9
- parseDependencies,
10
- buildDependencyGraph,
11
- detectCircularDependency,
12
- satisfiesVersion,
13
- checkVersionConstraints,
14
- findMissingDependencies,
15
- topologicalSort,
16
- resolveDependencies,
17
- getAllDependencies,
18
- getAllDependents,
19
- validateWidgetDependencies,
20
- } from '../src/widgets/dependency-resolver.js';
21
-
22
- describe('parseDependency', () => {
23
- test('should parse simple string dependency', () => {
24
- const dep = parseDependency('my-widget');
25
- expect(dep).toEqual({ id: 'my-widget', optional: false });
26
- });
27
-
28
- test('should parse object dependency with optional flag', () => {
29
- const dep = parseDependency({ id: 'my-widget', optional: true });
30
- expect(dep).toEqual({ id: 'my-widget', optional: true });
31
- });
32
-
33
- test('should parse object dependency with version constraint', () => {
34
- const dep = parseDependency({ id: 'my-widget', version: '>=1.0.0' });
35
- expect(dep).toEqual({ id: 'my-widget', optional: false, version: '>=1.0.0' });
36
- });
37
-
38
- test('should parse full dependency object', () => {
39
- const dep = parseDependency({ id: 'my-widget', optional: true, version: '^2.0.0' });
40
- expect(dep).toEqual({ id: 'my-widget', optional: true, version: '^2.0.0' });
41
- });
42
-
43
- test('should throw for invalid dependency types', () => {
44
- expect(() => parseDependency(123)).toThrow(/must be a string or an object/);
45
- expect(() => parseDependency(null)).toThrow(/must be a string or an object/);
46
- expect(() => parseDependency(undefined)).toThrow(/must be a string or an object/);
47
- });
48
-
49
- test('should throw for object without id', () => {
50
- expect(() => parseDependency({ optional: true })).toThrow(/must have a string "id" property/);
51
- expect(() => parseDependency({ id: 123 })).toThrow(/must have a string "id" property/);
52
- });
53
- });
54
-
55
- describe('parseDependencies', () => {
56
- test('should return empty array for undefined dependencies', () => {
57
- expect(parseDependencies({})).toEqual([]);
58
- expect(parseDependencies({ dependencies: null })).toEqual([]);
59
- });
60
-
61
- test('should parse mixed dependency formats', () => {
62
- const metadata = {
63
- id: 'test-widget',
64
- dependencies: [
65
- 'simple-dep',
66
- { id: 'optional-dep', optional: true },
67
- { id: 'versioned-dep', version: '>=1.0.0' },
68
- ],
69
- };
70
-
71
- const deps = parseDependencies(metadata);
72
- expect(deps).toHaveLength(3);
73
- expect(deps[0]).toEqual({ id: 'simple-dep', optional: false });
74
- expect(deps[1]).toEqual({ id: 'optional-dep', optional: true });
75
- expect(deps[2]).toEqual({ id: 'versioned-dep', optional: false, version: '>=1.0.0' });
76
- });
77
-
78
- test('should skip invalid dependencies and log warning', () => {
79
- const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
80
-
81
- const metadata = {
82
- id: 'test-widget',
83
- dependencies: ['valid-dep', 123, null, 'another-valid'],
84
- };
85
-
86
- const deps = parseDependencies(metadata);
87
- expect(deps).toHaveLength(2);
88
- expect(deps[0].id).toBe('valid-dep');
89
- expect(deps[1].id).toBe('another-valid');
90
-
91
- consoleSpy.mockRestore();
92
- });
93
- });
94
-
95
- describe('buildDependencyGraph', () => {
96
- test('should build graph for widgets with no dependencies', () => {
97
- const registry = new Map([
98
- ['widget-a', { metadata: { id: 'widget-a', dependencies: [] } }],
99
- ['widget-b', { metadata: { id: 'widget-b' } }],
100
- ]);
101
-
102
- const graph = buildDependencyGraph(registry);
103
-
104
- expect(graph.size).toBe(2);
105
- expect(graph.get('widget-a').dependencies).toEqual([]);
106
- expect(graph.get('widget-b').dependencies).toEqual([]);
107
- });
108
-
109
- test('should build graph with dependencies', () => {
110
- const registry = new Map([
111
- ['base', { metadata: { id: 'base', dependencies: [] } }],
112
- ['child', { metadata: { id: 'child', dependencies: ['base'] } }],
113
- ['grandchild', { metadata: { id: 'grandchild', dependencies: ['child'] } }],
114
- ]);
115
-
116
- const graph = buildDependencyGraph(registry);
117
-
118
- expect(graph.get('child').inDegree).toBe(1);
119
- expect(graph.get('grandchild').inDegree).toBe(1);
120
- expect(graph.get('base').dependents).toContain('child');
121
- expect(graph.get('child').dependents).toContain('grandchild');
122
- });
123
-
124
- test('should handle widgets with multiple dependencies', () => {
125
- const registry = new Map([
126
- ['a', { metadata: { id: 'a' } }],
127
- ['b', { metadata: { id: 'b' } }],
128
- ['c', { metadata: { id: 'c', dependencies: ['a', 'b'] } }],
129
- ]);
130
-
131
- const graph = buildDependencyGraph(registry);
132
-
133
- expect(graph.get('c').inDegree).toBe(2);
134
- expect(graph.get('c').dependencies).toHaveLength(2);
135
- });
136
-
137
- test('should handle missing dependencies (not in registry)', () => {
138
- const registry = new Map([
139
- ['widget-a', { metadata: { id: 'widget-a', dependencies: ['missing'] } }],
140
- ]);
141
-
142
- const graph = buildDependencyGraph(registry);
143
-
144
- // Dependency listed but not in registry - inDegree stays 0 for missing dep
145
- expect(graph.get('widget-a').inDegree).toBe(0);
146
- expect(graph.get('widget-a').dependencies[0].id).toBe('missing');
147
- });
148
- });
149
-
150
- describe('detectCircularDependency', () => {
151
- test('should return null for acyclic graph', () => {
152
- const registry = new Map([
153
- ['a', { metadata: { id: 'a' } }],
154
- ['b', { metadata: { id: 'b', dependencies: ['a'] } }],
155
- ['c', { metadata: { id: 'c', dependencies: ['b'] } }],
156
- ]);
157
-
158
- const graph = buildDependencyGraph(registry);
159
- const cycle = detectCircularDependency(graph);
160
-
161
- expect(cycle).toBeNull();
162
- });
163
-
164
- test('should detect simple circular dependency', () => {
165
- const registry = new Map([
166
- ['a', { metadata: { id: 'a', dependencies: ['b'] } }],
167
- ['b', { metadata: { id: 'b', dependencies: ['a'] } }],
168
- ]);
169
-
170
- const graph = buildDependencyGraph(registry);
171
- const cycle = detectCircularDependency(graph);
172
-
173
- expect(cycle).not.toBeNull();
174
- expect(cycle).toContain('a');
175
- expect(cycle).toContain('b');
176
- });
177
-
178
- test('should detect complex circular dependency', () => {
179
- const registry = new Map([
180
- ['a', { metadata: { id: 'a', dependencies: ['b'] } }],
181
- ['b', { metadata: { id: 'b', dependencies: ['c'] } }],
182
- ['c', { metadata: { id: 'c', dependencies: ['a'] } }],
183
- ]);
184
-
185
- const graph = buildDependencyGraph(registry);
186
- const cycle = detectCircularDependency(graph);
187
-
188
- expect(cycle).not.toBeNull();
189
- expect(cycle).toContain('a');
190
- expect(cycle).toContain('b');
191
- expect(cycle).toContain('c');
192
- });
193
-
194
- test('should return null for empty graph', () => {
195
- const graph = new Map();
196
- const cycle = detectCircularDependency(graph);
197
- expect(cycle).toBeNull();
198
- });
199
- });
200
-
201
- describe('satisfiesVersion', () => {
202
- test('should return true for exact match', () => {
203
- expect(satisfiesVersion('1.0.0', '1.0.0')).toBe(true);
204
- expect(satisfiesVersion('2.5.3', '2.5.3')).toBe(true);
205
- });
206
-
207
- test('should return false for non-matching exact versions', () => {
208
- expect(satisfiesVersion('1.0.0', '2.0.0')).toBe(false);
209
- expect(satisfiesVersion('1.0.0', '1.1.0')).toBe(false);
210
- });
211
-
212
- test('should handle >= constraint', () => {
213
- expect(satisfiesVersion('1.0.0', '>=1.0.0')).toBe(true);
214
- expect(satisfiesVersion('2.0.0', '>=1.0.0')).toBe(true);
215
- expect(satisfiesVersion('0.9.0', '>=1.0.0')).toBe(false);
216
- });
217
-
218
- test('should handle ^ constraint (compatible major)', () => {
219
- expect(satisfiesVersion('1.0.0', '^1.0.0')).toBe(true);
220
- expect(satisfiesVersion('1.5.0', '^1.0.0')).toBe(true);
221
- expect(satisfiesVersion('2.0.0', '^1.0.0')).toBe(false);
222
- });
223
-
224
- test('should handle ~ constraint (approximately equivalent)', () => {
225
- expect(satisfiesVersion('1.0.0', '~1.0.0')).toBe(true);
226
- expect(satisfiesVersion('1.0.5', '~1.0.0')).toBe(true);
227
- expect(satisfiesVersion('1.1.0', '~1.0.0')).toBe(false);
228
- });
229
-
230
- test('should return true when no version or constraint provided', () => {
231
- expect(satisfiesVersion('1.0.0', null)).toBe(true);
232
- expect(satisfiesVersion(null, '1.0.0')).toBe(true);
233
- });
234
-
235
- test('should handle v prefix in version', () => {
236
- expect(satisfiesVersion('v1.0.0', '1.0.0')).toBe(true);
237
- expect(satisfiesVersion('1.0.0', 'v1.0.0')).toBe(true);
238
- });
239
- });
240
-
241
- describe('checkVersionConstraints', () => {
242
- test('should return null when all constraints satisfied', () => {
243
- const registry = new Map([
244
- ['dep', { metadata: { version: '2.0.0' } }],
245
- ['widget', { metadata: { dependencies: [{ id: 'dep', version: '^2.0.0' }] } }],
246
- ]);
247
-
248
- const graph = buildDependencyGraph(registry);
249
- const violations = checkVersionConstraints(graph, registry);
250
-
251
- expect(violations).toBeNull();
252
- });
253
-
254
- test('should detect version constraint violations', () => {
255
- const registry = new Map([
256
- ['dep', { metadata: { version: '1.0.0' } }],
257
- ['widget', { metadata: { dependencies: [{ id: 'dep', version: '>=2.0.0' }] } }],
258
- ]);
259
-
260
- const graph = buildDependencyGraph(registry);
261
- const violations = checkVersionConstraints(graph, registry);
262
-
263
- expect(violations).not.toBeNull();
264
- expect(violations.widget).toHaveLength(1);
265
- expect(violations.widget[0].dependency).toBe('dep');
266
- });
267
-
268
- test('should handle dependency with no version', () => {
269
- const registry = new Map([
270
- ['dep', { metadata: {} }],
271
- ['widget', { metadata: { dependencies: [{ id: 'dep', version: '>=1.0.0' }] } }],
272
- ]);
273
-
274
- const graph = buildDependencyGraph(registry);
275
- const violations = checkVersionConstraints(graph, registry);
276
-
277
- expect(violations).not.toBeNull();
278
- expect(violations.widget[0].reason).toContain('no version');
279
- });
280
- });
281
-
282
- describe('findMissingDependencies', () => {
283
- test('should return null when all dependencies present', () => {
284
- const registry = new Map([
285
- ['dep', { metadata: {} }],
286
- ['widget', { metadata: { dependencies: ['dep'] } }],
287
- ]);
288
-
289
- const graph = buildDependencyGraph(registry);
290
- const missing = findMissingDependencies(graph, registry);
291
-
292
- expect(missing).toBeNull();
293
- });
294
-
295
- test('should find missing required dependencies', () => {
296
- const registry = new Map([
297
- ['widget', { metadata: { dependencies: ['missing-dep'] } }],
298
- ]);
299
-
300
- const graph = buildDependencyGraph(registry);
301
- const missing = findMissingDependencies(graph, registry);
302
-
303
- expect(missing).toEqual({ widget: ['missing-dep'] });
304
- });
305
-
306
- test('should ignore optional dependencies that are missing', () => {
307
- const registry = new Map([
308
- ['widget', { metadata: { dependencies: [{ id: 'optional', optional: true }] } }],
309
- ]);
310
-
311
- const graph = buildDependencyGraph(registry);
312
- const missing = findMissingDependencies(graph, registry);
313
-
314
- expect(missing).toBeNull();
315
- });
316
- });
317
-
318
- describe('topologicalSort', () => {
319
- test('should sort widgets in dependency order', () => {
320
- const registry = new Map([
321
- ['c', { metadata: { id: 'c', dependencies: ['b'] } }],
322
- ['a', { metadata: { id: 'a' } }],
323
- ['b', { metadata: { id: 'b', dependencies: ['a'] } }],
324
- ]);
325
-
326
- const graph = buildDependencyGraph(registry);
327
- const order = topologicalSort(graph);
328
-
329
- expect(order).toEqual(['a', 'b', 'c']);
330
- });
331
-
332
- test('should handle independent widgets', () => {
333
- const registry = new Map([
334
- ['b', { metadata: { id: 'b' } }],
335
- ['a', { metadata: { id: 'a' } }],
336
- ['c', { metadata: { id: 'c' } }],
337
- ]);
338
-
339
- const graph = buildDependencyGraph(registry);
340
- const order = topologicalSort(graph);
341
-
342
- expect(order).toEqual(['a', 'b', 'c']); // Alphabetically sorted
343
- });
344
-
345
- test('should sort only specified target widgets', () => {
346
- const registry = new Map([
347
- ['base', { metadata: { id: 'base' } }],
348
- ['other', { metadata: { id: 'other' } }],
349
- ['child', { metadata: { id: 'child', dependencies: ['base'] } }],
350
- ]);
351
-
352
- const graph = buildDependencyGraph(registry);
353
- const order = topologicalSort(graph, ['child']);
354
-
355
- expect(order).toEqual(['base', 'child']);
356
- expect(order).not.toContain('other');
357
- });
358
-
359
- test('should return empty array for empty graph', () => {
360
- const graph = new Map();
361
- const order = topologicalSort(graph);
362
- expect(order).toEqual([]);
363
- });
364
- });
365
-
366
- describe('resolveDependencies', () => {
367
- test('should resolve dependencies successfully', () => {
368
- const registry = new Map([
369
- ['a', { metadata: {} }],
370
- ['b', { metadata: { dependencies: ['a'] } }],
371
- ]);
372
-
373
- const result = resolveDependencies(registry);
374
-
375
- expect(result.success).toBe(true);
376
- expect(result.order).toEqual(['a', 'b']);
377
- });
378
-
379
- test('should detect circular dependencies', () => {
380
- const registry = new Map([
381
- ['a', { metadata: { dependencies: ['b'] } }],
382
- ['b', { metadata: { dependencies: ['a'] } }],
383
- ]);
384
-
385
- const result = resolveDependencies(registry);
386
-
387
- expect(result.success).toBe(false);
388
- expect(result.error).toContain('Circular dependency');
389
- expect(result.circularPath).toBeDefined();
390
- });
391
-
392
- test('should detect missing dependencies', () => {
393
- const registry = new Map([
394
- ['widget', { metadata: { dependencies: ['missing'] } }],
395
- ]);
396
-
397
- const result = resolveDependencies(registry);
398
-
399
- expect(result.success).toBe(false);
400
- expect(result.error).toContain('Missing');
401
- expect(result.missingDeps).toEqual({ widget: ['missing'] });
402
- });
403
-
404
- test('should allow partial resolution when enabled', () => {
405
- const registry = new Map([
406
- ['good', { metadata: {} }],
407
- ['bad', { metadata: { dependencies: ['missing'] } }],
408
- ]);
409
-
410
- const result = resolveDependencies(registry, { allowPartial: true });
411
-
412
- expect(result.success).toBe(true);
413
- expect(result.order).toEqual(['good']);
414
- expect(result.missingDeps).toEqual({ bad: ['missing'] });
415
- });
416
-
417
- test('should return empty order for empty registry', () => {
418
- const registry = new Map();
419
- const result = resolveDependencies(registry);
420
-
421
- expect(result.success).toBe(true);
422
- expect(result.order).toEqual([]);
423
- });
424
-
425
- test('should check version constraints', () => {
426
- const registry = new Map([
427
- ['dep', { metadata: { version: '1.0.0' } }],
428
- ['widget', { metadata: { dependencies: [{ id: 'dep', version: '>=2.0.0' }] } }],
429
- ]);
430
-
431
- const result = resolveDependencies(registry);
432
-
433
- expect(result.success).toBe(false);
434
- expect(result.error).toContain('constraint');
435
- expect(result.constraintViolations).toBeDefined();
436
- });
437
-
438
- test('should skip version check when requested', () => {
439
- const registry = new Map([
440
- ['dep', { metadata: { version: '1.0.0' } }],
441
- ['widget', { metadata: { dependencies: [{ id: 'dep', version: '>=2.0.0' }] } }],
442
- ]);
443
-
444
- const result = resolveDependencies(registry, { skipVersionCheck: true });
445
-
446
- expect(result.success).toBe(true);
447
- });
448
- });
449
-
450
- describe('getAllDependencies', () => {
451
- test('should return all transitive dependencies', () => {
452
- const registry = new Map([
453
- ['a', { metadata: {} }],
454
- ['b', { metadata: { dependencies: ['a'] } }],
455
- ['c', { metadata: { dependencies: ['b'] } }],
456
- ]);
457
-
458
- const graph = buildDependencyGraph(registry);
459
- const deps = getAllDependencies(graph, 'c');
460
-
461
- expect(deps).toContain('a');
462
- expect(deps).toContain('b');
463
- expect(deps).not.toContain('c');
464
- });
465
-
466
- test('should handle widgets with no dependencies', () => {
467
- const registry = new Map([
468
- ['a', { metadata: {} }],
469
- ]);
470
-
471
- const graph = buildDependencyGraph(registry);
472
- const deps = getAllDependencies(graph, 'a');
473
-
474
- expect(deps).toEqual([]);
475
- });
476
-
477
- test('should optionally exclude optional dependencies', () => {
478
- const registry = new Map([
479
- ['required', { metadata: {} }],
480
- ['optional', { metadata: {} }],
481
- ['widget', { metadata: { dependencies: ['required', { id: 'optional', optional: true }] } }],
482
- ]);
483
-
484
- const graph = buildDependencyGraph(registry);
485
- const depsWithOptional = getAllDependencies(graph, 'widget', { includeOptional: true });
486
- const depsWithoutOptional = getAllDependencies(graph, 'widget', { includeOptional: false });
487
-
488
- expect(depsWithOptional).toContain('optional');
489
- expect(depsWithoutOptional).not.toContain('optional');
490
- });
491
- });
492
-
493
- describe('getAllDependents', () => {
494
- test('should return all widgets that depend on given widget', () => {
495
- const registry = new Map([
496
- ['a', { metadata: {} }],
497
- ['b', { metadata: { dependencies: ['a'] } }],
498
- ['c', { metadata: { dependencies: ['b'] } }],
499
- ]);
500
-
501
- const graph = buildDependencyGraph(registry);
502
- const dependents = getAllDependents(graph, 'a');
503
-
504
- expect(dependents).toContain('b');
505
- expect(dependents).toContain('c');
506
- expect(dependents).not.toContain('a');
507
- });
508
-
509
- test('should return empty array for widgets with no dependents', () => {
510
- const registry = new Map([
511
- ['a', { metadata: {} }],
512
- ['b', { metadata: { dependencies: ['a'] } }],
513
- ]);
514
-
515
- const graph = buildDependencyGraph(registry);
516
- const dependents = getAllDependents(graph, 'b');
517
-
518
- expect(dependents).toEqual([]);
519
- });
520
- });
521
-
522
- describe('validateWidgetDependencies', () => {
523
- test('should return valid for widget with no dependencies', () => {
524
- const registry = new Map([
525
- ['widget', { metadata: {} }],
526
- ]);
527
-
528
- const result = validateWidgetDependencies(registry, 'widget');
529
-
530
- expect(result.valid).toBe(true);
531
- expect(result.dependencies).toEqual([]);
532
- });
533
-
534
- test('should return valid when all dependencies satisfied', () => {
535
- const registry = new Map([
536
- ['dep', { metadata: { version: '1.0.0' } }],
537
- ['widget', { metadata: { dependencies: [{ id: 'dep', version: '1.0.0' }] } }],
538
- ]);
539
-
540
- const result = validateWidgetDependencies(registry, 'widget');
541
-
542
- expect(result.valid).toBe(true);
543
- expect(result.dependencies).toEqual(['dep']);
544
- });
545
-
546
- test('should return invalid for missing dependency', () => {
547
- const registry = new Map([
548
- ['widget', { metadata: { dependencies: ['missing'] } }],
549
- ]);
550
-
551
- const result = validateWidgetDependencies(registry, 'widget');
552
-
553
- expect(result.valid).toBe(false);
554
- expect(result.error).toContain('Missing');
555
- });
556
-
557
- test('should return invalid for version constraint violation', () => {
558
- const registry = new Map([
559
- ['dep', { metadata: { version: '1.0.0' } }],
560
- ['widget', { metadata: { dependencies: [{ id: 'dep', version: '>=2.0.0' }] } }],
561
- ]);
562
-
563
- const result = validateWidgetDependencies(registry, 'widget');
564
-
565
- expect(result.valid).toBe(false);
566
- expect(result.error).toContain('does not satisfy');
567
- });
568
-
569
- test('should return not found for non-existent widget', () => {
570
- const registry = new Map();
571
-
572
- const result = validateWidgetDependencies(registry, 'nonexistent');
573
-
574
- expect(result.valid).toBe(false);
575
- expect(result.error).toContain('not found');
576
- });
577
-
578
- test('should return all transitive dependencies', () => {
579
- const registry = new Map([
580
- ['base', { metadata: {} }],
581
- ['mid', { metadata: { dependencies: ['base'] } }],
582
- ['top', { metadata: { dependencies: ['mid'] } }],
583
- ]);
584
-
585
- const result = validateWidgetDependencies(registry, 'top');
586
-
587
- expect(result.valid).toBe(true);
588
- expect(result.allDependencies).toContain('base');
589
- expect(result.allDependencies).toContain('mid');
590
- });
591
- });