claw-dashboard 1.9.0 → 2.1.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/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5285 -566
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -6
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1934 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1056 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- package/tests/worker-pool.test.js +522 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest Validation on Load Tests
|
|
3
|
+
* Tests that plugin manifests are validated against the schema when loaded
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { mkdtemp, rm, writeFile, mkdir } from 'fs/promises';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
// Import modules under test
|
|
16
|
+
import { WidgetLoader } from '../src/widgets/widget-loader.js';
|
|
17
|
+
|
|
18
|
+
describe('Manifest Validation on Load', () => {
|
|
19
|
+
let loader;
|
|
20
|
+
let tempDir;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tempDir = await mkdtemp(join(tmpdir(), 'manifest-validation-test-'));
|
|
24
|
+
loader = new WidgetLoader({ pluginsDir: tempDir });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
if (loader) {
|
|
29
|
+
await loader.clear();
|
|
30
|
+
}
|
|
31
|
+
if (tempDir) {
|
|
32
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const createPlugin = async (pluginId, manifest, indexJs = '') => {
|
|
37
|
+
const pluginDir = join(tempDir, pluginId);
|
|
38
|
+
await mkdir(pluginDir, { recursive: true });
|
|
39
|
+
await writeFile(join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2));
|
|
40
|
+
await writeFile(
|
|
41
|
+
join(pluginDir, 'index.js'),
|
|
42
|
+
indexJs || `
|
|
43
|
+
export default class TestWidget {
|
|
44
|
+
constructor(config) { this.config = config; this.name = '${manifest.name || pluginId}'; }
|
|
45
|
+
async render() {}
|
|
46
|
+
async getData() { return {}; }
|
|
47
|
+
}
|
|
48
|
+
`
|
|
49
|
+
);
|
|
50
|
+
return pluginDir;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe('Valid Manifests', () => {
|
|
54
|
+
test('should load plugin with valid manifest', async () => {
|
|
55
|
+
const validManifest = {
|
|
56
|
+
id: 'test-widget',
|
|
57
|
+
name: 'Test Widget',
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
type: 'widget',
|
|
60
|
+
category: 'custom',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await createPlugin('test-widget', validManifest);
|
|
64
|
+
const id = await loader.loadPlugin(join(tempDir, 'test-widget'));
|
|
65
|
+
|
|
66
|
+
expect(id).toBe('test-widget');
|
|
67
|
+
expect(loader.widgetRegistry.has('test-widget')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should discover plugin with valid manifest', async () => {
|
|
71
|
+
const validManifest = {
|
|
72
|
+
id: 'discover-test',
|
|
73
|
+
name: 'Discover Test',
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
type: 'widget',
|
|
76
|
+
category: 'system',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
await createPlugin('discover-test', validManifest);
|
|
80
|
+
const discovered = await loader.discoverPlugins();
|
|
81
|
+
|
|
82
|
+
expect(discovered.some(p => p.id === 'discover-test')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('should accept manifest with all optional fields', async () => {
|
|
86
|
+
const fullManifest = {
|
|
87
|
+
id: 'full-widget',
|
|
88
|
+
name: 'Full Widget',
|
|
89
|
+
description: 'A complete widget with all fields',
|
|
90
|
+
version: '2.1.0-beta.1',
|
|
91
|
+
author: 'Test Author <test@example.com>',
|
|
92
|
+
category: 'monitoring',
|
|
93
|
+
type: 'widget',
|
|
94
|
+
lazyLoad: true,
|
|
95
|
+
priority: 50,
|
|
96
|
+
config: {
|
|
97
|
+
refreshInterval: 5000,
|
|
98
|
+
maxDataPoints: 30,
|
|
99
|
+
},
|
|
100
|
+
permissions: ['network', 'system'],
|
|
101
|
+
dependencies: ['base-widget'],
|
|
102
|
+
__version: 1,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await createPlugin('full-widget', fullManifest);
|
|
106
|
+
const id = await loader.loadPlugin(join(tempDir, 'full-widget'));
|
|
107
|
+
|
|
108
|
+
expect(id).toBe('full-widget');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should accept valid categories', async () => {
|
|
112
|
+
const categories = ['system', 'monitoring', 'custom', 'example'];
|
|
113
|
+
|
|
114
|
+
for (const category of categories) {
|
|
115
|
+
const loader2 = new WidgetLoader({ pluginsDir: tempDir });
|
|
116
|
+
const manifest = {
|
|
117
|
+
id: `cat-${category}`,
|
|
118
|
+
name: `Category ${category}`,
|
|
119
|
+
version: '1.0.0',
|
|
120
|
+
type: 'widget',
|
|
121
|
+
category,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
await createPlugin(`cat-${category}`, manifest);
|
|
125
|
+
const id = await loader2.loadPlugin(join(tempDir, `cat-${category}`));
|
|
126
|
+
expect(id).toBe(`cat-${category}`);
|
|
127
|
+
await loader2.clear();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should accept valid semantic versions', async () => {
|
|
132
|
+
const versions = ['1.0.0', '0.0.1', '10.20.30', '1.0.0-alpha', '2.0.0-beta.1+build123'];
|
|
133
|
+
|
|
134
|
+
for (const version of versions) {
|
|
135
|
+
const loader2 = new WidgetLoader({ pluginsDir: tempDir });
|
|
136
|
+
const manifest = {
|
|
137
|
+
id: `ver-${version.replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
138
|
+
name: `Version Test`,
|
|
139
|
+
version,
|
|
140
|
+
type: 'widget',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
await createPlugin(`ver-${version.replace(/[^a-zA-Z0-9]/g, '-')}`, manifest);
|
|
144
|
+
const id = await loader2.loadPlugin(join(tempDir, `ver-${version.replace(/[^a-zA-Z0-9]/g, '-')}`));
|
|
145
|
+
expect(id).toBeTruthy();
|
|
146
|
+
await loader2.clear();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Invalid Manifests - Required Fields', () => {
|
|
152
|
+
test('should reject manifest missing id', async () => {
|
|
153
|
+
const invalidManifest = {
|
|
154
|
+
name: 'Missing ID',
|
|
155
|
+
version: '1.0.0',
|
|
156
|
+
type: 'widget',
|
|
157
|
+
// missing id
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await createPlugin('missing-id', invalidManifest);
|
|
161
|
+
const id = await loader.loadPlugin(join(tempDir, 'missing-id'));
|
|
162
|
+
|
|
163
|
+
expect(id).toBeNull();
|
|
164
|
+
expect(loader.widgetRegistry.has('missing-id')).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should reject manifest missing name', async () => {
|
|
168
|
+
const invalidManifest = {
|
|
169
|
+
id: 'missing-name',
|
|
170
|
+
version: '1.0.0',
|
|
171
|
+
type: 'widget',
|
|
172
|
+
// missing name
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await createPlugin('missing-name', invalidManifest);
|
|
176
|
+
const id = await loader.loadPlugin(join(tempDir, 'missing-name'));
|
|
177
|
+
|
|
178
|
+
expect(id).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('should reject manifest missing version', async () => {
|
|
182
|
+
const invalidManifest = {
|
|
183
|
+
id: 'missing-version',
|
|
184
|
+
name: 'Missing Version',
|
|
185
|
+
type: 'widget',
|
|
186
|
+
// missing version
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
await createPlugin('missing-version', invalidManifest);
|
|
190
|
+
const id = await loader.loadPlugin(join(tempDir, 'missing-version'));
|
|
191
|
+
|
|
192
|
+
expect(id).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('should reject manifest missing type', async () => {
|
|
196
|
+
const invalidManifest = {
|
|
197
|
+
id: 'missing-type',
|
|
198
|
+
name: 'Missing Type',
|
|
199
|
+
version: '1.0.0',
|
|
200
|
+
// missing type
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
await createPlugin('missing-type', invalidManifest);
|
|
204
|
+
const id = await loader.loadPlugin(join(tempDir, 'missing-type'));
|
|
205
|
+
|
|
206
|
+
expect(id).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Invalid Manifests - Field Formats', () => {
|
|
211
|
+
test('should reject invalid id format', async () => {
|
|
212
|
+
const invalidManifest = {
|
|
213
|
+
id: '_invalid-start',
|
|
214
|
+
name: 'Invalid ID',
|
|
215
|
+
version: '1.0.0',
|
|
216
|
+
type: 'widget',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
await createPlugin('_invalid-start', invalidManifest);
|
|
220
|
+
const id = await loader.loadPlugin(join(tempDir, '_invalid-start'));
|
|
221
|
+
|
|
222
|
+
expect(id).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('should reject id ending with underscore', async () => {
|
|
226
|
+
const invalidManifest = {
|
|
227
|
+
id: 'invalid-end_',
|
|
228
|
+
name: 'Invalid ID End',
|
|
229
|
+
version: '1.0.0',
|
|
230
|
+
type: 'widget',
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
await createPlugin('invalid-end_', invalidManifest);
|
|
234
|
+
const id = await loader.loadPlugin(join(tempDir, 'invalid-end_'));
|
|
235
|
+
|
|
236
|
+
expect(id).toBeNull();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('should reject invalid semantic version', async () => {
|
|
240
|
+
const invalidManifest = {
|
|
241
|
+
id: 'bad-version',
|
|
242
|
+
name: 'Bad Version',
|
|
243
|
+
version: 'not-a-version',
|
|
244
|
+
type: 'widget',
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
await createPlugin('bad-version', invalidManifest);
|
|
248
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-version'));
|
|
249
|
+
|
|
250
|
+
expect(id).toBeNull();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('should reject invalid version format (missing patch)', async () => {
|
|
254
|
+
const invalidManifest = {
|
|
255
|
+
id: 'short-version',
|
|
256
|
+
name: 'Short Version',
|
|
257
|
+
version: '1.0', // missing patch
|
|
258
|
+
type: 'widget',
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
await createPlugin('short-version', invalidManifest);
|
|
262
|
+
const id = await loader.loadPlugin(join(tempDir, 'short-version'));
|
|
263
|
+
|
|
264
|
+
expect(id).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('should reject invalid category', async () => {
|
|
268
|
+
const invalidManifest = {
|
|
269
|
+
id: 'bad-category',
|
|
270
|
+
name: 'Bad Category',
|
|
271
|
+
version: '1.0.0',
|
|
272
|
+
type: 'widget',
|
|
273
|
+
category: 'invalid-category',
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
await createPlugin('bad-category', invalidManifest);
|
|
277
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-category'));
|
|
278
|
+
|
|
279
|
+
expect(id).toBeNull();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('should reject invalid type', async () => {
|
|
283
|
+
const invalidManifest = {
|
|
284
|
+
id: 'bad-type',
|
|
285
|
+
name: 'Bad Type',
|
|
286
|
+
version: '1.0.0',
|
|
287
|
+
type: 'invalid-type',
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
await createPlugin('bad-type', invalidManifest);
|
|
291
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-type'));
|
|
292
|
+
|
|
293
|
+
expect(id).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('Invalid Manifests - Type Validation', () => {
|
|
298
|
+
test('should reject non-string id', async () => {
|
|
299
|
+
const invalidManifest = {
|
|
300
|
+
id: 123,
|
|
301
|
+
name: 'Non-string ID',
|
|
302
|
+
version: '1.0.0',
|
|
303
|
+
type: 'widget',
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
await createPlugin('123', invalidManifest);
|
|
307
|
+
const id = await loader.loadPlugin(join(tempDir, '123'));
|
|
308
|
+
|
|
309
|
+
expect(id).toBeNull();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('should reject non-string version', async () => {
|
|
313
|
+
const invalidManifest = {
|
|
314
|
+
id: 'num-version',
|
|
315
|
+
name: 'Number Version',
|
|
316
|
+
version: 100, // number instead of string
|
|
317
|
+
type: 'widget',
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
await createPlugin('num-version', invalidManifest);
|
|
321
|
+
const id = await loader.loadPlugin(join(tempDir, 'num-version'));
|
|
322
|
+
|
|
323
|
+
expect(id).toBeNull();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('should reject non-boolean lazyLoad', async () => {
|
|
327
|
+
const invalidManifest = {
|
|
328
|
+
id: 'bad-lazyload',
|
|
329
|
+
name: 'Bad LazyLoad',
|
|
330
|
+
version: '1.0.0',
|
|
331
|
+
type: 'widget',
|
|
332
|
+
lazyLoad: 'true', // string instead of boolean
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
await createPlugin('bad-lazyload', invalidManifest);
|
|
336
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-lazyload'));
|
|
337
|
+
|
|
338
|
+
expect(id).toBeNull();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('should reject priority out of range (negative)', async () => {
|
|
342
|
+
const invalidManifest = {
|
|
343
|
+
id: 'bad-priority-neg',
|
|
344
|
+
name: 'Bad Priority Negative',
|
|
345
|
+
version: '1.0.0',
|
|
346
|
+
type: 'widget',
|
|
347
|
+
priority: -1,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
await createPlugin('bad-priority-neg', invalidManifest);
|
|
351
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-priority-neg'));
|
|
352
|
+
|
|
353
|
+
expect(id).toBeNull();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('should reject priority out of range (too high)', async () => {
|
|
357
|
+
const invalidManifest = {
|
|
358
|
+
id: 'bad-priority-high',
|
|
359
|
+
name: 'Bad Priority High',
|
|
360
|
+
version: '1.0.0',
|
|
361
|
+
type: 'widget',
|
|
362
|
+
priority: 2000,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
await createPlugin('bad-priority-high', invalidManifest);
|
|
366
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-priority-high'));
|
|
367
|
+
|
|
368
|
+
expect(id).toBeNull();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('Invalid Manifests - Array Validation', () => {
|
|
373
|
+
test('should reject duplicate permissions', async () => {
|
|
374
|
+
const invalidManifest = {
|
|
375
|
+
id: 'dup-perms',
|
|
376
|
+
name: 'Duplicate Permissions',
|
|
377
|
+
version: '1.0.0',
|
|
378
|
+
type: 'widget',
|
|
379
|
+
permissions: ['network', 'network', 'system'],
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
await createPlugin('dup-perms', invalidManifest);
|
|
383
|
+
const id = await loader.loadPlugin(join(tempDir, 'dup-perms'));
|
|
384
|
+
|
|
385
|
+
expect(id).toBeNull();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('should reject invalid permission value', async () => {
|
|
389
|
+
const invalidManifest = {
|
|
390
|
+
id: 'bad-perm',
|
|
391
|
+
name: 'Bad Permission',
|
|
392
|
+
version: '1.0.0',
|
|
393
|
+
type: 'widget',
|
|
394
|
+
permissions: ['network', 'invalid-perm'],
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
await createPlugin('bad-perm', invalidManifest);
|
|
398
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-perm'));
|
|
399
|
+
|
|
400
|
+
expect(id).toBeNull();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('should reject duplicate dependencies', async () => {
|
|
404
|
+
const invalidManifest = {
|
|
405
|
+
id: 'dup-deps',
|
|
406
|
+
name: 'Duplicate Dependencies',
|
|
407
|
+
version: '1.0.0',
|
|
408
|
+
type: 'widget',
|
|
409
|
+
dependencies: ['base-widget', 'base-widget'],
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
await createPlugin('dup-deps', invalidManifest);
|
|
413
|
+
const id = await loader.loadPlugin(join(tempDir, 'dup-deps'));
|
|
414
|
+
|
|
415
|
+
expect(id).toBeNull();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('should reject invalid dependency id format', async () => {
|
|
419
|
+
const invalidManifest = {
|
|
420
|
+
id: 'bad-dep-id',
|
|
421
|
+
name: 'Bad Dependency ID',
|
|
422
|
+
version: '1.0.0',
|
|
423
|
+
type: 'widget',
|
|
424
|
+
dependencies: ['_invalid-dep'],
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
await createPlugin('bad-dep-id', invalidManifest);
|
|
428
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-dep-id'));
|
|
429
|
+
|
|
430
|
+
expect(id).toBeNull();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('Validation with fallbackOnError', () => {
|
|
435
|
+
test('should throw on invalid manifest when fallbackOnError is false', async () => {
|
|
436
|
+
const invalidManifest = {
|
|
437
|
+
id: 'no-fallback',
|
|
438
|
+
name: 'No Fallback',
|
|
439
|
+
version: 'invalid',
|
|
440
|
+
type: 'widget',
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
await createPlugin('no-fallback', invalidManifest);
|
|
444
|
+
|
|
445
|
+
await expect(
|
|
446
|
+
loader.loadPlugin(join(tempDir, 'no-fallback'), { fallbackOnError: false })
|
|
447
|
+
).rejects.toThrow('Invalid plugin manifest');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('should return null on invalid manifest when fallbackOnError is true', async () => {
|
|
451
|
+
const invalidManifest = {
|
|
452
|
+
id: 'with-fallback',
|
|
453
|
+
name: 'With Fallback',
|
|
454
|
+
version: 'invalid',
|
|
455
|
+
type: 'widget',
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
await createPlugin('with-fallback', invalidManifest);
|
|
459
|
+
const id = await loader.loadPlugin(join(tempDir, 'with-fallback'), { fallbackOnError: true });
|
|
460
|
+
|
|
461
|
+
expect(id).toBeNull();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('Discovery with Invalid Plugins', () => {
|
|
466
|
+
test('should skip invalid plugins during discovery', async () => {
|
|
467
|
+
const validManifest = {
|
|
468
|
+
id: 'valid-plugin',
|
|
469
|
+
name: 'Valid Plugin',
|
|
470
|
+
version: '1.0.0',
|
|
471
|
+
type: 'widget',
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const invalidManifest = {
|
|
475
|
+
id: 'invalid-plugin',
|
|
476
|
+
name: 'Invalid Plugin',
|
|
477
|
+
version: 'bad-version',
|
|
478
|
+
type: 'widget',
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
await createPlugin('valid-plugin', validManifest);
|
|
482
|
+
await createPlugin('invalid-plugin', invalidManifest);
|
|
483
|
+
|
|
484
|
+
const discovered = await loader.discoverPlugins();
|
|
485
|
+
|
|
486
|
+
expect(discovered.some(p => p.id === 'valid-plugin')).toBe(true);
|
|
487
|
+
expect(discovered.some(p => p.id === 'invalid-plugin')).toBe(false);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('should discover only valid plugins when mixed with invalid ones', async () => {
|
|
491
|
+
const manifests = [
|
|
492
|
+
{ id: 'plugin-1', name: 'Plugin 1', version: '1.0.0', type: 'widget' },
|
|
493
|
+
{ id: 'plugin-2', name: 'Plugin 2', version: 'invalid', type: 'widget' },
|
|
494
|
+
{ id: 'plugin-3', name: 'Plugin 3', version: '2.0.0', type: 'widget' },
|
|
495
|
+
{ id: 'plugin-4', name: 'Plugin 4', type: 'widget' }, // missing version
|
|
496
|
+
];
|
|
497
|
+
|
|
498
|
+
for (const manifest of manifests) {
|
|
499
|
+
await createPlugin(manifest.id || 'unknown', manifest);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const discovered = await loader.discoverPlugins();
|
|
503
|
+
const ids = discovered.map(p => p.id);
|
|
504
|
+
|
|
505
|
+
expect(ids).toContain('plugin-1');
|
|
506
|
+
expect(ids).not.toContain('plugin-2');
|
|
507
|
+
expect(ids).toContain('plugin-3');
|
|
508
|
+
expect(ids).not.toContain('plugin-4');
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe('Registration with Validation', () => {
|
|
513
|
+
test('should register plugin with valid manifest', async () => {
|
|
514
|
+
const validManifest = {
|
|
515
|
+
id: 'reg-test',
|
|
516
|
+
name: 'Registration Test',
|
|
517
|
+
version: '1.0.0',
|
|
518
|
+
type: 'widget',
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
await createPlugin('reg-test', validManifest);
|
|
522
|
+
const id = await loader.registerPlugin(join(tempDir, 'reg-test'));
|
|
523
|
+
|
|
524
|
+
expect(id).toBe('reg-test');
|
|
525
|
+
expect(loader.widgetRegistry.has('reg-test')).toBe(true);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('should skip registration for invalid manifest', async () => {
|
|
529
|
+
const invalidManifest = {
|
|
530
|
+
id: 'reg-invalid',
|
|
531
|
+
name: 'Registration Invalid',
|
|
532
|
+
version: 'bad',
|
|
533
|
+
type: 'widget',
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
await createPlugin('reg-invalid', invalidManifest);
|
|
537
|
+
const id = await loader.registerPlugin(join(tempDir, 'reg-invalid'));
|
|
538
|
+
|
|
539
|
+
expect(id).toBeNull();
|
|
540
|
+
expect(loader.widgetRegistry.has('reg-invalid')).toBe(false);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe('Edge Cases', () => {
|
|
545
|
+
test('should handle empty manifest object', async () => {
|
|
546
|
+
await createPlugin('empty-manifest', {});
|
|
547
|
+
const id = await loader.loadPlugin(join(tempDir, 'empty-manifest'));
|
|
548
|
+
|
|
549
|
+
expect(id).toBeNull();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('should handle null manifest values', async () => {
|
|
553
|
+
const invalidManifest = {
|
|
554
|
+
id: null,
|
|
555
|
+
name: null,
|
|
556
|
+
version: null,
|
|
557
|
+
type: null,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
await createPlugin('null-values', invalidManifest);
|
|
561
|
+
const id = await loader.loadPlugin(join(tempDir, 'null-values'));
|
|
562
|
+
|
|
563
|
+
expect(id).toBeNull();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('should handle very long id', async () => {
|
|
567
|
+
const invalidManifest = {
|
|
568
|
+
id: 'a'.repeat(100),
|
|
569
|
+
name: 'Long ID',
|
|
570
|
+
version: '1.0.0',
|
|
571
|
+
type: 'widget',
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
await createPlugin('long-id', invalidManifest);
|
|
575
|
+
const id = await loader.loadPlugin(join(tempDir, 'long-id'));
|
|
576
|
+
|
|
577
|
+
expect(id).toBeNull();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('should handle empty string name', async () => {
|
|
581
|
+
const invalidManifest = {
|
|
582
|
+
id: 'empty-name',
|
|
583
|
+
name: '',
|
|
584
|
+
version: '1.0.0',
|
|
585
|
+
type: 'widget',
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
await createPlugin('empty-name', invalidManifest);
|
|
589
|
+
const id = await loader.loadPlugin(join(tempDir, 'empty-name'));
|
|
590
|
+
|
|
591
|
+
expect(id).toBeNull();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('should validate __version field', async () => {
|
|
595
|
+
const invalidManifest = {
|
|
596
|
+
id: 'bad-config-version',
|
|
597
|
+
name: 'Bad Config Version',
|
|
598
|
+
version: '1.0.0',
|
|
599
|
+
type: 'widget',
|
|
600
|
+
__version: 0, // below minimum
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
await createPlugin('bad-config-version', invalidManifest);
|
|
604
|
+
const id = await loader.loadPlugin(join(tempDir, 'bad-config-version'));
|
|
605
|
+
|
|
606
|
+
expect(id).toBeNull();
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
describe('Description and Author Field Validation', () => {
|
|
611
|
+
test('should accept manifest with long description', async () => {
|
|
612
|
+
const manifest = {
|
|
613
|
+
id: 'long-desc',
|
|
614
|
+
name: 'Long Description',
|
|
615
|
+
version: '1.0.0',
|
|
616
|
+
type: 'widget',
|
|
617
|
+
description: 'A'.repeat(500), // at max length
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
await createPlugin('long-desc', manifest);
|
|
621
|
+
const id = await loader.loadPlugin(join(tempDir, 'long-desc'));
|
|
622
|
+
|
|
623
|
+
expect(id).toBe('long-desc');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test('should reject description exceeding max length', async () => {
|
|
627
|
+
const invalidManifest = {
|
|
628
|
+
id: 'too-long-desc',
|
|
629
|
+
name: 'Too Long Description',
|
|
630
|
+
version: '1.0.0',
|
|
631
|
+
type: 'widget',
|
|
632
|
+
description: 'A'.repeat(501), // exceeds max
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
await createPlugin('too-long-desc', invalidManifest);
|
|
636
|
+
const id = await loader.loadPlugin(join(tempDir, 'too-long-desc'));
|
|
637
|
+
|
|
638
|
+
expect(id).toBeNull();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test('should accept manifest with long author', async () => {
|
|
642
|
+
const manifest = {
|
|
643
|
+
id: 'long-author',
|
|
644
|
+
name: 'Long Author',
|
|
645
|
+
version: '1.0.0',
|
|
646
|
+
type: 'widget',
|
|
647
|
+
author: 'A'.repeat(200), // at max length
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
await createPlugin('long-author', manifest);
|
|
651
|
+
const id = await loader.loadPlugin(join(tempDir, 'long-author'));
|
|
652
|
+
|
|
653
|
+
expect(id).toBe('long-author');
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test('should reject author exceeding max length', async () => {
|
|
657
|
+
const invalidManifest = {
|
|
658
|
+
id: 'too-long-author',
|
|
659
|
+
name: 'Too Long Author',
|
|
660
|
+
version: '1.0.0',
|
|
661
|
+
type: 'widget',
|
|
662
|
+
author: 'A'.repeat(201), // exceeds max
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
await createPlugin('too-long-author', invalidManifest);
|
|
666
|
+
const id = await loader.loadPlugin(join(tempDir, 'too-long-author'));
|
|
667
|
+
|
|
668
|
+
expect(id).toBeNull();
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
});
|