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,710 +0,0 @@
1
- /**
2
- * Tests for config-validator module
3
- */
4
-
5
- import { validateConfig, validateConfigFile, formatConfigValidationResult, getDefaultConfigPath } from '../src/config-validator.js';
6
- import { mkdtempSync, writeFileSync, rmSync } from 'fs';
7
- import { join } from 'path';
8
- import os from 'os';
9
- import { execSync } from 'child_process';
10
-
11
- const CLI_PATH = join(process.cwd(), 'index.js');
12
-
13
- describe('config-validator', () => {
14
- describe('validateConfig', () => {
15
- describe('valid configurations', () => {
16
- test('validates minimal valid config', () => {
17
- const config = {
18
- refreshInterval: 2000,
19
- theme: 'auto',
20
- };
21
- const result = validateConfig(config);
22
- expect(result.valid).toBe(true);
23
- expect(result.errors).toHaveLength(0);
24
- });
25
-
26
- test('validates full default config', () => {
27
- const config = {
28
- refreshInterval: 2000,
29
- logLevelFilter: 'all',
30
- sessionSortMode: 'time',
31
- theme: 'dark',
32
- exportFormat: 'json',
33
- showWidget1: true,
34
- showWidget2: true,
35
- showPerformanceMetrics: false,
36
- firstRun: false,
37
- showFavoritesOnly: false,
38
- favorites: {},
39
- gatewayEndpoints: [
40
- { name: 'local', host: 'localhost', port: 18789, enabled: true, type: 'local' }
41
- ],
42
- activeGatewayEndpoint: 'local',
43
- webInterface: {
44
- enabled: false,
45
- port: 18790,
46
- host: '0.0.0.0',
47
- cors: true,
48
- },
49
- widgetLoading: {
50
- enabled: true,
51
- preloadPriority: ['cpu', 'memory'],
52
- lazyLoadDelay: 500,
53
- maxConcurrent: 3,
54
- autoDiscover: true,
55
- },
56
- plugins: {},
57
- };
58
- const result = validateConfig(config);
59
- expect(result.valid).toBe(true);
60
- expect(result.errors).toHaveLength(0);
61
- });
62
-
63
- test('validates all valid themes', () => {
64
- const themes = ['default', 'dark', 'high-contrast', 'ocean', 'auto'];
65
- for (const theme of themes) {
66
- const result = validateConfig({ theme });
67
- expect(result.valid).toBe(true);
68
- expect(result.errors).toHaveLength(0);
69
- }
70
- });
71
-
72
- test('validates all valid log levels', () => {
73
- const levels = ['all', 'error', 'warn', 'info', 'debug'];
74
- for (const level of levels) {
75
- const result = validateConfig({ logLevelFilter: level });
76
- expect(result.valid).toBe(true);
77
- expect(result.errors).toHaveLength(0);
78
- }
79
- });
80
-
81
- test('validates all valid sort modes', () => {
82
- const modes = ['time', 'tokens', 'idle', 'name'];
83
- for (const mode of modes) {
84
- const result = validateConfig({ sessionSortMode: mode });
85
- expect(result.valid).toBe(true);
86
- expect(result.errors).toHaveLength(0);
87
- }
88
- });
89
-
90
- test('validates all valid export formats', () => {
91
- const formats = ['json', 'csv'];
92
- for (const format of formats) {
93
- const result = validateConfig({ exportFormat: format });
94
- expect(result.valid).toBe(true);
95
- expect(result.errors).toHaveLength(0);
96
- }
97
- });
98
- });
99
-
100
- describe('invalid configurations', () => {
101
- test('rejects non-object config', () => {
102
- const result = validateConfig('not an object');
103
- expect(result.valid).toBe(false);
104
- expect(result.errors[0]).toContain('JSON object');
105
- });
106
-
107
- test('rejects null config', () => {
108
- const result = validateConfig(null);
109
- expect(result.valid).toBe(false);
110
- });
111
-
112
- test('rejects invalid theme', () => {
113
- const result = validateConfig({ theme: 'invalid-theme' });
114
- expect(result.valid).toBe(false);
115
- expect(result.errors[0]).toContain('theme');
116
- });
117
-
118
- test('rejects invalid log level', () => {
119
- const result = validateConfig({ logLevelFilter: 'verbose' });
120
- expect(result.valid).toBe(false);
121
- expect(result.errors[0]).toContain('logLevelFilter');
122
- });
123
-
124
- test('rejects invalid sort mode', () => {
125
- const result = validateConfig({ sessionSortMode: 'alphabetical' });
126
- expect(result.valid).toBe(false);
127
- expect(result.errors[0]).toContain('sessionSortMode');
128
- });
129
-
130
- test('rejects invalid export format', () => {
131
- const result = validateConfig({ exportFormat: 'xml' });
132
- expect(result.valid).toBe(false);
133
- expect(result.errors[0]).toContain('exportFormat');
134
- });
135
-
136
- test('rejects refresh interval too low', () => {
137
- const result = validateConfig({ refreshInterval: 100 });
138
- expect(result.valid).toBe(false);
139
- expect(result.errors[0]).toContain('refreshInterval');
140
- expect(result.errors[0]).toContain('at least');
141
- });
142
-
143
- test('rejects refresh interval too high', () => {
144
- const result = validateConfig({ refreshInterval: 120000 });
145
- expect(result.valid).toBe(false);
146
- expect(result.errors[0]).toContain('refreshInterval');
147
- expect(result.errors[0]).toContain('at most');
148
- });
149
- });
150
-
151
- describe('gateway endpoints validation', () => {
152
- test('validates valid endpoint', () => {
153
- const config = {
154
- gatewayEndpoints: [
155
- { name: 'local', host: 'localhost', port: 18789, enabled: true, type: 'local' }
156
- ]
157
- };
158
- const result = validateConfig(config);
159
- expect(result.valid).toBe(true);
160
- });
161
-
162
- test('rejects missing required endpoint fields', () => {
163
- const config = {
164
- gatewayEndpoints: [
165
- { host: 'localhost', port: 18789 } // missing name
166
- ]
167
- };
168
- const result = validateConfig(config);
169
- expect(result.valid).toBe(false);
170
- expect(result.errors.some(e => e.includes('name'))).toBe(true);
171
- });
172
-
173
- test('rejects invalid endpoint name', () => {
174
- const config = {
175
- gatewayEndpoints: [
176
- { name: 'invalid name!', host: 'localhost', port: 18789 }
177
- ]
178
- };
179
- const result = validateConfig(config);
180
- expect(result.valid).toBe(false);
181
- expect(result.errors.some(e => e.includes('name'))).toBe(true);
182
- });
183
-
184
- test('rejects too long endpoint name', () => {
185
- const config = {
186
- gatewayEndpoints: [
187
- { name: 'a'.repeat(50), host: 'localhost', port: 18789 }
188
- ]
189
- };
190
- const result = validateConfig(config);
191
- expect(result.valid).toBe(false);
192
- });
193
-
194
- test('rejects invalid port number', () => {
195
- const config = {
196
- gatewayEndpoints: [
197
- { name: 'local', host: 'localhost', port: 99999 }
198
- ]
199
- };
200
- const result = validateConfig(config);
201
- expect(result.valid).toBe(false);
202
- expect(result.errors.some(e => e.includes('port'))).toBe(true);
203
- });
204
-
205
- test('rejects invalid endpoint type', () => {
206
- const config = {
207
- gatewayEndpoints: [
208
- { name: 'local', host: 'localhost', port: 18789, type: 'invalid' }
209
- ]
210
- };
211
- const result = validateConfig(config);
212
- expect(result.valid).toBe(false);
213
- expect(result.errors.some(e => e.includes('type'))).toBe(true);
214
- });
215
-
216
- test('warns on empty gatewayEndpoints', () => {
217
- const config = { gatewayEndpoints: [] };
218
- const result = validateConfig(config);
219
- expect(result.valid).toBe(true);
220
- expect(result.warnings.some(w => w.includes('empty'))).toBe(true);
221
- });
222
-
223
- test('rejects too many endpoints', () => {
224
- const endpoints = Array(15).fill(null).map((_, i) => ({
225
- name: `endpoint${i}`,
226
- host: 'localhost',
227
- port: 18789
228
- }));
229
- const config = { gatewayEndpoints: endpoints };
230
- const result = validateConfig(config);
231
- expect(result.valid).toBe(false);
232
- expect(result.errors.some(e => e.includes('maximum'))).toBe(true);
233
- });
234
- });
235
-
236
- describe('web interface validation', () => {
237
- test('validates valid web interface config', () => {
238
- const config = {
239
- webInterface: {
240
- enabled: true,
241
- port: 18790,
242
- host: '0.0.0.0',
243
- cors: true,
244
- }
245
- };
246
- const result = validateConfig(config);
247
- expect(result.valid).toBe(true);
248
- });
249
-
250
- test('rejects invalid port in web interface', () => {
251
- const config = {
252
- webInterface: {
253
- enabled: true,
254
- port: 70000,
255
- }
256
- };
257
- const result = validateConfig(config);
258
- expect(result.valid).toBe(false);
259
- expect(result.errors.some(e => e.includes('port'))).toBe(true);
260
- });
261
-
262
- test('validates rate limit config', () => {
263
- const config = {
264
- webInterface: {
265
- rateLimit: {
266
- enabled: true,
267
- windowMs: 60000,
268
- maxRequests: 100,
269
- }
270
- }
271
- };
272
- const result = validateConfig(config);
273
- expect(result.valid).toBe(true);
274
- });
275
-
276
- test('rejects invalid rate limit windowMs', () => {
277
- const config = {
278
- webInterface: {
279
- rateLimit: {
280
- enabled: true,
281
- windowMs: 500,
282
- maxRequests: 100,
283
- }
284
- }
285
- };
286
- const result = validateConfig(config);
287
- expect(result.valid).toBe(true); // Still valid, but warns
288
- expect(result.warnings.some(w => w.includes('windowMs'))).toBe(true);
289
- });
290
-
291
- test('validates CORS origins as array', () => {
292
- const config = {
293
- webInterface: {
294
- corsOrigins: ['https://example.com', 'https://app.example.com']
295
- }
296
- };
297
- const result = validateConfig(config);
298
- expect(result.valid).toBe(true);
299
- });
300
-
301
- test('validates CORS origins as string', () => {
302
- const config = {
303
- webInterface: {
304
- corsOrigins: '*'
305
- }
306
- };
307
- const result = validateConfig(config);
308
- expect(result.valid).toBe(true);
309
- });
310
-
311
- test('rejects invalid CORS origins type', () => {
312
- const config = {
313
- webInterface: {
314
- corsOrigins: 123
315
- }
316
- };
317
- const result = validateConfig(config);
318
- expect(result.valid).toBe(false);
319
- });
320
- });
321
-
322
- describe('widget loading validation', () => {
323
- test('validates valid widget loading config', () => {
324
- const config = {
325
- widgetLoading: {
326
- enabled: true,
327
- preloadPriority: ['cpu', 'memory'],
328
- lazyLoadDelay: 500,
329
- maxConcurrent: 3,
330
- autoDiscover: true,
331
- }
332
- };
333
- const result = validateConfig(config);
334
- expect(result.valid).toBe(true);
335
- });
336
-
337
- test('rejects negative lazyLoadDelay', () => {
338
- const config = {
339
- widgetLoading: {
340
- lazyLoadDelay: -100
341
- }
342
- };
343
- const result = validateConfig(config);
344
- expect(result.valid).toBe(false);
345
- });
346
-
347
- test('rejects zero maxConcurrent', () => {
348
- const config = {
349
- widgetLoading: {
350
- maxConcurrent: 0
351
- }
352
- };
353
- const result = validateConfig(config);
354
- expect(result.valid).toBe(false);
355
- });
356
- });
357
-
358
- describe('strict mode', () => {
359
- test('allows unknown properties when not strict', () => {
360
- const config = { unknownField: 'value' };
361
- const result = validateConfig(config, { strict: false });
362
- expect(result.valid).toBe(true);
363
- });
364
-
365
- test('rejects unknown properties in strict mode', () => {
366
- const config = { unknownField: 'value' };
367
- const result = validateConfig(config, { strict: true });
368
- expect(result.valid).toBe(false);
369
- expect(result.errors.some(e => e.includes('unknownField'))).toBe(true);
370
- });
371
- });
372
-
373
- describe('stats', () => {
374
- test('returns field count in stats', () => {
375
- const config = { refreshInterval: 2000, theme: 'auto' };
376
- const result = validateConfig(config);
377
- expect(result.stats.fieldCount).toBe(2);
378
- });
379
- });
380
- });
381
-
382
- describe('validateConfigFile', () => {
383
- let tempDir;
384
-
385
- beforeEach(() => {
386
- tempDir = mkdtempSync(join(os.tmpdir(), 'config-validator-test-'));
387
- });
388
-
389
- afterEach(() => {
390
- try {
391
- rmSync(tempDir, { recursive: true, force: true });
392
- } catch {}
393
- });
394
-
395
- test('validates existing config file', () => {
396
- const config = { refreshInterval: 2000, theme: 'dark' };
397
- const configPath = join(tempDir, 'config.json');
398
- writeFileSync(configPath, JSON.stringify(config, null, 2));
399
-
400
- const result = validateConfigFile(configPath);
401
- expect(result.valid).toBe(true);
402
- });
403
-
404
- test('returns error for non-existent file', () => {
405
- const result = validateConfigFile(join(tempDir, 'nonexistent.json'));
406
- expect(result.valid).toBe(false);
407
- expect(result.errors[0]).toContain('not found');
408
- });
409
-
410
- test('returns error for invalid JSON', () => {
411
- const configPath = join(tempDir, 'invalid.json');
412
- writeFileSync(configPath, 'not valid json {');
413
-
414
- const result = validateConfigFile(configPath);
415
- expect(result.valid).toBe(false);
416
- expect(result.errors[0]).toContain('parse');
417
- });
418
- });
419
-
420
- describe('formatConfigValidationResult', () => {
421
- test('formats valid result', () => {
422
- const result = {
423
- valid: true,
424
- errors: [],
425
- warnings: [],
426
- info: ['Test info'],
427
- stats: { fieldCount: 5 }
428
- };
429
- const formatted = formatConfigValidationResult(result);
430
- expect(formatted).toContain('is valid');
431
- expect(formatted).toContain('5 field(s)');
432
- });
433
-
434
- test('formats invalid result', () => {
435
- const result = {
436
- valid: false,
437
- errors: ['Error 1', 'Error 2'],
438
- warnings: ['Warning 1'],
439
- info: [],
440
- stats: { fieldCount: 3 }
441
- };
442
- const formatted = formatConfigValidationResult(result, '/test/config.json');
443
- expect(formatted).toContain('validation failed');
444
- expect(formatted).toContain('/test/config.json');
445
- expect(formatted).toContain('Error 1');
446
- expect(formatted).toContain('Warning 1');
447
- });
448
-
449
- test('includes path in output', () => {
450
- const result = {
451
- valid: true,
452
- errors: [],
453
- warnings: [],
454
- info: [],
455
- stats: { fieldCount: 1 }
456
- };
457
- const formatted = formatConfigValidationResult(result, '/path/to/config.json');
458
- expect(formatted).toContain('/path/to/config.json');
459
- });
460
- });
461
-
462
- describe('getDefaultConfigPath', () => {
463
- test('returns path in home directory', () => {
464
- const path = getDefaultConfigPath();
465
- expect(path).toContain('.openclaw');
466
- expect(path).toContain('dashboard-settings.json');
467
- expect(path.startsWith(os.homedir())).toBe(true);
468
- });
469
- });
470
- });
471
-
472
- describe('validate-config CLI', () => {
473
- let tempDir;
474
-
475
- beforeEach(() => {
476
- tempDir = mkdtempSync(join(os.tmpdir(), 'validate-config-test-'));
477
- });
478
-
479
- afterEach(() => {
480
- try {
481
- rmSync(tempDir, { recursive: true, force: true });
482
- } catch {}
483
- });
484
-
485
- const runCli = (args) => {
486
- try {
487
- const output = execSync(`node ${CLI_PATH} ${args}`, {
488
- encoding: 'utf8',
489
- cwd: process.cwd(),
490
- });
491
- return { exitCode: 0, output, stderr: '' };
492
- } catch (error) {
493
- return {
494
- exitCode: error.status || 1,
495
- output: error.stdout || '',
496
- stderr: error.stderr || error.message,
497
- };
498
- }
499
- };
500
-
501
- describe('help', () => {
502
- test('shows help with --help', () => {
503
- const result = runCli('validate-config --help');
504
- expect(result.exitCode).toBe(0);
505
- expect(result.output).toContain('Validate Dashboard Configuration');
506
- expect(result.output).toContain('Usage:');
507
- expect(result.output).toContain('Options:');
508
- expect(result.output).toContain('--json');
509
- expect(result.output).toContain('--strict');
510
- });
511
-
512
- test('shows help with -h', () => {
513
- const result = runCli('validate-config -h');
514
- expect(result.exitCode).toBe(0);
515
- expect(result.output).toContain('Validate Dashboard Configuration');
516
- });
517
- });
518
-
519
- describe('validation', () => {
520
- test('validates a valid config file', () => {
521
- const validConfig = {
522
- refreshInterval: 2000,
523
- theme: 'dark',
524
- logLevelFilter: 'info',
525
- sessionSortMode: 'time',
526
- };
527
- const configPath = join(tempDir, 'valid-config.json');
528
- writeFileSync(configPath, JSON.stringify(validConfig, null, 2));
529
-
530
- const result = runCli(`validate-config ${configPath}`);
531
- expect(result.exitCode).toBe(0);
532
- expect(result.output).toContain('is valid');
533
- });
534
-
535
- test('returns error for non-existent file', () => {
536
- const result = runCli('validate-config /nonexistent/path/config.json');
537
- expect(result.exitCode).toBe(1);
538
- expect(result.output).toContain('not found');
539
- });
540
-
541
- test('returns error for invalid JSON', () => {
542
- const configPath = join(tempDir, 'invalid.json');
543
- writeFileSync(configPath, 'not valid json {');
544
-
545
- const result = runCli(`validate-config ${configPath}`);
546
- expect(result.exitCode).toBe(1);
547
- expect(result.output).toContain('parse');
548
- });
549
-
550
- test('detects invalid theme', () => {
551
- const config = { theme: 'invalid-theme' };
552
- const configPath = join(tempDir, 'bad-theme.json');
553
- writeFileSync(configPath, JSON.stringify(config, null, 2));
554
-
555
- const result = runCli(`validate-config ${configPath}`);
556
- expect(result.exitCode).toBe(1);
557
- expect(result.output).toContain('theme');
558
- });
559
-
560
- test('detects multiple validation errors', () => {
561
- const config = {
562
- theme: 'invalid',
563
- logLevelFilter: 'verbose',
564
- refreshInterval: 100,
565
- };
566
- const configPath = join(tempDir, 'multiple-errors.json');
567
- writeFileSync(configPath, JSON.stringify(config, null, 2));
568
-
569
- const result = runCli(`validate-config ${configPath}`);
570
- expect(result.exitCode).toBe(1);
571
- expect(result.errors?.length > 1 || result.output.split('✗').length > 2).toBeTruthy();
572
- });
573
- });
574
-
575
- describe('json output', () => {
576
- test('outputs valid result as JSON with --json', () => {
577
- const config = { refreshInterval: 2000, theme: 'auto' };
578
- const configPath = join(tempDir, 'config.json');
579
- writeFileSync(configPath, JSON.stringify(config, null, 2));
580
-
581
- const result = runCli(`validate-config ${configPath} --json`);
582
- expect(result.exitCode).toBe(0);
583
- const json = JSON.parse(result.output);
584
- expect(json.valid).toBe(true);
585
- expect(json.path).toBeDefined();
586
- expect(json.stats).toBeDefined();
587
- });
588
-
589
- test('outputs invalid result as JSON with -j', () => {
590
- const config = { theme: 'invalid' };
591
- const configPath = join(tempDir, 'config.json');
592
- writeFileSync(configPath, JSON.stringify(config, null, 2));
593
-
594
- const result = runCli(`validate-config ${configPath} -j`);
595
- expect(result.exitCode).toBe(1);
596
- const json = JSON.parse(result.output);
597
- expect(json.valid).toBe(false);
598
- expect(json.errors).toBeDefined();
599
- expect(json.errors.length).toBeGreaterThan(0);
600
- });
601
- });
602
-
603
- describe('strict mode', () => {
604
- test('accepts unknown properties without --strict', () => {
605
- const config = { customField: 'value', refreshInterval: 2000 };
606
- const configPath = join(tempDir, 'config.json');
607
- writeFileSync(configPath, JSON.stringify(config, null, 2));
608
-
609
- const result = runCli(`validate-config ${configPath}`);
610
- expect(result.exitCode).toBe(0);
611
- });
612
-
613
- test('rejects unknown properties with --strict', () => {
614
- const config = { customField: 'value', refreshInterval: 2000 };
615
- const configPath = join(tempDir, 'config.json');
616
- writeFileSync(configPath, JSON.stringify(config, null, 2));
617
-
618
- const result = runCli(`validate-config ${configPath} --strict`);
619
- expect(result.exitCode).toBe(1);
620
- expect(result.output).toContain('Unknown');
621
- });
622
-
623
- test('rejects unknown properties with -s', () => {
624
- const config = { customField: 'value' };
625
- const configPath = join(tempDir, 'config.json');
626
- writeFileSync(configPath, JSON.stringify(config, null, 2));
627
-
628
- const result = runCli(`validate-config ${configPath} -s`);
629
- expect(result.exitCode).toBe(1);
630
- });
631
- });
632
-
633
- describe('default config path', () => {
634
- test('uses default path when no path provided', () => {
635
- // When no path is provided, uses the default config path
636
- // Result depends on whether default config exists in environment
637
- const result = runCli('validate-config');
638
- // Should either succeed (if file exists) or fail with file not found
639
- expect(result.exitCode).toBeGreaterThanOrEqual(0);
640
- expect(result.output).toContain('Configuration');
641
- });
642
-
643
- test('shows file not found for missing default config', () => {
644
- // Test with a path that definitely doesn't exist
645
- const result = runCli('validate-config /nonexistent/path/dashboard-settings.json');
646
- expect(result.exitCode).toBe(1);
647
- expect(result.output).toContain('not found');
648
- });
649
- });
650
-
651
- describe('complex configurations', () => {
652
- test('validates config with gateway endpoints', () => {
653
- const config = {
654
- refreshInterval: 2000,
655
- gatewayEndpoints: [
656
- { name: 'local', host: 'localhost', port: 18789, enabled: true, type: 'local' },
657
- { name: 'remote', host: '192.168.1.100', port: 18789, token: 'secret', type: 'remote' }
658
- ]
659
- };
660
- const configPath = join(tempDir, 'config.json');
661
- writeFileSync(configPath, JSON.stringify(config, null, 2));
662
-
663
- const result = runCli(`validate-config ${configPath}`);
664
- expect(result.exitCode).toBe(0);
665
- });
666
-
667
- test('validates config with web interface', () => {
668
- const config = {
669
- webInterface: {
670
- enabled: true,
671
- port: 18790,
672
- host: '0.0.0.0',
673
- cors: true,
674
- corsOrigins: ['https://example.com'],
675
- rateLimit: {
676
- enabled: true,
677
- windowMs: 60000,
678
- maxRequests: 100,
679
- },
680
- auth: {
681
- enabled: false,
682
- keys: []
683
- }
684
- }
685
- };
686
- const configPath = join(tempDir, 'config.json');
687
- writeFileSync(configPath, JSON.stringify(config, null, 2));
688
-
689
- const result = runCli(`validate-config ${configPath}`);
690
- expect(result.exitCode).toBe(0);
691
- });
692
-
693
- test('validates config with widget loading settings', () => {
694
- const config = {
695
- widgetLoading: {
696
- enabled: true,
697
- preloadPriority: ['cpu', 'memory', 'gpu'],
698
- lazyLoadDelay: 1000,
699
- maxConcurrent: 5,
700
- autoDiscover: true,
701
- }
702
- };
703
- const configPath = join(tempDir, 'config.json');
704
- writeFileSync(configPath, JSON.stringify(config, null, 2));
705
-
706
- const result = runCli(`validate-config ${configPath}`);
707
- expect(result.exitCode).toBe(0);
708
- });
709
- });
710
- });