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