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,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
- });