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.
- package/CHANGELOG.md +29 -1
- package/docs/API.md +1 -2
- package/index.js +32 -42
- package/package.json +23 -12
- package/src/auto-save.js +11 -5
- package/src/cli/export-snapshot.js +3 -1
- package/src/cli/import-snapshot.js +3 -1
- package/src/config.js +9 -15
- package/src/errors.js +0 -9
- package/src/security.js +6 -2
- package/src/snapshot.js +73 -26
- package/src/web-server.js +7 -10
- package/src/widgets/builtin-widgets.js +0 -94
- package/src/widgets/index.js +0 -1
- package/.c8rc.json +0 -38
- package/.dockerignore +0 -68
- package/.github/dependabot.yml +0 -45
- package/.github/pull_request_template.md +0 -39
- package/.github/workflows/ci.yml +0 -147
- package/.github/workflows/release.yml +0 -40
- package/.github/workflows/security.yml +0 -84
- package/.husky/pre-commit +0 -1
- package/.planning/codebase/ARCHITECTURE.md +0 -206
- package/.planning/codebase/INTEGRATIONS.md +0 -150
- package/.planning/codebase/STACK.md +0 -122
- package/.planning/codebase/STRUCTURE.md +0 -201
- package/CONTRIBUTING.md +0 -378
- package/Dockerfile +0 -54
- package/FEATURES.md +0 -307
- package/TODO.md +0 -28
- package/ai.openclaw.dashboard.plist +0 -35
- package/build-cjs.js +0 -127
- package/cjs-shim.js +0 -13
- package/dist/clawdash +0 -1729
- package/dist/clawdash.meta.json +0 -2236
- package/dist/widgets.cjs +0 -6511
- package/docker-compose.yml +0 -67
- package/esbuild.config.js +0 -158
- package/eslint.config.js +0 -56
- package/examples/plugins/README.md +0 -122
- package/examples/plugins/api-status/index.js +0 -294
- package/examples/plugins/api-status/plugin.json +0 -19
- package/examples/plugins/hello-world/index.js +0 -104
- package/examples/plugins/hello-world/plugin.json +0 -15
- package/examples/plugins/system-metrics-chart/index.js +0 -339
- package/examples/plugins/system-metrics-chart/plugin.json +0 -18
- package/examples/plugins/weather-widget/index.js +0 -136
- package/examples/plugins/weather-widget/plugin.json +0 -19
- package/index.cjs +0 -23005
- package/jest.config.js +0 -11
- package/scripts/release.js +0 -595
- package/src/database.js +0 -734
- package/tests/alerts.test.js +0 -437
- package/tests/auto-save.test.js +0 -529
- package/tests/cache.test.js +0 -317
- package/tests/cli.test.js +0 -351
- package/tests/command-palette.test.js +0 -320
- package/tests/config-processor.test.js +0 -452
- package/tests/config-validator.test.js +0 -710
- package/tests/config-watcher.test.js +0 -594
- package/tests/config.test.js +0 -755
- package/tests/database.test.js +0 -438
- package/tests/errors.test.js +0 -189
- package/tests/example-plugins.test.js +0 -624
- package/tests/integration.test.js +0 -1321
- package/tests/loading-states.test.js +0 -300
- package/tests/manifest-validation-on-load.test.js +0 -671
- package/tests/performance-monitor.test.js +0 -190
- package/tests/plugin-api-rate-limit.test.js +0 -302
- package/tests/plugin-errors.test.js +0 -311
- package/tests/plugin-lifecycle-e2e.test.js +0 -1036
- package/tests/plugin-reload.test.js +0 -764
- package/tests/rate-limiter.test.js +0 -539
- package/tests/retry.test.js +0 -308
- package/tests/security.test.js +0 -411
- package/tests/settings-modal.test.js +0 -226
- package/tests/theme-selector.test.js +0 -57
- package/tests/utils.js +0 -242
- package/tests/utils.test.js +0 -317
- package/tests/validate-plugin-cli.test.js +0 -359
- package/tests/validation.test.js +0 -837
- package/tests/web-server.test.js +0 -646
- package/tests/widget-arrange-mode.test.js +0 -183
- package/tests/widget-config-hot-reload.test.js +0 -465
- package/tests/widget-dependency.test.js +0 -591
- package/tests/widget-error-boundary.test.js +0 -749
- package/tests/widget-error-isolation.test.js +0 -469
- package/tests/widget-integration.test.js +0 -1147
- package/tests/widget-refresh-intervals.test.js +0 -284
- 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
|
-
});
|