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,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for widget error isolation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
WidgetErrorIsolator,
|
|
7
|
+
WidgetHealthTracker,
|
|
8
|
+
WidgetIsolatedError,
|
|
9
|
+
WidgetHealthStatus,
|
|
10
|
+
WidgetErrorType,
|
|
11
|
+
executeWithIsolation,
|
|
12
|
+
getWidgetErrorIsolator,
|
|
13
|
+
DEFAULT_ISOLATION_CONFIG,
|
|
14
|
+
} from '../src/widgets/widget-error-isolation.js';
|
|
15
|
+
|
|
16
|
+
// Helper to create a simple mock function
|
|
17
|
+
function createMockFn(returnValue) {
|
|
18
|
+
let callCount = 0;
|
|
19
|
+
const fn = (...args) => {
|
|
20
|
+
callCount++;
|
|
21
|
+
fn.calls.push(args);
|
|
22
|
+
return Promise.resolve(returnValue);
|
|
23
|
+
};
|
|
24
|
+
fn.calls = [];
|
|
25
|
+
fn.mockClear = () => { fn.calls = []; callCount = 0; };
|
|
26
|
+
fn.mockResolvedValue = (val) => {
|
|
27
|
+
const newFn = createMockFn(val);
|
|
28
|
+
return newFn;
|
|
29
|
+
};
|
|
30
|
+
fn.mockRejectedValue = (err) => {
|
|
31
|
+
const newFn = (...args) => {
|
|
32
|
+
callCount++;
|
|
33
|
+
fn.calls.push(args);
|
|
34
|
+
return Promise.reject(err);
|
|
35
|
+
};
|
|
36
|
+
newFn.calls = fn.calls;
|
|
37
|
+
return newFn;
|
|
38
|
+
};
|
|
39
|
+
return fn;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('Widget Error Isolation', () => {
|
|
43
|
+
|
|
44
|
+
describe('WidgetHealthTracker', () => {
|
|
45
|
+
let tracker;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
tracker = new WidgetHealthTracker();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('recordSuccess', () => {
|
|
52
|
+
test('should mark widget as healthy on success', () => {
|
|
53
|
+
tracker.recordSuccess('test-widget');
|
|
54
|
+
const health = tracker.getHealth('test-widget');
|
|
55
|
+
|
|
56
|
+
expect(health.status).toBe(WidgetHealthStatus.HEALTHY);
|
|
57
|
+
expect(health.consecutiveErrors).toBe(0);
|
|
58
|
+
expect(health.isHealthy).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should reset consecutive errors on success', () => {
|
|
62
|
+
tracker.recordError('test-widget', new Error('Test error'));
|
|
63
|
+
tracker.recordSuccess('test-widget');
|
|
64
|
+
const health = tracker.getHealth('test-widget');
|
|
65
|
+
|
|
66
|
+
expect(health.consecutiveErrors).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('recordError', () => {
|
|
71
|
+
test('should track consecutive errors', () => {
|
|
72
|
+
tracker.recordError('test-widget', new Error('Error 1'));
|
|
73
|
+
tracker.recordError('test-widget', new Error('Error 2'));
|
|
74
|
+
|
|
75
|
+
const health = tracker.getHealth('test-widget');
|
|
76
|
+
expect(health.consecutiveErrors).toBe(2);
|
|
77
|
+
expect(health.totalErrors).toBe(2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should mark widget as degraded after first error', () => {
|
|
81
|
+
tracker.recordError('test-widget', new Error('Test error'));
|
|
82
|
+
const health = tracker.getHealth('test-widget');
|
|
83
|
+
|
|
84
|
+
expect(health.status).toBe(WidgetHealthStatus.DEGRADED);
|
|
85
|
+
expect(health.isHealthy).toBe(false);
|
|
86
|
+
expect(health.isOperational).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should mark widget as failed after max consecutive errors', () => {
|
|
90
|
+
const maxErrors = DEFAULT_ISOLATION_CONFIG.maxConsecutiveErrors;
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < maxErrors; i++) {
|
|
93
|
+
tracker.recordError('test-widget', new Error(`Error ${i}`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const health = tracker.getHealth('test-widget');
|
|
97
|
+
expect(health.status).toBe(WidgetHealthStatus.FAILED);
|
|
98
|
+
expect(health.isOperational).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should store error details', () => {
|
|
102
|
+
const error = new Error('Test error message');
|
|
103
|
+
error.stack = 'Test stack trace';
|
|
104
|
+
|
|
105
|
+
tracker.recordError('test-widget', error, WidgetErrorType.RENDER_ERROR);
|
|
106
|
+
const health = tracker.getHealth('test-widget');
|
|
107
|
+
|
|
108
|
+
expect(health.lastError.message).toBe('Test error message');
|
|
109
|
+
expect(health.lastError.type).toBe(WidgetErrorType.RENDER_ERROR);
|
|
110
|
+
expect(health.lastError.stack).toBe('Test stack trace');
|
|
111
|
+
expect(health.lastError.timestamp).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should track error history within window', () => {
|
|
115
|
+
tracker.recordError('test-widget', new Error('Error 1'));
|
|
116
|
+
tracker.recordError('test-widget', new Error('Error 2'));
|
|
117
|
+
|
|
118
|
+
const health = tracker.getHealth('test-widget');
|
|
119
|
+
expect(health.recentErrorCount).toBe(2);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('getHealth', () => {
|
|
124
|
+
test('should return null for untracked widget', () => {
|
|
125
|
+
const health = tracker.getHealth('unknown-widget');
|
|
126
|
+
expect(health).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should include computed properties', () => {
|
|
130
|
+
tracker.recordSuccess('test-widget');
|
|
131
|
+
const health = tracker.getHealth('test-widget');
|
|
132
|
+
|
|
133
|
+
expect(health).toHaveProperty('isHealthy');
|
|
134
|
+
expect(health).toHaveProperty('isOperational');
|
|
135
|
+
expect(health).toHaveProperty('recentErrorCount');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('getAllHealth', () => {
|
|
140
|
+
test('should return health for all tracked widgets', () => {
|
|
141
|
+
tracker.recordSuccess('widget-1');
|
|
142
|
+
tracker.recordSuccess('widget-2');
|
|
143
|
+
|
|
144
|
+
const allHealth = tracker.getAllHealth();
|
|
145
|
+
expect(Object.keys(allHealth)).toHaveLength(2);
|
|
146
|
+
expect(allHealth['widget-1']).toBeDefined();
|
|
147
|
+
expect(allHealth['widget-2']).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('canRecover', () => {
|
|
152
|
+
test('should allow recovery for degraded widget', () => {
|
|
153
|
+
tracker.recordError('test-widget', new Error('Error'));
|
|
154
|
+
expect(tracker.canRecover('test-widget')).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should allow recovery for failed widget after delay', () => {
|
|
158
|
+
tracker.config.recoveryDelayMs = 1000;
|
|
159
|
+
tracker.recordError('test-widget', new Error('Error 1'));
|
|
160
|
+
tracker.recordError('test-widget', new Error('Error 2'));
|
|
161
|
+
tracker.recordError('test-widget', new Error('Error 3'));
|
|
162
|
+
|
|
163
|
+
// Initially should not allow recovery
|
|
164
|
+
expect(tracker.canRecover('test-widget')).toBe(false);
|
|
165
|
+
|
|
166
|
+
// After waiting, should allow recovery
|
|
167
|
+
tracker.healthStatus.get('test-widget').failedSince = Date.now() - 2000;
|
|
168
|
+
expect(tracker.canRecover('test-widget')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should not allow recovery when autoRecover is disabled', () => {
|
|
172
|
+
tracker.config.autoRecover = false;
|
|
173
|
+
tracker.recordError('test-widget', new Error('Error'));
|
|
174
|
+
expect(tracker.canRecover('test-widget')).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should not allow recovery after max attempts', () => {
|
|
178
|
+
tracker.config.maxRecoveryAttempts = 2;
|
|
179
|
+
|
|
180
|
+
tracker.markRecovering('test-widget');
|
|
181
|
+
tracker.markRecovering('test-widget');
|
|
182
|
+
tracker.markRecovering('test-widget');
|
|
183
|
+
|
|
184
|
+
expect(tracker.canRecover('test-widget')).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('resetHealth', () => {
|
|
189
|
+
test('should clear health record', () => {
|
|
190
|
+
tracker.recordError('test-widget', new Error('Error'));
|
|
191
|
+
tracker.resetHealth('test-widget');
|
|
192
|
+
|
|
193
|
+
const health = tracker.getHealth('test-widget');
|
|
194
|
+
expect(health).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('getStats', () => {
|
|
199
|
+
test('should return summary statistics', () => {
|
|
200
|
+
tracker.recordSuccess('widget-1');
|
|
201
|
+
tracker.recordSuccess('widget-2');
|
|
202
|
+
tracker.recordError('widget-2', new Error('Error'));
|
|
203
|
+
tracker.recordError('widget-3', new Error('Error 1'));
|
|
204
|
+
tracker.recordError('widget-3', new Error('Error 2'));
|
|
205
|
+
tracker.recordError('widget-3', new Error('Error 3'));
|
|
206
|
+
|
|
207
|
+
const stats = tracker.getStats();
|
|
208
|
+
expect(stats.total).toBe(3);
|
|
209
|
+
expect(stats.healthy).toBe(1);
|
|
210
|
+
expect(stats.degraded).toBe(1);
|
|
211
|
+
expect(stats.failed).toBe(1);
|
|
212
|
+
expect(stats.totalErrors).toBe(4);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('WidgetErrorIsolator', () => {
|
|
218
|
+
let isolator;
|
|
219
|
+
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
isolator = new WidgetErrorIsolator({
|
|
222
|
+
failSilently: true,
|
|
223
|
+
logErrors: false,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
isolator.shutdown();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('wrapInit', () => {
|
|
232
|
+
test('should wrap successful init', async () => {
|
|
233
|
+
const initFn = () => Promise.resolve({ initialized: true });
|
|
234
|
+
const result = await isolator.wrapInit('test-widget', initFn);
|
|
235
|
+
|
|
236
|
+
expect(result).toEqual({ initialized: true });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('should catch init errors when failSilently is true', async () => {
|
|
240
|
+
const initFn = () => Promise.reject(new Error('Init failed'));
|
|
241
|
+
const result = await isolator.wrapInit('test-widget', initFn);
|
|
242
|
+
|
|
243
|
+
expect(result).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('should throw when failSilently is false', async () => {
|
|
247
|
+
isolator.config.failSilently = false;
|
|
248
|
+
const initFn = () => Promise.reject(new Error('Init failed'));
|
|
249
|
+
|
|
250
|
+
await expect(isolator.wrapInit('test-widget', initFn)).rejects.toThrow(WidgetIsolatedError);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('should mark widget as healthy after successful init', async () => {
|
|
254
|
+
const initFn = () => Promise.resolve({ initialized: true });
|
|
255
|
+
await isolator.wrapInit('test-widget', initFn);
|
|
256
|
+
|
|
257
|
+
const health = isolator.getHealth('test-widget');
|
|
258
|
+
expect(health.status).toBe(WidgetHealthStatus.HEALTHY);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('wrapCreate', () => {
|
|
263
|
+
test('should wrap successful create', async () => {
|
|
264
|
+
const createFn = () => Promise.resolve({ created: true });
|
|
265
|
+
const result = await isolator.wrapCreate('test-widget', createFn);
|
|
266
|
+
|
|
267
|
+
expect(result).toEqual({ created: true });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('should catch create errors', async () => {
|
|
271
|
+
const createFn = () => Promise.reject(new Error('Create failed'));
|
|
272
|
+
const result = await isolator.wrapCreate('test-widget', createFn);
|
|
273
|
+
|
|
274
|
+
expect(result).toBeNull();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('wrapGetData', () => {
|
|
279
|
+
test('should wrap successful data fetch', async () => {
|
|
280
|
+
const dataFn = () => Promise.resolve({ data: 'test' });
|
|
281
|
+
const result = await isolator.wrapGetData('test-widget', dataFn);
|
|
282
|
+
|
|
283
|
+
expect(result).toEqual({ data: 'test' });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('should catch data fetch errors', async () => {
|
|
287
|
+
const dataFn = () => Promise.reject(new Error('Data fetch failed'));
|
|
288
|
+
const result = await isolator.wrapGetData('test-widget', dataFn);
|
|
289
|
+
|
|
290
|
+
expect(result).toBeNull();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('wrapRender', () => {
|
|
295
|
+
test('should wrap successful render', async () => {
|
|
296
|
+
const renderFn = () => Promise.resolve(undefined);
|
|
297
|
+
const result = await isolator.wrapRender('test-widget', renderFn);
|
|
298
|
+
|
|
299
|
+
expect(result).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('should catch render errors', async () => {
|
|
303
|
+
const renderFn = () => Promise.reject(new Error('Render failed'));
|
|
304
|
+
const result = await isolator.wrapRender('test-widget', renderFn);
|
|
305
|
+
|
|
306
|
+
expect(result).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('wrapDestroy', () => {
|
|
311
|
+
test('should wrap successful destroy', async () => {
|
|
312
|
+
const destroyFn = () => Promise.resolve(undefined);
|
|
313
|
+
const result = await isolator.wrapDestroy('test-widget', destroyFn);
|
|
314
|
+
|
|
315
|
+
expect(result).toBeUndefined();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('should catch destroy errors', async () => {
|
|
319
|
+
const destroyFn = () => Promise.reject(new Error('Destroy failed'));
|
|
320
|
+
const result = await isolator.wrapDestroy('test-widget', destroyFn);
|
|
321
|
+
|
|
322
|
+
expect(result).toBeNull();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('timeout handling', () => {
|
|
327
|
+
test('should timeout slow operations', async () => {
|
|
328
|
+
isolator.config.initTimeoutMs = 50;
|
|
329
|
+
|
|
330
|
+
const slowFn = () =>
|
|
331
|
+
new Promise((resolve) => setTimeout(() => resolve('done'), 200));
|
|
332
|
+
|
|
333
|
+
const startTime = Date.now();
|
|
334
|
+
const result = await isolator.wrapInit('test-widget', slowFn);
|
|
335
|
+
const elapsed = Date.now() - startTime;
|
|
336
|
+
|
|
337
|
+
expect(result).toBeNull();
|
|
338
|
+
expect(elapsed).toBeLessThan(150); // Should timeout before 200ms
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('isOperational', () => {
|
|
343
|
+
test('should return true for new widgets', () => {
|
|
344
|
+
expect(isolator.isOperational('new-widget')).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('should return false for failed widgets', async () => {
|
|
348
|
+
const errorFn = () => Promise.reject(new Error('Error'));
|
|
349
|
+
|
|
350
|
+
// Trigger failures
|
|
351
|
+
for (let i = 0; i < DEFAULT_ISOLATION_CONFIG.maxConsecutiveErrors; i++) {
|
|
352
|
+
await isolator.wrapRender('test-widget', errorFn);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
expect(isolator.isOperational('test-widget')).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('resetWidget', () => {
|
|
360
|
+
test('should reset widget health', async () => {
|
|
361
|
+
const errorFn = () => Promise.reject(new Error('Error'));
|
|
362
|
+
await isolator.wrapRender('test-widget', errorFn);
|
|
363
|
+
|
|
364
|
+
isolator.resetWidget('test-widget');
|
|
365
|
+
|
|
366
|
+
const health = isolator.getHealth('test-widget');
|
|
367
|
+
expect(health).toBeNull();
|
|
368
|
+
expect(isolator.isOperational('test-widget')).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('getStats', () => {
|
|
373
|
+
test('should return isolator statistics', () => {
|
|
374
|
+
const stats = isolator.getStats();
|
|
375
|
+
|
|
376
|
+
expect(stats).toHaveProperty('total');
|
|
377
|
+
expect(stats).toHaveProperty('healthy');
|
|
378
|
+
expect(stats).toHaveProperty('degraded');
|
|
379
|
+
expect(stats).toHaveProperty('failed');
|
|
380
|
+
expect(stats).toHaveProperty('failedWidgetCount');
|
|
381
|
+
expect(stats).toHaveProperty('pendingRecoveries');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('WidgetIsolatedError', () => {
|
|
387
|
+
test('should create error with correct properties', () => {
|
|
388
|
+
const originalError = new Error('Original error');
|
|
389
|
+
const error = new WidgetIsolatedError(
|
|
390
|
+
'test-widget',
|
|
391
|
+
'render',
|
|
392
|
+
originalError,
|
|
393
|
+
WidgetErrorType.RENDER_ERROR
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
expect(error.widgetId).toBe('test-widget');
|
|
397
|
+
expect(error.operation).toBe('render');
|
|
398
|
+
expect(error.errorType).toBe(WidgetErrorType.RENDER_ERROR);
|
|
399
|
+
expect(error.originalError).toBe(originalError);
|
|
400
|
+
expect(error.message).toContain('test-widget');
|
|
401
|
+
expect(error.message).toContain('render');
|
|
402
|
+
expect(error.code).toBe('WIDGET_ISOLATED_ERROR');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('should handle null original error', () => {
|
|
406
|
+
const error = new WidgetIsolatedError('test-widget', 'init', null);
|
|
407
|
+
|
|
408
|
+
expect(error.originalError).toBeNull();
|
|
409
|
+
expect(error.message).toContain('Unknown error');
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('executeWithIsolation', () => {
|
|
414
|
+
test('should execute multiple operations with isolation', async () => {
|
|
415
|
+
const operations = [
|
|
416
|
+
{
|
|
417
|
+
widgetId: 'widget-1',
|
|
418
|
+
operation: () => Promise.resolve('result-1'),
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
widgetId: 'widget-2',
|
|
422
|
+
operation: () => Promise.reject(new Error('Failed')),
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
widgetId: 'widget-3',
|
|
426
|
+
operation: () => Promise.resolve('result-3'),
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
const results = await executeWithIsolation(operations, { failSilently: true });
|
|
431
|
+
|
|
432
|
+
expect(results).toHaveLength(3);
|
|
433
|
+
expect(results[0].success).toBe(true);
|
|
434
|
+
expect(results[0].result).toBe('result-1');
|
|
435
|
+
expect(results[1].success).toBe(false);
|
|
436
|
+
expect(results[1].error).toBeDefined();
|
|
437
|
+
expect(results[2].success).toBe(true);
|
|
438
|
+
expect(results[2].result).toBe('result-3');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('should allow partial success', async () => {
|
|
442
|
+
const operations = [
|
|
443
|
+
{ widgetId: 'widget-1', operation: () => Promise.resolve('ok') },
|
|
444
|
+
{ widgetId: 'widget-2', operation: () => Promise.reject(new Error('fail')) },
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
const results = await executeWithIsolation(operations);
|
|
448
|
+
|
|
449
|
+
expect(results[0].success).toBe(true);
|
|
450
|
+
expect(results[1].success).toBe(false);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('getWidgetErrorIsolator', () => {
|
|
455
|
+
test('should return singleton instance', () => {
|
|
456
|
+
const isolator1 = getWidgetErrorIsolator();
|
|
457
|
+
const isolator2 = getWidgetErrorIsolator();
|
|
458
|
+
|
|
459
|
+
expect(isolator1).toBe(isolator2);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('should create instance if not exists', () => {
|
|
463
|
+
const isolator = getWidgetErrorIsolator({ failSilently: false });
|
|
464
|
+
|
|
465
|
+
expect(isolator).toBeInstanceOf(WidgetErrorIsolator);
|
|
466
|
+
// Note: singleton may retain previous config
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|