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