claw-dashboard 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/docs/API.md +1 -2
- package/index.js +31 -41
- package/package.json +22 -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/.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/removed-dependencies.test.js +0 -74
- 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
package/tests/validation.test.js
DELETED
|
@@ -1,837 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
validateSettings,
|
|
3
|
-
validateRefreshInterval,
|
|
4
|
-
validateLogLevelFilter,
|
|
5
|
-
validateSessionSortMode,
|
|
6
|
-
validateTheme,
|
|
7
|
-
validateExportFormat,
|
|
8
|
-
validateExportDirectory,
|
|
9
|
-
validateWidgetVisibility,
|
|
10
|
-
validateAlertThresholds,
|
|
11
|
-
validatePath,
|
|
12
|
-
validateType,
|
|
13
|
-
validateGatewayEndpoint,
|
|
14
|
-
getDefaultSettings,
|
|
15
|
-
VALID_THEMES,
|
|
16
|
-
VALID_SORT_MODES,
|
|
17
|
-
VALID_LOG_LEVELS,
|
|
18
|
-
VALID_EXPORT_FORMATS
|
|
19
|
-
} from '../src/validation.js';
|
|
20
|
-
import config from '../src/config.js';
|
|
21
|
-
import os from 'os';
|
|
22
|
-
import { mkdtempSync, writeFileSync, mkdirSync, rmdirSync, rmSync } from 'fs';
|
|
23
|
-
import { join } from 'path';
|
|
24
|
-
|
|
25
|
-
describe('Validation Module', () => {
|
|
26
|
-
describe('validatePath', () => {
|
|
27
|
-
let tempDir;
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
tempDir = mkdtempSync(join(os.tmpdir(), 'validation-test-'));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
try {
|
|
35
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
36
|
-
} catch {}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('returns error for non-string path', () => {
|
|
40
|
-
const result = validatePath(123);
|
|
41
|
-
expect(result.valid).toBe(false);
|
|
42
|
-
expect(result.error).toBe('Path must be a non-empty string');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('returns error for empty path', () => {
|
|
46
|
-
const result = validatePath('');
|
|
47
|
-
expect(result.valid).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('returns error for null path', () => {
|
|
51
|
-
const result = validatePath(null);
|
|
52
|
-
expect(result.valid).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('rejects paths with traversal', () => {
|
|
56
|
-
const result = validatePath('/etc/../passwd');
|
|
57
|
-
expect(result.valid).toBe(false);
|
|
58
|
-
expect(result.error).toBe('Path traversal not allowed');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('expands tilde to home directory', () => {
|
|
62
|
-
const result = validatePath('~/test');
|
|
63
|
-
expect(result.valid).toBe(true);
|
|
64
|
-
// The implementation uses resolve() which may produce different path formats
|
|
65
|
-
expect(result.resolvedPath).toContain('test');
|
|
66
|
-
expect(result.resolvedPath).not.toContain('~');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('validates existing path when mustExist is true', () => {
|
|
70
|
-
const result = validatePath(tempDir, true);
|
|
71
|
-
expect(result.valid).toBe(true);
|
|
72
|
-
expect(result.resolvedPath).toBe(tempDir);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('returns error for non-existent path when mustExist is true', () => {
|
|
76
|
-
const result = validatePath('/nonexistent/path/12345', true);
|
|
77
|
-
expect(result.valid).toBe(false);
|
|
78
|
-
expect(result.error).toContain('does not exist');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('returns warning for non-existent parent directory', () => {
|
|
82
|
-
const result = validatePath('/tmp/nonexistent/deep/path');
|
|
83
|
-
expect(result.valid).toBe(true);
|
|
84
|
-
expect(result.warning).toContain('Parent directory will be created');
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe('validateRefreshInterval', () => {
|
|
89
|
-
test('returns default for undefined', () => {
|
|
90
|
-
const result = validateRefreshInterval(undefined);
|
|
91
|
-
expect(result.valid).toBe(true);
|
|
92
|
-
expect(result.value).toBe(config.REFRESH_INTERVALS.DEFAULT);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('returns default for null', () => {
|
|
96
|
-
const result = validateRefreshInterval(null);
|
|
97
|
-
expect(result.valid).toBe(true);
|
|
98
|
-
expect(result.value).toBe(config.REFRESH_INTERVALS.DEFAULT);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('accepts valid number within range', () => {
|
|
102
|
-
const result = validateRefreshInterval(2000);
|
|
103
|
-
expect(result.valid).toBe(true);
|
|
104
|
-
expect(result.value).toBe(2000);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('converts string numbers', () => {
|
|
108
|
-
const result = validateRefreshInterval('3000');
|
|
109
|
-
expect(result.valid).toBe(true);
|
|
110
|
-
expect(result.value).toBe(3000);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('rejects NaN values', () => {
|
|
114
|
-
const result = validateRefreshInterval('invalid');
|
|
115
|
-
expect(result.valid).toBe(false);
|
|
116
|
-
expect(result.error).toContain('must be a number');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('rejects values below minimum', () => {
|
|
120
|
-
const result = validateRefreshInterval(100);
|
|
121
|
-
expect(result.valid).toBe(false);
|
|
122
|
-
expect(result.error).toContain('between');
|
|
123
|
-
expect(result.error).toContain(String(config.VALIDATION.REFRESH_INTERVAL.MIN));
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('rejects values above maximum', () => {
|
|
127
|
-
const result = validateRefreshInterval(100000);
|
|
128
|
-
expect(result.valid).toBe(false);
|
|
129
|
-
expect(result.error).toContain('between');
|
|
130
|
-
expect(result.error).toContain(String(config.VALIDATION.REFRESH_INTERVAL.MAX));
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('accepts minimum boundary value', () => {
|
|
134
|
-
const result = validateRefreshInterval(config.VALIDATION.REFRESH_INTERVAL.MIN);
|
|
135
|
-
expect(result.valid).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('accepts maximum boundary value', () => {
|
|
139
|
-
const result = validateRefreshInterval(config.VALIDATION.REFRESH_INTERVAL.MAX);
|
|
140
|
-
expect(result.valid).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('validateLogLevelFilter', () => {
|
|
145
|
-
test('returns default for undefined', () => {
|
|
146
|
-
const result = validateLogLevelFilter(undefined);
|
|
147
|
-
expect(result.valid).toBe(true);
|
|
148
|
-
expect(result.value).toBe('all');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('returns default for empty string', () => {
|
|
152
|
-
const result = validateLogLevelFilter('');
|
|
153
|
-
expect(result.valid).toBe(true);
|
|
154
|
-
expect(result.value).toBe('all');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('returns default for null', () => {
|
|
158
|
-
const result = validateLogLevelFilter(null);
|
|
159
|
-
expect(result.valid).toBe(true);
|
|
160
|
-
expect(result.value).toBe('all');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('accepts valid log levels', () => {
|
|
164
|
-
for (const level of VALID_LOG_LEVELS) {
|
|
165
|
-
const result = validateLogLevelFilter(level);
|
|
166
|
-
expect(result.valid).toBe(true);
|
|
167
|
-
expect(result.value).toBe(level);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('normalizes to lowercase', () => {
|
|
172
|
-
const result = validateLogLevelFilter('ERROR');
|
|
173
|
-
expect(result.valid).toBe(true);
|
|
174
|
-
expect(result.value).toBe('error');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('rejects non-string values', () => {
|
|
178
|
-
const result = validateLogLevelFilter(123);
|
|
179
|
-
expect(result.valid).toBe(false);
|
|
180
|
-
expect(result.error).toContain('must be a string');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('rejects invalid log levels', () => {
|
|
184
|
-
const result = validateLogLevelFilter('invalid');
|
|
185
|
-
expect(result.valid).toBe(false);
|
|
186
|
-
expect(result.error).toContain('Invalid log level');
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('validateSessionSortMode', () => {
|
|
191
|
-
test('returns default for undefined', () => {
|
|
192
|
-
const result = validateSessionSortMode(undefined);
|
|
193
|
-
expect(result.valid).toBe(true);
|
|
194
|
-
expect(result.value).toBe('time');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test('returns default for null', () => {
|
|
198
|
-
const result = validateSessionSortMode(null);
|
|
199
|
-
expect(result.valid).toBe(true);
|
|
200
|
-
expect(result.value).toBe('time');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('accepts valid sort modes', () => {
|
|
204
|
-
for (const mode of VALID_SORT_MODES) {
|
|
205
|
-
const result = validateSessionSortMode(mode);
|
|
206
|
-
expect(result.valid).toBe(true);
|
|
207
|
-
expect(result.value).toBe(mode);
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('normalizes to lowercase', () => {
|
|
212
|
-
const result = validateSessionSortMode('TOKENS');
|
|
213
|
-
expect(result.valid).toBe(true);
|
|
214
|
-
expect(result.value).toBe('tokens');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test('rejects non-string values', () => {
|
|
218
|
-
const result = validateSessionSortMode(true);
|
|
219
|
-
expect(result.valid).toBe(false);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('rejects invalid sort modes', () => {
|
|
223
|
-
const result = validateSessionSortMode('invalid');
|
|
224
|
-
expect(result.valid).toBe(false);
|
|
225
|
-
expect(result.error).toContain('Invalid sort mode');
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe('validateTheme', () => {
|
|
230
|
-
test('returns default for undefined', () => {
|
|
231
|
-
const result = validateTheme(undefined);
|
|
232
|
-
expect(result.valid).toBe(true);
|
|
233
|
-
expect(result.value).toBe('default');
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test('returns default for null', () => {
|
|
237
|
-
const result = validateTheme(null);
|
|
238
|
-
expect(result.valid).toBe(true);
|
|
239
|
-
expect(result.value).toBe('default');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test('accepts valid themes', () => {
|
|
243
|
-
for (const theme of VALID_THEMES) {
|
|
244
|
-
const result = validateTheme(theme);
|
|
245
|
-
expect(result.valid).toBe(true);
|
|
246
|
-
expect(result.value).toBe(theme);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('normalizes to lowercase', () => {
|
|
251
|
-
const result = validateTheme('DARK');
|
|
252
|
-
expect(result.valid).toBe(true);
|
|
253
|
-
expect(result.value).toBe('dark');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test('rejects non-string values', () => {
|
|
257
|
-
const result = validateTheme({});
|
|
258
|
-
expect(result.valid).toBe(false);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test('rejects invalid themes', () => {
|
|
262
|
-
const result = validateTheme('invalid');
|
|
263
|
-
expect(result.valid).toBe(false);
|
|
264
|
-
expect(result.error).toContain('Invalid theme');
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
describe('validateExportFormat', () => {
|
|
269
|
-
test('returns default for undefined', () => {
|
|
270
|
-
const result = validateExportFormat(undefined);
|
|
271
|
-
expect(result.valid).toBe(true);
|
|
272
|
-
expect(result.value).toBe('json');
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test('returns default for null', () => {
|
|
276
|
-
const result = validateExportFormat(null);
|
|
277
|
-
expect(result.valid).toBe(true);
|
|
278
|
-
expect(result.value).toBe('json');
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test('accepts valid export formats', () => {
|
|
282
|
-
for (const format of VALID_EXPORT_FORMATS) {
|
|
283
|
-
const result = validateExportFormat(format);
|
|
284
|
-
expect(result.valid).toBe(true);
|
|
285
|
-
expect(result.value).toBe(format);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
test('normalizes to lowercase', () => {
|
|
290
|
-
const result = validateExportFormat('CSV');
|
|
291
|
-
expect(result.valid).toBe(true);
|
|
292
|
-
expect(result.value).toBe('csv');
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test('rejects non-string values', () => {
|
|
296
|
-
const result = validateExportFormat([]);
|
|
297
|
-
expect(result.valid).toBe(false);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test('rejects invalid export formats', () => {
|
|
301
|
-
const result = validateExportFormat('xml');
|
|
302
|
-
expect(result.valid).toBe(false);
|
|
303
|
-
expect(result.error).toContain('Invalid export format');
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
describe('validateExportDirectory', () => {
|
|
308
|
-
test('returns default for undefined', () => {
|
|
309
|
-
const result = validateExportDirectory(undefined);
|
|
310
|
-
expect(result.valid).toBe(true);
|
|
311
|
-
expect(result.value).toBe(config.PATHS.EXPORTS);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test('returns default for null', () => {
|
|
315
|
-
const result = validateExportDirectory(null);
|
|
316
|
-
expect(result.valid).toBe(true);
|
|
317
|
-
expect(result.value).toBe(config.PATHS.EXPORTS);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test('rejects non-string values', () => {
|
|
321
|
-
const result = validateExportDirectory(123);
|
|
322
|
-
expect(result.valid).toBe(false);
|
|
323
|
-
expect(result.error).toContain('must be a string');
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test('expands tilde in path', () => {
|
|
327
|
-
const result = validateExportDirectory('~/exports');
|
|
328
|
-
expect(result.valid).toBe(true);
|
|
329
|
-
// The implementation uses resolve() which may produce different path formats
|
|
330
|
-
expect(result.resolvedPath).toContain('exports');
|
|
331
|
-
expect(result.resolvedPath).not.toContain('~');
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
describe('validateWidgetVisibility', () => {
|
|
336
|
-
test('returns default true for undefined', () => {
|
|
337
|
-
const result = validateWidgetVisibility(undefined);
|
|
338
|
-
expect(result.valid).toBe(true);
|
|
339
|
-
expect(result.value).toBe(true);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test('returns default true for null', () => {
|
|
343
|
-
const result = validateWidgetVisibility(null);
|
|
344
|
-
expect(result.valid).toBe(true);
|
|
345
|
-
expect(result.value).toBe(true);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test('accepts boolean true', () => {
|
|
349
|
-
const result = validateWidgetVisibility(true);
|
|
350
|
-
expect(result.valid).toBe(true);
|
|
351
|
-
expect(result.value).toBe(true);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
test('accepts boolean false', () => {
|
|
355
|
-
const result = validateWidgetVisibility(false);
|
|
356
|
-
expect(result.valid).toBe(true);
|
|
357
|
-
expect(result.value).toBe(false);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test('parses string "true"', () => {
|
|
361
|
-
const result = validateWidgetVisibility('true');
|
|
362
|
-
expect(result.valid).toBe(true);
|
|
363
|
-
expect(result.value).toBe(true);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
test('parses string "1"', () => {
|
|
367
|
-
const result = validateWidgetVisibility('1');
|
|
368
|
-
expect(result.valid).toBe(true);
|
|
369
|
-
expect(result.value).toBe(true);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('parses string "yes"', () => {
|
|
373
|
-
const result = validateWidgetVisibility('yes');
|
|
374
|
-
expect(result.valid).toBe(true);
|
|
375
|
-
expect(result.value).toBe(true);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
test('parses string "false"', () => {
|
|
379
|
-
const result = validateWidgetVisibility('false');
|
|
380
|
-
expect(result.valid).toBe(true);
|
|
381
|
-
expect(result.value).toBe(false);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
test('parses string "0"', () => {
|
|
385
|
-
const result = validateWidgetVisibility('0');
|
|
386
|
-
expect(result.valid).toBe(true);
|
|
387
|
-
expect(result.value).toBe(false);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test('parses string "no"', () => {
|
|
391
|
-
const result = validateWidgetVisibility('no');
|
|
392
|
-
expect(result.valid).toBe(true);
|
|
393
|
-
expect(result.value).toBe(false);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
test('rejects invalid string values', () => {
|
|
397
|
-
const result = validateWidgetVisibility('maybe');
|
|
398
|
-
expect(result.valid).toBe(false);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test('coerces truthy values to boolean', () => {
|
|
402
|
-
const result = validateWidgetVisibility(1);
|
|
403
|
-
expect(result.valid).toBe(false);
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
describe('validateAlertThresholds', () => {
|
|
408
|
-
test('rejects non-object values', () => {
|
|
409
|
-
const result = validateAlertThresholds('invalid');
|
|
410
|
-
expect(result.valid).toBe(false);
|
|
411
|
-
expect(result.error).toContain('must be an object');
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
test('rejects null', () => {
|
|
415
|
-
const result = validateAlertThresholds(null);
|
|
416
|
-
expect(result.valid).toBe(false);
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
test('accepts empty object', () => {
|
|
420
|
-
const result = validateAlertThresholds({});
|
|
421
|
-
expect(result.valid).toBe(true);
|
|
422
|
-
expect(result.value).toEqual({});
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test('validates CPU thresholds', () => {
|
|
426
|
-
const result = validateAlertThresholds({
|
|
427
|
-
cpu: { warning: 70, critical: 90 }
|
|
428
|
-
});
|
|
429
|
-
expect(result.valid).toBe(true);
|
|
430
|
-
expect(result.value.cpu).toEqual({ warning: 70, critical: 90 });
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
test('validates memory thresholds', () => {
|
|
434
|
-
const result = validateAlertThresholds({
|
|
435
|
-
memory: { warning: 75, critical: 90 }
|
|
436
|
-
});
|
|
437
|
-
expect(result.valid).toBe(true);
|
|
438
|
-
expect(result.value.memory).toEqual({ warning: 75, critical: 90 });
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
test('validates disk thresholds with different defaults', () => {
|
|
442
|
-
const result = validateAlertThresholds({
|
|
443
|
-
disk: { warning: 80, critical: 95 }
|
|
444
|
-
});
|
|
445
|
-
expect(result.valid).toBe(true);
|
|
446
|
-
expect(result.value.disk).toEqual({ warning: 80, critical: 95 });
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
test('applies default thresholds when not specified', () => {
|
|
450
|
-
const result = validateAlertThresholds({ cpu: {} });
|
|
451
|
-
expect(result.valid).toBe(true);
|
|
452
|
-
expect(result.value.cpu.warning).toBe(70);
|
|
453
|
-
expect(result.value.cpu.critical).toBe(90);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test('rejects non-numeric warning values', () => {
|
|
457
|
-
const result = validateAlertThresholds({
|
|
458
|
-
cpu: { warning: 'high' }
|
|
459
|
-
});
|
|
460
|
-
expect(result.valid).toBe(false);
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
test('rejects warning values below 0', () => {
|
|
464
|
-
const result = validateAlertThresholds({
|
|
465
|
-
cpu: { warning: -1 }
|
|
466
|
-
});
|
|
467
|
-
expect(result.valid).toBe(false);
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
test('rejects warning values above 100', () => {
|
|
471
|
-
const result = validateAlertThresholds({
|
|
472
|
-
cpu: { warning: 101 }
|
|
473
|
-
});
|
|
474
|
-
expect(result.valid).toBe(false);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
test('rejects critical values below warning', () => {
|
|
478
|
-
const result = validateAlertThresholds({
|
|
479
|
-
cpu: { warning: 90, critical: 80 }
|
|
480
|
-
});
|
|
481
|
-
expect(result.valid).toBe(false);
|
|
482
|
-
expect(result.error).toContain('critical threshold must be >= warning');
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
test('accepts critical equal to warning', () => {
|
|
486
|
-
const result = validateAlertThresholds({
|
|
487
|
-
cpu: { warning: 80, critical: 80 }
|
|
488
|
-
});
|
|
489
|
-
expect(result.valid).toBe(true);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
test('rejects non-object threshold type', () => {
|
|
493
|
-
const result = validateAlertThresholds({
|
|
494
|
-
cpu: 'invalid'
|
|
495
|
-
});
|
|
496
|
-
expect(result.valid).toBe(false);
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe('validateType', () => {
|
|
501
|
-
test('validates number type', () => {
|
|
502
|
-
expect(validateType(42, 'number')).toBe(true);
|
|
503
|
-
expect(validateType(NaN, 'number')).toBe(false);
|
|
504
|
-
expect(validateType('42', 'number')).toBe(false);
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
test('validates string type', () => {
|
|
508
|
-
expect(validateType('hello', 'string')).toBe(true);
|
|
509
|
-
expect(validateType(123, 'string')).toBe(false);
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
test('validates boolean type', () => {
|
|
513
|
-
expect(validateType(true, 'boolean')).toBe(true);
|
|
514
|
-
expect(validateType(false, 'boolean')).toBe(true);
|
|
515
|
-
expect(validateType('true', 'boolean')).toBe(false);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
test('validates object type', () => {
|
|
519
|
-
expect(validateType({}, 'object')).toBe(true);
|
|
520
|
-
expect(validateType({ key: 'value' }, 'object')).toBe(true);
|
|
521
|
-
expect(validateType(null, 'object')).toBe(false);
|
|
522
|
-
expect(validateType([], 'object')).toBe(true);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
test('returns false for unknown type', () => {
|
|
526
|
-
expect(validateType('test', 'unknown')).toBe(false);
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
describe('validateGatewayEndpoint', () => {
|
|
531
|
-
test('rejects non-object values', () => {
|
|
532
|
-
const result = validateGatewayEndpoint('invalid');
|
|
533
|
-
expect(result.valid).toBe(false);
|
|
534
|
-
expect(result.error).toContain('must be an object');
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test('rejects null', () => {
|
|
538
|
-
const result = validateGatewayEndpoint(null);
|
|
539
|
-
expect(result.valid).toBe(false);
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
test('rejects missing name', () => {
|
|
543
|
-
const result = validateGatewayEndpoint({
|
|
544
|
-
host: 'localhost',
|
|
545
|
-
port: 18789
|
|
546
|
-
});
|
|
547
|
-
expect(result.valid).toBe(false);
|
|
548
|
-
expect(result.error).toContain('name is required');
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test('rejects empty name', () => {
|
|
552
|
-
const result = validateGatewayEndpoint({
|
|
553
|
-
name: '',
|
|
554
|
-
host: 'localhost',
|
|
555
|
-
port: 18789
|
|
556
|
-
});
|
|
557
|
-
expect(result.valid).toBe(false);
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
test('rejects name exceeding max length', () => {
|
|
561
|
-
const result = validateGatewayEndpoint({
|
|
562
|
-
name: 'a'.repeat(50),
|
|
563
|
-
host: 'localhost',
|
|
564
|
-
port: 18789
|
|
565
|
-
});
|
|
566
|
-
expect(result.valid).toBe(false);
|
|
567
|
-
expect(result.error).toContain('at most');
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
test('rejects name with invalid characters', () => {
|
|
571
|
-
const result = validateGatewayEndpoint({
|
|
572
|
-
name: 'my endpoint!',
|
|
573
|
-
host: 'localhost',
|
|
574
|
-
port: 18789
|
|
575
|
-
});
|
|
576
|
-
expect(result.valid).toBe(false);
|
|
577
|
-
expect(result.error).toContain('alphanumeric');
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
test('rejects missing host', () => {
|
|
581
|
-
const result = validateGatewayEndpoint({
|
|
582
|
-
name: 'local',
|
|
583
|
-
port: 18789
|
|
584
|
-
});
|
|
585
|
-
expect(result.valid).toBe(false);
|
|
586
|
-
expect(result.error).toContain('host is required');
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
test('rejects empty host', () => {
|
|
590
|
-
const result = validateGatewayEndpoint({
|
|
591
|
-
name: 'local',
|
|
592
|
-
host: '',
|
|
593
|
-
port: 18789
|
|
594
|
-
});
|
|
595
|
-
expect(result.valid).toBe(false);
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
test('rejects invalid port (0)', () => {
|
|
599
|
-
const result = validateGatewayEndpoint({
|
|
600
|
-
name: 'local',
|
|
601
|
-
host: 'localhost',
|
|
602
|
-
port: 0
|
|
603
|
-
});
|
|
604
|
-
expect(result.valid).toBe(false);
|
|
605
|
-
expect(result.error).toContain('valid port number');
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
test('rejects invalid port (negative)', () => {
|
|
609
|
-
const result = validateGatewayEndpoint({
|
|
610
|
-
name: 'local',
|
|
611
|
-
host: 'localhost',
|
|
612
|
-
port: -1
|
|
613
|
-
});
|
|
614
|
-
expect(result.valid).toBe(false);
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
test('rejects invalid port (above 65535)', () => {
|
|
618
|
-
const result = validateGatewayEndpoint({
|
|
619
|
-
name: 'local',
|
|
620
|
-
host: 'localhost',
|
|
621
|
-
port: 70000
|
|
622
|
-
});
|
|
623
|
-
expect(result.valid).toBe(false);
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
test('rejects invalid port type', () => {
|
|
627
|
-
const result = validateGatewayEndpoint({
|
|
628
|
-
name: 'local',
|
|
629
|
-
host: 'localhost',
|
|
630
|
-
port: 'invalid'
|
|
631
|
-
});
|
|
632
|
-
expect(result.valid).toBe(false);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
test('accepts valid endpoint configuration', () => {
|
|
636
|
-
const result = validateGatewayEndpoint({
|
|
637
|
-
name: 'local',
|
|
638
|
-
host: 'localhost',
|
|
639
|
-
port: 18789
|
|
640
|
-
});
|
|
641
|
-
expect(result.valid).toBe(true);
|
|
642
|
-
expect(result.value).toEqual({
|
|
643
|
-
name: 'local',
|
|
644
|
-
host: 'localhost',
|
|
645
|
-
port: 18789,
|
|
646
|
-
token: null,
|
|
647
|
-
enabled: true,
|
|
648
|
-
type: 'local'
|
|
649
|
-
});
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
test('accepts endpoint with valid type', () => {
|
|
653
|
-
const result = validateGatewayEndpoint({
|
|
654
|
-
name: 'remote',
|
|
655
|
-
host: '192.168.1.1',
|
|
656
|
-
port: 18789,
|
|
657
|
-
type: 'remote'
|
|
658
|
-
});
|
|
659
|
-
expect(result.valid).toBe(true);
|
|
660
|
-
expect(result.value.type).toBe('remote');
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
test('rejects invalid endpoint type', () => {
|
|
664
|
-
const result = validateGatewayEndpoint({
|
|
665
|
-
name: 'local',
|
|
666
|
-
host: 'localhost',
|
|
667
|
-
port: 18789,
|
|
668
|
-
type: 'invalid'
|
|
669
|
-
});
|
|
670
|
-
expect(result.valid).toBe(false);
|
|
671
|
-
expect(result.error).toContain('type must be one of');
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
test('rejects non-boolean enabled', () => {
|
|
675
|
-
const result = validateGatewayEndpoint({
|
|
676
|
-
name: 'local',
|
|
677
|
-
host: 'localhost',
|
|
678
|
-
port: 18789,
|
|
679
|
-
enabled: 'yes'
|
|
680
|
-
});
|
|
681
|
-
expect(result.valid).toBe(false);
|
|
682
|
-
expect(result.error).toContain('enabled must be a boolean');
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
test('rejects non-string/null token', () => {
|
|
686
|
-
const result = validateGatewayEndpoint({
|
|
687
|
-
name: 'local',
|
|
688
|
-
host: 'localhost',
|
|
689
|
-
port: 18789,
|
|
690
|
-
token: 123
|
|
691
|
-
});
|
|
692
|
-
expect(result.valid).toBe(false);
|
|
693
|
-
expect(result.error).toContain('token must be a string or null');
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
test('accepts endpoint with token', () => {
|
|
697
|
-
const result = validateGatewayEndpoint({
|
|
698
|
-
name: 'remote',
|
|
699
|
-
host: 'remote.example.com',
|
|
700
|
-
port: 18789,
|
|
701
|
-
token: 'secret-token-123'
|
|
702
|
-
});
|
|
703
|
-
expect(result.valid).toBe(true);
|
|
704
|
-
expect(result.value.token).toBe('secret-token-123');
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
test('preserves explicit enabled=false', () => {
|
|
708
|
-
const result = validateGatewayEndpoint({
|
|
709
|
-
name: 'local',
|
|
710
|
-
host: 'localhost',
|
|
711
|
-
port: 18789,
|
|
712
|
-
enabled: false
|
|
713
|
-
});
|
|
714
|
-
expect(result.valid).toBe(true);
|
|
715
|
-
expect(result.value.enabled).toBe(false);
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
describe('validateSettings', () => {
|
|
720
|
-
test('returns defaults for null', () => {
|
|
721
|
-
const result = validateSettings(null);
|
|
722
|
-
// When input is null, getDefaultSettings() is returned directly
|
|
723
|
-
expect(result.refreshInterval).toBe(config.REFRESH_INTERVALS.DEFAULT);
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
test('returns defaults for non-object', () => {
|
|
727
|
-
const result = validateSettings('invalid');
|
|
728
|
-
// When input is not an object, getDefaultSettings() is returned directly
|
|
729
|
-
expect(result).toBeDefined();
|
|
730
|
-
expect(result.refreshInterval).toBe(config.REFRESH_INTERVALS.DEFAULT);
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
test('validates complete settings object', () => {
|
|
734
|
-
const settings = {
|
|
735
|
-
refreshInterval: 5000,
|
|
736
|
-
logLevelFilter: 'error',
|
|
737
|
-
sessionSortMode: 'tokens',
|
|
738
|
-
theme: 'dark',
|
|
739
|
-
exportFormat: 'csv',
|
|
740
|
-
showWidget1: false,
|
|
741
|
-
showWidget2: true
|
|
742
|
-
};
|
|
743
|
-
const result = validateSettings(settings);
|
|
744
|
-
expect(result.valid).toBe(true);
|
|
745
|
-
expect(result.value.refreshInterval).toBe(5000);
|
|
746
|
-
expect(result.value.logLevelFilter).toBe('error');
|
|
747
|
-
expect(result.value.theme).toBe('dark');
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
test('uses default for invalid refreshInterval', () => {
|
|
751
|
-
const settings = { refreshInterval: 999999 };
|
|
752
|
-
const result = validateSettings(settings);
|
|
753
|
-
expect(result.valid).toBe(true);
|
|
754
|
-
expect(result.value.refreshInterval).toBe(config.REFRESH_INTERVALS.DEFAULT);
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
test('uses default for invalid theme', () => {
|
|
758
|
-
const settings = { theme: 'neon' };
|
|
759
|
-
const result = validateSettings(settings);
|
|
760
|
-
expect(result.valid).toBe(true);
|
|
761
|
-
expect(result.value.theme).toBe('default');
|
|
762
|
-
});
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
describe('getDefaultSettings', () => {
|
|
766
|
-
test('returns complete default settings object', () => {
|
|
767
|
-
const defaults = getDefaultSettings();
|
|
768
|
-
expect(defaults.refreshInterval).toBe(config.REFRESH_INTERVALS.DEFAULT);
|
|
769
|
-
expect(defaults.logLevelFilter).toBe('all');
|
|
770
|
-
expect(defaults.sessionSortMode).toBe('time');
|
|
771
|
-
expect(defaults.theme).toBe('default');
|
|
772
|
-
expect(defaults.exportFormat).toBe('json');
|
|
773
|
-
expect(defaults.exportDirectory).toBe(config.PATHS.EXPORTS);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
test('includes all widget visibility settings', () => {
|
|
777
|
-
const defaults = getDefaultSettings();
|
|
778
|
-
for (let i = 1; i <= 8; i++) {
|
|
779
|
-
expect(defaults[`showWidget${i}`]).toBe(true);
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
test('includes performance metrics flag', () => {
|
|
784
|
-
const defaults = getDefaultSettings();
|
|
785
|
-
expect(defaults.showPerformanceMetrics).toBe(false);
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
test('includes favorites settings', () => {
|
|
789
|
-
const defaults = getDefaultSettings();
|
|
790
|
-
expect(defaults.favorites).toEqual({});
|
|
791
|
-
expect(defaults.showFavoritesOnly).toBe(false);
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
test('includes firstRun flag', () => {
|
|
795
|
-
const defaults = getDefaultSettings();
|
|
796
|
-
expect(defaults.firstRun).toBe(true);
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
test('includes gateway endpoints array', () => {
|
|
800
|
-
const defaults = getDefaultSettings();
|
|
801
|
-
expect(Array.isArray(defaults.gatewayEndpoints)).toBe(true);
|
|
802
|
-
expect(defaults.gatewayEndpoints[0]).toMatchObject({
|
|
803
|
-
name: 'local',
|
|
804
|
-
host: 'localhost',
|
|
805
|
-
enabled: true
|
|
806
|
-
});
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
test('includes web interface config', () => {
|
|
810
|
-
const defaults = getDefaultSettings();
|
|
811
|
-
expect(defaults.webInterface).toMatchObject({
|
|
812
|
-
enabled: false,
|
|
813
|
-
port: config.WEB.DEFAULT_PORT,
|
|
814
|
-
host: config.WEB.HOST,
|
|
815
|
-
cors: true
|
|
816
|
-
});
|
|
817
|
-
});
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
describe('exported constants', () => {
|
|
821
|
-
test('VALID_THEMES matches config', () => {
|
|
822
|
-
expect(VALID_THEMES).toEqual(config.VALIDATION.VALID_THEMES);
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
test('VALID_SORT_MODES matches config', () => {
|
|
826
|
-
expect(VALID_SORT_MODES).toEqual(config.VALIDATION.VALID_SORT_MODES);
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
test('VALID_LOG_LEVELS matches config', () => {
|
|
830
|
-
expect(VALID_LOG_LEVELS).toEqual(config.VALIDATION.VALID_LOG_LEVELS);
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
test('VALID_EXPORT_FORMATS matches config', () => {
|
|
834
|
-
expect(VALID_EXPORT_FORMATS).toEqual(config.VALIDATION.VALID_EXPORT_FORMATS);
|
|
835
|
-
});
|
|
836
|
-
});
|
|
837
|
-
});
|