claw-dashboard 1.9.0 → 2.0.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 +5236 -566
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -5
- 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 +1941 -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 +1057 -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,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for config-processor module
|
|
3
|
+
* Environment variable interpolation, versioning, and migration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
interpolateEnvVars,
|
|
8
|
+
processConfigValues,
|
|
9
|
+
processWidgetConfig,
|
|
10
|
+
validateConfigVersion,
|
|
11
|
+
migrateConfig,
|
|
12
|
+
registerMigration,
|
|
13
|
+
compareVersions,
|
|
14
|
+
extractEnvRequirements,
|
|
15
|
+
createConfigPreprocessor,
|
|
16
|
+
CONFIG_VERSION,
|
|
17
|
+
DEFAULT_PROCESSING_OPTIONS,
|
|
18
|
+
} from '../src/widgets/config-processor.js';
|
|
19
|
+
|
|
20
|
+
describe('config-processor', () => {
|
|
21
|
+
// Save original env
|
|
22
|
+
const originalEnv = process.env;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// Reset process.env before each test
|
|
26
|
+
process.env = { ...originalEnv };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
process.env = originalEnv;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('interpolateEnvVars', () => {
|
|
34
|
+
test('should return non-string values unchanged', () => {
|
|
35
|
+
expect(interpolateEnvVars(123)).toBe(123);
|
|
36
|
+
expect(interpolateEnvVars(true)).toBe(true);
|
|
37
|
+
expect(interpolateEnvVars(null)).toBe(null);
|
|
38
|
+
expect(interpolateEnvVars(undefined)).toBe(undefined);
|
|
39
|
+
expect(interpolateEnvVars({ key: 'value' })).toEqual({ key: 'value' });
|
|
40
|
+
expect(interpolateEnvVars([1, 2, 3])).toEqual([1, 2, 3]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should interpolate simple environment variables', () => {
|
|
44
|
+
process.env.TEST_VAR = 'test_value';
|
|
45
|
+
expect(interpolateEnvVars('${TEST_VAR}')).toBe('test_value');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should interpolate env vars within text', () => {
|
|
49
|
+
process.env.USER_NAME = 'Alice';
|
|
50
|
+
expect(interpolateEnvVars('Hello, ${USER_NAME}!')).toBe('Hello, Alice!');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should use default value when env var is not set', () => {
|
|
54
|
+
delete process.env.MISSING_VAR;
|
|
55
|
+
expect(interpolateEnvVars('${MISSING_VAR:-default}')).toBe('default');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should prefer env var over default when set', () => {
|
|
59
|
+
process.env.EXISTING_VAR = 'actual_value';
|
|
60
|
+
expect(interpolateEnvVars('${EXISTING_VAR:-default}')).toBe('actual_value');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should keep original pattern when env var missing and no default', () => {
|
|
64
|
+
delete process.env.MISSING_VAR;
|
|
65
|
+
expect(interpolateEnvVars('${MISSING_VAR}')).toBe('${MISSING_VAR}');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should handle multiple interpolations', () => {
|
|
69
|
+
process.env.HOST = 'localhost';
|
|
70
|
+
process.env.PORT = '3000';
|
|
71
|
+
expect(interpolateEnvVars('http://${HOST}:${PORT}')).toBe('http://localhost:3000');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('should handle mixed interpolations with and without defaults', () => {
|
|
75
|
+
process.env.API_KEY = 'secret123';
|
|
76
|
+
expect(interpolateEnvVars('Key: ${API_KEY}, Timeout: ${TIMEOUT:-5000}')).toBe('Key: secret123, Timeout: 5000');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should handle empty default values', () => {
|
|
80
|
+
delete process.env.EMPTY_VAR;
|
|
81
|
+
expect(interpolateEnvVars('${EMPTY_VAR:-}')).toBe('');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should handle custom env object', () => {
|
|
85
|
+
const customEnv = { CUSTOM_VAR: 'custom_value' };
|
|
86
|
+
expect(interpolateEnvVars('${CUSTOM_VAR}', customEnv)).toBe('custom_value');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should handle special characters in values', () => {
|
|
90
|
+
process.env.SPECIAL = 'hello@world.com/path?query=1&other=2';
|
|
91
|
+
expect(interpolateEnvVars('${SPECIAL}')).toBe('hello@world.com/path?query=1&other=2');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('processConfigValues', () => {
|
|
96
|
+
test('should process simple string values', () => {
|
|
97
|
+
process.env.API_URL = 'https://api.example.com';
|
|
98
|
+
const config = { url: '${API_URL}' };
|
|
99
|
+
expect(processConfigValues(config)).toEqual({ url: 'https://api.example.com' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('should process nested objects', () => {
|
|
103
|
+
process.env.DB_HOST = 'localhost';
|
|
104
|
+
process.env.DB_PORT = '5432';
|
|
105
|
+
const config = {
|
|
106
|
+
database: {
|
|
107
|
+
host: '${DB_HOST}',
|
|
108
|
+
port: '${DB_PORT}',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
expect(processConfigValues(config)).toEqual({
|
|
112
|
+
database: {
|
|
113
|
+
host: 'localhost',
|
|
114
|
+
port: '5432',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should process arrays', () => {
|
|
120
|
+
process.env.FIRST = 'one';
|
|
121
|
+
process.env.SECOND = 'two';
|
|
122
|
+
const config = ['${FIRST}', '${SECOND}', 'static'];
|
|
123
|
+
expect(processConfigValues(config)).toEqual(['one', 'two', 'static']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should handle mixed types in arrays', () => {
|
|
127
|
+
process.env.STR = 'string';
|
|
128
|
+
const config = ['${STR}', 123, true, null];
|
|
129
|
+
expect(processConfigValues(config)).toEqual(['string', 123, true, null]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should process complex nested structures', () => {
|
|
133
|
+
process.env.API_KEY = 'key123';
|
|
134
|
+
process.env.BASE_URL = 'https://api.test';
|
|
135
|
+
const config = {
|
|
136
|
+
api: {
|
|
137
|
+
key: '${API_KEY}',
|
|
138
|
+
endpoints: [
|
|
139
|
+
'${BASE_URL}/users',
|
|
140
|
+
'${BASE_URL}/posts',
|
|
141
|
+
],
|
|
142
|
+
timeout: 5000,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
expect(processConfigValues(config)).toEqual({
|
|
146
|
+
api: {
|
|
147
|
+
key: 'key123',
|
|
148
|
+
endpoints: ['https://api.test/users', 'https://api.test/posts'],
|
|
149
|
+
timeout: 5000,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('should handle null values', () => {
|
|
155
|
+
const config = { value: null, nested: { empty: null } };
|
|
156
|
+
expect(processConfigValues(config)).toEqual({ value: null, nested: { empty: null } });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('should handle circular references gracefully', () => {
|
|
160
|
+
const config = { a: 'value' };
|
|
161
|
+
config.self = config;
|
|
162
|
+
const result = processConfigValues(config);
|
|
163
|
+
expect(result.a).toBe('value');
|
|
164
|
+
expect(result.self).toBe(config); // Circular ref preserved as-is
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should preserve non-plain objects', () => {
|
|
168
|
+
const date = new Date('2024-01-01');
|
|
169
|
+
const config = { date, regex: /test/g };
|
|
170
|
+
const result = processConfigValues(config);
|
|
171
|
+
expect(result.date).toBe(date);
|
|
172
|
+
expect(result.regex).toEqual(/test/g);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('compareVersions', () => {
|
|
177
|
+
test('should return 0 for equal versions', () => {
|
|
178
|
+
expect(compareVersions('1.0.0', '1.0.0')).toBe(0);
|
|
179
|
+
expect(compareVersions('2.5.3', '2.5.3')).toBe(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('should return -1 when first version is lower', () => {
|
|
183
|
+
expect(compareVersions('1.0.0', '1.0.1')).toBe(-1);
|
|
184
|
+
expect(compareVersions('1.0.0', '1.1.0')).toBe(-1);
|
|
185
|
+
expect(compareVersions('1.0.0', '2.0.0')).toBe(-1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('should return 1 when first version is higher', () => {
|
|
189
|
+
expect(compareVersions('1.0.1', '1.0.0')).toBe(1);
|
|
190
|
+
expect(compareVersions('1.1.0', '1.0.0')).toBe(1);
|
|
191
|
+
expect(compareVersions('2.0.0', '1.0.0')).toBe(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('should handle different length versions', () => {
|
|
195
|
+
expect(compareVersions('1.0', '1.0.0')).toBe(0);
|
|
196
|
+
expect(compareVersions('1', '1.0.0')).toBe(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('should handle versions with non-numeric parts', () => {
|
|
200
|
+
// Non-numeric parts are treated as 0 in simple comparison
|
|
201
|
+
expect(compareVersions('1.0.0-alpha', '1.0.0')).toBe(0); // both become 1.0.0
|
|
202
|
+
expect(compareVersions('2.0.0-beta', '2.0.0')).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('validateConfigVersion', () => {
|
|
207
|
+
test('should validate current version', () => {
|
|
208
|
+
const config = { __version: CONFIG_VERSION.CURRENT };
|
|
209
|
+
const result = validateConfigVersion(config);
|
|
210
|
+
expect(result.valid).toBe(true);
|
|
211
|
+
expect(result.version).toBe(CONFIG_VERSION.CURRENT);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('should validate minimum supported version', () => {
|
|
215
|
+
const config = { __version: CONFIG_VERSION.MIN_SUPPORTED };
|
|
216
|
+
const result = validateConfigVersion(config);
|
|
217
|
+
expect(result.valid).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should reject versions below minimum', () => {
|
|
221
|
+
const config = { __version: '0.9.0' };
|
|
222
|
+
const result = validateConfigVersion(config);
|
|
223
|
+
expect(result.valid).toBe(false);
|
|
224
|
+
expect(result.error).toContain('below minimum supported');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('should reject versions above current', () => {
|
|
228
|
+
const config = { __version: '99.0.0' };
|
|
229
|
+
const result = validateConfigVersion(config);
|
|
230
|
+
expect(result.valid).toBe(false);
|
|
231
|
+
expect(result.error).toContain('newer than current');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('should treat missing version as 1.0.0', () => {
|
|
235
|
+
const config = {};
|
|
236
|
+
const result = validateConfigVersion(config);
|
|
237
|
+
expect(result.valid).toBe(true);
|
|
238
|
+
expect(result.version).toBe('1.0.0');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('should handle null/undefined config', () => {
|
|
242
|
+
expect(validateConfigVersion(null).valid).toBe(true);
|
|
243
|
+
expect(validateConfigVersion(undefined).valid).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('migrateConfig', () => {
|
|
248
|
+
beforeEach(() => {
|
|
249
|
+
// Clear any registered migrations
|
|
250
|
+
// Note: We can't easily clear the Map, so we work with what we have
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('should return same config if version matches', () => {
|
|
254
|
+
const config = { __version: '1.0.0', data: 'test' };
|
|
255
|
+
const result = migrateConfig(config, '1.0.0');
|
|
256
|
+
expect(result.success).toBe(true);
|
|
257
|
+
expect(result.config).toEqual(config);
|
|
258
|
+
expect(result.path).toEqual([]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('should return error for invalid config', () => {
|
|
262
|
+
const result = migrateConfig(null);
|
|
263
|
+
expect(result.success).toBe(false);
|
|
264
|
+
expect(result.error).toContain('Invalid config');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('should return error when no migration path exists', () => {
|
|
268
|
+
const config = { __version: '0.1.0' };
|
|
269
|
+
const result = migrateConfig(config, '2.0.0');
|
|
270
|
+
expect(result.success).toBe(false);
|
|
271
|
+
expect(result.error).toContain('No migration path');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('should apply registered migration', () => {
|
|
275
|
+
// Register a simple migration
|
|
276
|
+
registerMigration('1.0.0', '1.1.0', (config) => ({
|
|
277
|
+
...config,
|
|
278
|
+
newField: 'migrated',
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
const config = { __version: '1.0.0', oldField: 'value' };
|
|
282
|
+
const result = migrateConfig(config, '1.1.0');
|
|
283
|
+
|
|
284
|
+
expect(result.success).toBe(true);
|
|
285
|
+
expect(result.config.newField).toBe('migrated');
|
|
286
|
+
expect(result.config.oldField).toBe('value');
|
|
287
|
+
expect(result.config.__version).toBe('1.1.0');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('extractEnvRequirements', () => {
|
|
292
|
+
test('should extract simple env vars', () => {
|
|
293
|
+
const config = { url: '${API_URL}' };
|
|
294
|
+
const reqs = extractEnvRequirements(config);
|
|
295
|
+
expect(reqs).toEqual([{ name: 'API_URL', hasDefault: false }]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('should extract env vars with defaults', () => {
|
|
299
|
+
const config = { timeout: '${TIMEOUT:-5000}' };
|
|
300
|
+
const reqs = extractEnvRequirements(config);
|
|
301
|
+
expect(reqs).toEqual([{ name: 'TIMEOUT', hasDefault: true, defaultValue: '5000' }]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('should extract from nested objects', () => {
|
|
305
|
+
const config = {
|
|
306
|
+
db: { host: '${DB_HOST}', port: '${DB_PORT:-5432}' },
|
|
307
|
+
};
|
|
308
|
+
const reqs = extractEnvRequirements(config);
|
|
309
|
+
expect(reqs).toHaveLength(2);
|
|
310
|
+
expect(reqs).toContainEqual({ name: 'DB_HOST', hasDefault: false });
|
|
311
|
+
expect(reqs).toContainEqual({ name: 'DB_PORT', hasDefault: true, defaultValue: '5432' });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('should extract from arrays', () => {
|
|
315
|
+
const config = ['${VAR1}', '${VAR2:-default}'];
|
|
316
|
+
const reqs = extractEnvRequirements(config);
|
|
317
|
+
expect(reqs).toHaveLength(2);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('should deduplicate env vars', () => {
|
|
321
|
+
const config = {
|
|
322
|
+
a: '${SAME_VAR}',
|
|
323
|
+
b: '${SAME_VAR}',
|
|
324
|
+
};
|
|
325
|
+
const reqs = extractEnvRequirements(config);
|
|
326
|
+
expect(reqs).toHaveLength(1);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('should handle complex nested structures', () => {
|
|
330
|
+
const config = {
|
|
331
|
+
api: {
|
|
332
|
+
key: '${API_KEY}',
|
|
333
|
+
endpoints: ['${BASE_URL}/users', '${BASE_URL}/posts'],
|
|
334
|
+
},
|
|
335
|
+
timeout: '${TIMEOUT:-30000}',
|
|
336
|
+
};
|
|
337
|
+
const reqs = extractEnvRequirements(config);
|
|
338
|
+
expect(reqs).toHaveLength(3);
|
|
339
|
+
expect(reqs).toContainEqual({ name: 'API_KEY', hasDefault: false });
|
|
340
|
+
expect(reqs).toContainEqual({ name: 'BASE_URL', hasDefault: false });
|
|
341
|
+
expect(reqs).toContainEqual({ name: 'TIMEOUT', hasDefault: true, defaultValue: '30000' });
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('createConfigPreprocessor', () => {
|
|
346
|
+
test('should return a function', () => {
|
|
347
|
+
const preprocessor = createConfigPreprocessor();
|
|
348
|
+
expect(typeof preprocessor).toBe('function');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('should process config when called', () => {
|
|
352
|
+
process.env.PREPROCESS_TEST = 'success';
|
|
353
|
+
const preprocessor = createConfigPreprocessor({ interpolateEnv: true });
|
|
354
|
+
const result = preprocessor({ value: '${PREPROCESS_TEST}' });
|
|
355
|
+
expect(result.success).toBe(true);
|
|
356
|
+
expect(result.config.value).toBe('success');
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('processWidgetConfig', () => {
|
|
361
|
+
test('should successfully process valid config', () => {
|
|
362
|
+
process.env.PROCESS_TEST = 'processed';
|
|
363
|
+
const config = {
|
|
364
|
+
__version: '1.0.0',
|
|
365
|
+
value: '${PROCESS_TEST}',
|
|
366
|
+
};
|
|
367
|
+
const result = processWidgetConfig(config);
|
|
368
|
+
expect(result.success).toBe(true);
|
|
369
|
+
expect(result.config.value).toBe('processed');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('should return warnings for migrations', () => {
|
|
373
|
+
// Register a migration first
|
|
374
|
+
registerMigration('1.0.0', CONFIG_VERSION.CURRENT, (config) => ({
|
|
375
|
+
...config,
|
|
376
|
+
migrated: true,
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
const config = { __version: '1.0.0' };
|
|
380
|
+
const result = processWidgetConfig(config);
|
|
381
|
+
|
|
382
|
+
// Should either succeed with warnings or succeed without (if already migrated)
|
|
383
|
+
if (result.warnings) {
|
|
384
|
+
expect(result.warnings.some(w => w.includes('Migrated'))).toBe(true);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('should skip env interpolation when disabled', () => {
|
|
389
|
+
process.env.SKIP_TEST = 'should_not_appear';
|
|
390
|
+
const config = { value: '${SKIP_TEST}' };
|
|
391
|
+
const result = processWidgetConfig(config, { interpolateEnv: false });
|
|
392
|
+
expect(result.success).toBe(true);
|
|
393
|
+
expect(result.config.value).toBe('${SKIP_TEST}');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('should handle errors gracefully', () => {
|
|
397
|
+
const result = processWidgetConfig(null, { throwOnError: false });
|
|
398
|
+
expect(result.success).toBe(true); // null is valid (returns as-is after processing)
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('should throw when throwOnError is true', () => {
|
|
402
|
+
const config = { __version: '99.99.99' }; // Future version
|
|
403
|
+
expect(() => {
|
|
404
|
+
processWidgetConfig(config, { throwOnError: true });
|
|
405
|
+
}).toThrow();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('should skip version validation when disabled', () => {
|
|
409
|
+
const config = { __version: '99.99.99' }; // Future version
|
|
410
|
+
const result = processWidgetConfig(config, { validateVersion: false });
|
|
411
|
+
expect(result.success).toBe(true);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('should handle multiple processing steps', () => {
|
|
415
|
+
process.env.MULTI_TEST = 'multi_value';
|
|
416
|
+
const config = {
|
|
417
|
+
__version: '1.0.0',
|
|
418
|
+
nested: {
|
|
419
|
+
array: ['${MULTI_TEST}', 'static'],
|
|
420
|
+
number: 42,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
const result = processWidgetConfig(config);
|
|
424
|
+
expect(result.success).toBe(true);
|
|
425
|
+
expect(result.config.nested.array[0]).toBe('multi_value');
|
|
426
|
+
expect(result.config.nested.array[1]).toBe('static');
|
|
427
|
+
expect(result.config.nested.number).toBe(42);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('DEFAULT_PROCESSING_OPTIONS', () => {
|
|
432
|
+
test('should have correct default values', () => {
|
|
433
|
+
expect(DEFAULT_PROCESSING_OPTIONS.interpolateEnv).toBe(true);
|
|
434
|
+
expect(DEFAULT_PROCESSING_OPTIONS.supportLegacy).toBe(true);
|
|
435
|
+
expect(DEFAULT_PROCESSING_OPTIONS.validateVersion).toBe(true);
|
|
436
|
+
expect(DEFAULT_PROCESSING_OPTIONS.throwOnError).toBe(false);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe('CONFIG_VERSION', () => {
|
|
441
|
+
test('should have CURRENT and MIN_SUPPORTED', () => {
|
|
442
|
+
expect(CONFIG_VERSION.CURRENT).toBeDefined();
|
|
443
|
+
expect(CONFIG_VERSION.MIN_SUPPORTED).toBeDefined();
|
|
444
|
+
expect(typeof CONFIG_VERSION.CURRENT).toBe('string');
|
|
445
|
+
expect(typeof CONFIG_VERSION.MIN_SUPPORTED).toBe('string');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('CURRENT should be >= MIN_SUPPORTED', () => {
|
|
449
|
+
expect(compareVersions(CONFIG_VERSION.CURRENT, CONFIG_VERSION.MIN_SUPPORTED)).toBeGreaterThanOrEqual(0);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
});
|