@react-native-harness/jest 1.4.0-rc.1 → 1.4.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/dist/__tests__/bridge.test.js +6 -2
- package/dist/__tests__/crash-monitor.test.js +64 -0
- package/dist/__tests__/environment-reset.test.d.ts +2 -0
- package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
- package/dist/__tests__/environment-reset.test.js +238 -0
- package/dist/__tests__/execute-run.test.js +250 -4
- package/dist/__tests__/format-harness-error.test.d.ts +2 -0
- package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
- package/dist/__tests__/format-harness-error.test.js +27 -0
- package/dist/__tests__/harness-cache.test.js +62 -41
- package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
- package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session-signal.test.js +298 -0
- package/dist/__tests__/harness-session.test.js +161 -2
- package/dist/__tests__/index.test.js +3 -1
- package/dist/__tests__/metro-port.test.js +2 -0
- package/dist/__tests__/run.test.d.ts +2 -0
- package/dist/__tests__/run.test.d.ts.map +1 -0
- package/dist/__tests__/run.test.js +48 -0
- package/dist/crash-monitor.d.ts +7 -0
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +19 -0
- package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/derivers.test.js +248 -0
- package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/report.test.js +97 -0
- package/dist/diagnostics/derivers.d.ts +24 -0
- package/dist/diagnostics/derivers.d.ts.map +1 -0
- package/dist/diagnostics/derivers.js +146 -0
- package/dist/diagnostics/index.d.ts +3 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +2 -0
- package/dist/diagnostics/report.d.ts +7 -0
- package/dist/diagnostics/report.d.ts.map +1 -0
- package/dist/diagnostics/report.js +84 -0
- package/dist/environment-reset.d.ts +51 -0
- package/dist/environment-reset.d.ts.map +1 -0
- package/dist/environment-reset.js +83 -0
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +121 -69
- package/dist/format-harness-error.d.ts +5 -0
- package/dist/format-harness-error.d.ts.map +1 -0
- package/dist/format-harness-error.js +43 -0
- package/dist/harness-session.d.ts +26 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +260 -87
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +21 -4
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/bridge.test.ts +8 -3
- package/src/__tests__/crash-monitor.test.ts +84 -0
- package/src/__tests__/environment-reset.test.ts +308 -0
- package/src/__tests__/execute-run.test.ts +327 -4
- package/src/__tests__/format-harness-error.test.ts +32 -0
- package/src/__tests__/harness-cache.test.ts +91 -0
- package/src/__tests__/harness-session.test.ts +211 -1
- package/src/__tests__/index.test.ts +3 -1
- package/src/__tests__/metro-port.test.ts +2 -0
- package/src/__tests__/run.test.ts +74 -0
- package/src/crash-monitor.ts +28 -0
- package/src/diagnostics/__tests__/derivers.test.ts +300 -0
- package/src/diagnostics/__tests__/report.test.ts +128 -0
- package/src/diagnostics/derivers.ts +170 -0
- package/src/diagnostics/index.ts +2 -0
- package/src/diagnostics/report.ts +124 -0
- package/src/environment-reset.ts +138 -0
- package/src/execute-run.ts +136 -77
- package/src/format-harness-error.ts +73 -0
- package/src/harness-session.ts +369 -131
- package/src/run.ts +23 -4
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/jest",
|
|
3
|
-
"version": "1.4.0
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -39,12 +39,13 @@
|
|
|
39
39
|
"jest-util": "^30.2.0",
|
|
40
40
|
"tslib": "^2.3.0",
|
|
41
41
|
"yargs": "^17.7.2",
|
|
42
|
-
"@react-native-harness/
|
|
43
|
-
"@react-native-harness/
|
|
44
|
-
"@react-native-harness/
|
|
45
|
-
"@react-native-harness/
|
|
46
|
-
"@react-native-harness/
|
|
47
|
-
"@react-native-harness/
|
|
42
|
+
"@react-native-harness/cache": "1.4.0",
|
|
43
|
+
"@react-native-harness/bundler-metro": "1.4.0",
|
|
44
|
+
"@react-native-harness/config": "1.4.0",
|
|
45
|
+
"@react-native-harness/bridge": "1.4.0",
|
|
46
|
+
"@react-native-harness/plugins": "1.4.0",
|
|
47
|
+
"@react-native-harness/platforms": "1.4.0",
|
|
48
|
+
"@react-native-harness/tools": "1.4.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/yargs": "^17.0.32",
|
|
@@ -37,8 +37,12 @@ afterEach(async () => {
|
|
|
37
37
|
await new Promise((r) => setTimeout(r, 10));
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
const connect = (
|
|
41
|
-
connectToHarness
|
|
40
|
+
const connect = (
|
|
41
|
+
callbacks: Parameters<typeof connectToHarness>[1] = {
|
|
42
|
+
runTests: vi.fn(),
|
|
43
|
+
resetEnvironment: vi.fn(),
|
|
44
|
+
},
|
|
45
|
+
) => connectToHarness(`ws://127.0.0.1:${bridgePort}`, callbacks);
|
|
42
46
|
|
|
43
47
|
const device = {
|
|
44
48
|
platform: 'ios' as const,
|
|
@@ -140,7 +144,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
140
144
|
const runTestsCb = vi.fn(async () => suiteResult);
|
|
141
145
|
|
|
142
146
|
const connectionPromise = bridge.nextConnection();
|
|
143
|
-
const handle = await connect({ runTests: runTestsCb });
|
|
147
|
+
const handle = await connect({ runTests: runTestsCb, resetEnvironment: vi.fn() });
|
|
144
148
|
handle.reportReady(device);
|
|
145
149
|
|
|
146
150
|
const conn = await connectionPromise;
|
|
@@ -157,6 +161,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
157
161
|
const firstHandle = await connect({
|
|
158
162
|
runTests: async (): Promise<TestSuiteResult> =>
|
|
159
163
|
await new Promise<TestSuiteResult>(() => undefined),
|
|
164
|
+
resetEnvironment: vi.fn(),
|
|
160
165
|
});
|
|
161
166
|
firstHandle.reportReady(device);
|
|
162
167
|
|
|
@@ -236,4 +236,88 @@ describe('createCrashMonitor', () => {
|
|
|
236
236
|
expect(session.removeListener).toHaveBeenCalledOnce();
|
|
237
237
|
expect(cm.isAlive()).toBe(false);
|
|
238
238
|
});
|
|
239
|
+
|
|
240
|
+
describe('expectDisconnect', () => {
|
|
241
|
+
it('suppresses handleBridgeDisconnect while the window is open', async () => {
|
|
242
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
243
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
244
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
245
|
+
watch.promise.catch(noop);
|
|
246
|
+
|
|
247
|
+
const closeWindow = cm.expectDisconnect();
|
|
248
|
+
cm.handleBridgeDisconnect();
|
|
249
|
+
await waitForClassification();
|
|
250
|
+
|
|
251
|
+
const settled = vi.fn();
|
|
252
|
+
watch.promise.then(settled, settled);
|
|
253
|
+
await Promise.resolve();
|
|
254
|
+
expect(settled).not.toHaveBeenCalled();
|
|
255
|
+
|
|
256
|
+
closeWindow();
|
|
257
|
+
watch.cancel();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('resumes classifying disconnects once the window closes', async () => {
|
|
261
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
262
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
263
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
264
|
+
watch.promise.catch(noop);
|
|
265
|
+
|
|
266
|
+
const closeWindow = cm.expectDisconnect();
|
|
267
|
+
closeWindow();
|
|
268
|
+
|
|
269
|
+
cm.handleBridgeDisconnect();
|
|
270
|
+
|
|
271
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('keeps the window open until every opener has closed it (nested windows)', async () => {
|
|
275
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
276
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
277
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
278
|
+
watch.promise.catch(noop);
|
|
279
|
+
|
|
280
|
+
const closeFirst = cm.expectDisconnect();
|
|
281
|
+
const closeSecond = cm.expectDisconnect();
|
|
282
|
+
closeFirst();
|
|
283
|
+
|
|
284
|
+
cm.handleBridgeDisconnect();
|
|
285
|
+
await waitForClassification();
|
|
286
|
+
|
|
287
|
+
const settled = vi.fn();
|
|
288
|
+
watch.promise.then(settled, settled);
|
|
289
|
+
await Promise.resolve();
|
|
290
|
+
expect(settled).not.toHaveBeenCalled();
|
|
291
|
+
|
|
292
|
+
closeSecond();
|
|
293
|
+
watch.cancel();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('ignores a redundant close() call', async () => {
|
|
297
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
298
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
299
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
300
|
+
watch.promise.catch(noop);
|
|
301
|
+
|
|
302
|
+
const closeWindow = cm.expectDisconnect();
|
|
303
|
+
closeWindow();
|
|
304
|
+
closeWindow();
|
|
305
|
+
|
|
306
|
+
cm.handleBridgeDisconnect();
|
|
307
|
+
|
|
308
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('clears the expected-disconnect window on dispose', async () => {
|
|
312
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
313
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
314
|
+
cm.expectDisconnect();
|
|
315
|
+
|
|
316
|
+
await cm.dispose();
|
|
317
|
+
|
|
318
|
+
// A disconnect after dispose should not throw or hang; monitoring is
|
|
319
|
+
// fully torn down regardless of the window state.
|
|
320
|
+
expect(() => cm.handleBridgeDisconnect()).not.toThrow();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
239
323
|
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createProcessResetStrategy,
|
|
4
|
+
createRuntimeResetStrategy,
|
|
5
|
+
resolveResetStrategyKind,
|
|
6
|
+
withEscalation,
|
|
7
|
+
type EnvironmentResetStrategy,
|
|
8
|
+
} from '../environment-reset.js';
|
|
9
|
+
|
|
10
|
+
describe('resolveResetStrategyKind', () => {
|
|
11
|
+
it('normalizes true to the process strategy', () => {
|
|
12
|
+
expect(resolveResetStrategyKind(true)).toBe('process');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('normalizes false to null (no reset)', () => {
|
|
16
|
+
expect(resolveResetStrategyKind(false)).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('passes through the process string value', () => {
|
|
20
|
+
expect(resolveResetStrategyKind('process')).toBe('process');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('passes through the runtime string value', () => {
|
|
24
|
+
expect(resolveResetStrategyKind('runtime')).toBe('runtime');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('createProcessResetStrategy', () => {
|
|
29
|
+
it('has kind "process" and delegates to the injected restart function', async () => {
|
|
30
|
+
const restart = vi.fn(async () => undefined);
|
|
31
|
+
const strategy = createProcessResetStrategy({ restart });
|
|
32
|
+
|
|
33
|
+
expect(strategy.kind).toBe('process');
|
|
34
|
+
await strategy.reset({ testFilePath: '/tests/example.harness.ts' });
|
|
35
|
+
|
|
36
|
+
expect(restart).toHaveBeenCalledWith('/tests/example.harness.ts');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('propagates restart failures', async () => {
|
|
40
|
+
const restart = vi.fn(async () => {
|
|
41
|
+
throw new Error('restart failed');
|
|
42
|
+
});
|
|
43
|
+
const strategy = createProcessResetStrategy({ restart });
|
|
44
|
+
|
|
45
|
+
await expect(
|
|
46
|
+
strategy.reset({ testFilePath: '/tests/example.harness.ts' })
|
|
47
|
+
).rejects.toThrow('restart failed');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('createRuntimeResetStrategy', () => {
|
|
52
|
+
const testFilePath = '/tests/example.harness.ts';
|
|
53
|
+
|
|
54
|
+
it('has kind "runtime" and resets via the connection then waits for reconnect', async () => {
|
|
55
|
+
const callOrder: string[] = [];
|
|
56
|
+
const closeWindow = vi.fn();
|
|
57
|
+
const connection = {
|
|
58
|
+
resetEnvironment: vi.fn(async () => {
|
|
59
|
+
callOrder.push('resetEnvironment');
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
const waitForReconnect = vi.fn(async () => {
|
|
63
|
+
callOrder.push('waitForReconnect-started');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const strategy = createRuntimeResetStrategy({
|
|
67
|
+
getConnection: () => connection,
|
|
68
|
+
expectDisconnect: () => {
|
|
69
|
+
callOrder.push('expectDisconnect');
|
|
70
|
+
return closeWindow;
|
|
71
|
+
},
|
|
72
|
+
waitForReconnect,
|
|
73
|
+
isDisconnectError: () => false,
|
|
74
|
+
timeoutMs: 1000,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(strategy.kind).toBe('runtime');
|
|
78
|
+
await strategy.reset({ testFilePath });
|
|
79
|
+
|
|
80
|
+
expect(callOrder).toEqual([
|
|
81
|
+
'expectDisconnect',
|
|
82
|
+
'waitForReconnect-started',
|
|
83
|
+
'resetEnvironment',
|
|
84
|
+
]);
|
|
85
|
+
expect(waitForReconnect).toHaveBeenCalledWith(expect.any(AbortSignal));
|
|
86
|
+
expect(closeWindow).toHaveBeenCalledOnce();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('starts waiting for reconnect before firing the RPC', async () => {
|
|
90
|
+
const callOrder: string[] = [];
|
|
91
|
+
const connection = {
|
|
92
|
+
resetEnvironment: vi.fn(async () => {
|
|
93
|
+
callOrder.push('resetEnvironment');
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
const waitForReconnect = vi.fn(() => {
|
|
97
|
+
callOrder.push('waitForReconnect-called');
|
|
98
|
+
return Promise.resolve();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const strategy = createRuntimeResetStrategy({
|
|
102
|
+
getConnection: () => connection,
|
|
103
|
+
expectDisconnect: () => () => undefined,
|
|
104
|
+
waitForReconnect,
|
|
105
|
+
isDisconnectError: () => false,
|
|
106
|
+
timeoutMs: 1000,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await strategy.reset({ testFilePath });
|
|
110
|
+
|
|
111
|
+
expect(callOrder).toEqual(['waitForReconnect-called', 'resetEnvironment']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('throws when there is no active app connection', async () => {
|
|
115
|
+
const expectDisconnect = vi.fn();
|
|
116
|
+
const strategy = createRuntimeResetStrategy({
|
|
117
|
+
getConnection: () => null,
|
|
118
|
+
expectDisconnect,
|
|
119
|
+
waitForReconnect: vi.fn(async () => undefined),
|
|
120
|
+
isDisconnectError: () => false,
|
|
121
|
+
timeoutMs: 1000,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow(
|
|
125
|
+
'No active app connection'
|
|
126
|
+
);
|
|
127
|
+
expect(expectDisconnect).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('closes the expected-disconnect window even when the RPC call fails', async () => {
|
|
131
|
+
const closeWindow = vi.fn();
|
|
132
|
+
const connection = {
|
|
133
|
+
resetEnvironment: vi.fn(async () => {
|
|
134
|
+
throw new Error('rpc failed');
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const strategy = createRuntimeResetStrategy({
|
|
139
|
+
getConnection: () => connection,
|
|
140
|
+
expectDisconnect: () => closeWindow,
|
|
141
|
+
waitForReconnect: vi.fn(async () => undefined),
|
|
142
|
+
isDisconnectError: () => false,
|
|
143
|
+
timeoutMs: 1000,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
|
|
147
|
+
expect(closeWindow).toHaveBeenCalledOnce();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('closes the expected-disconnect window even when reconnect times out', async () => {
|
|
151
|
+
const closeWindow = vi.fn();
|
|
152
|
+
const connection = { resetEnvironment: vi.fn(async () => undefined) };
|
|
153
|
+
|
|
154
|
+
const strategy = createRuntimeResetStrategy({
|
|
155
|
+
getConnection: () => connection,
|
|
156
|
+
expectDisconnect: () => closeWindow,
|
|
157
|
+
waitForReconnect: vi.fn(async () => {
|
|
158
|
+
throw new Error('reconnect timed out');
|
|
159
|
+
}),
|
|
160
|
+
isDisconnectError: () => false,
|
|
161
|
+
timeoutMs: 1000,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow(
|
|
165
|
+
'reconnect timed out'
|
|
166
|
+
);
|
|
167
|
+
expect(closeWindow).toHaveBeenCalledOnce();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('treats a disconnect-flavored RPC rejection as reload-in-flight and succeeds', async () => {
|
|
171
|
+
const disconnectError = new Error('app bridge disconnected');
|
|
172
|
+
const connection = {
|
|
173
|
+
resetEnvironment: vi.fn(async () => {
|
|
174
|
+
throw disconnectError;
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
const waitForReconnect = vi.fn(async () => undefined);
|
|
178
|
+
|
|
179
|
+
const strategy = createRuntimeResetStrategy({
|
|
180
|
+
getConnection: () => connection,
|
|
181
|
+
expectDisconnect: () => () => undefined,
|
|
182
|
+
waitForReconnect,
|
|
183
|
+
isDisconnectError: (error) => error === disconnectError,
|
|
184
|
+
timeoutMs: 1000,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await expect(strategy.reset({ testFilePath })).resolves.toBeUndefined();
|
|
188
|
+
expect(waitForReconnect).toHaveBeenCalledOnce();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('still throws on a non-disconnect RPC rejection', async () => {
|
|
192
|
+
const connection = {
|
|
193
|
+
resetEnvironment: vi.fn(async () => {
|
|
194
|
+
throw new Error('rpc failed');
|
|
195
|
+
}),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const strategy = createRuntimeResetStrategy({
|
|
199
|
+
getConnection: () => connection,
|
|
200
|
+
expectDisconnect: () => () => undefined,
|
|
201
|
+
waitForReconnect: vi.fn(async () => undefined),
|
|
202
|
+
isDisconnectError: (error) =>
|
|
203
|
+
error instanceof Error && error.message === 'app bridge disconnected',
|
|
204
|
+
timeoutMs: 1000,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('does not leave an unhandled rejection when the RPC fails before the reconnect wait settles', async () => {
|
|
211
|
+
let rejectReconnect!: (error: Error) => void;
|
|
212
|
+
const waitForReconnect = vi.fn(
|
|
213
|
+
() =>
|
|
214
|
+
new Promise<void>((_, reject) => {
|
|
215
|
+
rejectReconnect = reject;
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
const connection = {
|
|
219
|
+
resetEnvironment: vi.fn(async () => {
|
|
220
|
+
throw new Error('rpc failed');
|
|
221
|
+
}),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const unhandled: unknown[] = [];
|
|
225
|
+
const onUnhandledRejection = (reason: unknown) => {
|
|
226
|
+
unhandled.push(reason);
|
|
227
|
+
};
|
|
228
|
+
process.on('unhandledRejection', onUnhandledRejection);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const strategy = createRuntimeResetStrategy({
|
|
232
|
+
getConnection: () => connection,
|
|
233
|
+
expectDisconnect: () => () => undefined,
|
|
234
|
+
waitForReconnect,
|
|
235
|
+
isDisconnectError: () => false,
|
|
236
|
+
timeoutMs: 1000,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
|
|
240
|
+
|
|
241
|
+
// Simulate the reconnect timeout firing after the strategy already
|
|
242
|
+
// threw; the strategy must have attached a rejection handler.
|
|
243
|
+
rejectReconnect(new Error('reconnect timed out'));
|
|
244
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
245
|
+
|
|
246
|
+
expect(unhandled).toEqual([]);
|
|
247
|
+
} finally {
|
|
248
|
+
process.off('unhandledRejection', onUnhandledRejection);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('withEscalation', () => {
|
|
254
|
+
const testFilePath = '/tests/example.harness.ts';
|
|
255
|
+
|
|
256
|
+
const makeStrategy = (
|
|
257
|
+
kind: EnvironmentResetStrategy['kind'],
|
|
258
|
+
reset: EnvironmentResetStrategy['reset']
|
|
259
|
+
): EnvironmentResetStrategy => ({ kind, reset });
|
|
260
|
+
|
|
261
|
+
it('reports the primary strategy kind', () => {
|
|
262
|
+
const primary = makeStrategy('runtime', async () => undefined);
|
|
263
|
+
const fallback = makeStrategy('process', async () => undefined);
|
|
264
|
+
|
|
265
|
+
expect(withEscalation(primary, fallback).kind).toBe('runtime');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('does not call the fallback when the primary succeeds', async () => {
|
|
269
|
+
const fallbackReset = vi.fn(async () => undefined);
|
|
270
|
+
const primary = makeStrategy('runtime', async () => undefined);
|
|
271
|
+
const fallback = makeStrategy('process', fallbackReset);
|
|
272
|
+
const onEscalate = vi.fn();
|
|
273
|
+
|
|
274
|
+
await withEscalation(primary, fallback, { onEscalate }).reset({ testFilePath });
|
|
275
|
+
|
|
276
|
+
expect(fallbackReset).not.toHaveBeenCalled();
|
|
277
|
+
expect(onEscalate).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('escalates to the fallback when the primary fails', async () => {
|
|
281
|
+
const primaryError = new Error('runtime reset failed');
|
|
282
|
+
const fallbackReset = vi.fn(async () => undefined);
|
|
283
|
+
const primary = makeStrategy('runtime', async () => {
|
|
284
|
+
throw primaryError;
|
|
285
|
+
});
|
|
286
|
+
const fallback = makeStrategy('process', fallbackReset);
|
|
287
|
+
const onEscalate = vi.fn();
|
|
288
|
+
|
|
289
|
+
await withEscalation(primary, fallback, { onEscalate }).reset({ testFilePath });
|
|
290
|
+
|
|
291
|
+
expect(fallbackReset).toHaveBeenCalledWith({ testFilePath });
|
|
292
|
+
expect(onEscalate).toHaveBeenCalledWith(primaryError);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('propagates the fallback error when both strategies fail', async () => {
|
|
296
|
+
const fallbackError = new Error('process reset also failed');
|
|
297
|
+
const primary = makeStrategy('runtime', async () => {
|
|
298
|
+
throw new Error('runtime reset failed');
|
|
299
|
+
});
|
|
300
|
+
const fallback = makeStrategy('process', async () => {
|
|
301
|
+
throw fallbackError;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await expect(
|
|
305
|
+
withEscalation(primary, fallback).reset({ testFilePath })
|
|
306
|
+
).rejects.toBe(fallbackError);
|
|
307
|
+
});
|
|
308
|
+
});
|