@react-native-harness/jest 1.4.0 → 1.4.1

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 (86) hide show
  1. package/package.json +8 -8
  2. package/dist/__tests__/bridge.test.d.ts +0 -2
  3. package/dist/__tests__/bridge.test.d.ts.map +0 -1
  4. package/dist/__tests__/bridge.test.js +0 -167
  5. package/dist/__tests__/client-log-handler.test.d.ts +0 -2
  6. package/dist/__tests__/client-log-handler.test.d.ts.map +0 -1
  7. package/dist/__tests__/client-log-handler.test.js +0 -306
  8. package/dist/__tests__/crash-monitor.test.d.ts +0 -2
  9. package/dist/__tests__/crash-monitor.test.d.ts.map +0 -1
  10. package/dist/__tests__/crash-monitor.test.js +0 -230
  11. package/dist/__tests__/crash-supervisor.test.d.ts +0 -2
  12. package/dist/__tests__/crash-supervisor.test.d.ts.map +0 -1
  13. package/dist/__tests__/crash-supervisor.test.js +0 -151
  14. package/dist/__tests__/environment-reset.test.d.ts +0 -2
  15. package/dist/__tests__/environment-reset.test.d.ts.map +0 -1
  16. package/dist/__tests__/environment-reset.test.js +0 -238
  17. package/dist/__tests__/errors.test.d.ts +0 -2
  18. package/dist/__tests__/errors.test.d.ts.map +0 -1
  19. package/dist/__tests__/errors.test.js +0 -98
  20. package/dist/__tests__/execute-run.test.d.ts +0 -2
  21. package/dist/__tests__/execute-run.test.d.ts.map +0 -1
  22. package/dist/__tests__/execute-run.test.js +0 -639
  23. package/dist/__tests__/fixtures/mock-platform-runner.d.ts +0 -7
  24. package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +0 -1
  25. package/dist/__tests__/fixtures/mock-platform-runner.js +0 -6
  26. package/dist/__tests__/format-harness-error.test.d.ts +0 -2
  27. package/dist/__tests__/format-harness-error.test.d.ts.map +0 -1
  28. package/dist/__tests__/format-harness-error.test.js +0 -27
  29. package/dist/__tests__/harness-cache.test.d.ts +0 -2
  30. package/dist/__tests__/harness-cache.test.d.ts.map +0 -1
  31. package/dist/__tests__/harness-cache.test.js +0 -72
  32. package/dist/__tests__/harness-session-signal.test.d.ts +0 -2
  33. package/dist/__tests__/harness-session-signal.test.d.ts.map +0 -1
  34. package/dist/__tests__/harness-session-signal.test.js +0 -298
  35. package/dist/__tests__/harness-session.test.d.ts +0 -2
  36. package/dist/__tests__/harness-session.test.d.ts.map +0 -1
  37. package/dist/__tests__/harness-session.test.js +0 -233
  38. package/dist/__tests__/harness.test.d.ts +0 -2
  39. package/dist/__tests__/harness.test.d.ts.map +0 -1
  40. package/dist/__tests__/harness.test.js +0 -636
  41. package/dist/__tests__/hook-queue.test.d.ts +0 -2
  42. package/dist/__tests__/hook-queue.test.d.ts.map +0 -1
  43. package/dist/__tests__/hook-queue.test.js +0 -56
  44. package/dist/__tests__/index.test.d.ts +0 -2
  45. package/dist/__tests__/index.test.d.ts.map +0 -1
  46. package/dist/__tests__/index.test.js +0 -150
  47. package/dist/__tests__/metro-port.test.d.ts +0 -2
  48. package/dist/__tests__/metro-port.test.d.ts.map +0 -1
  49. package/dist/__tests__/metro-port.test.js +0 -54
  50. package/dist/__tests__/resource-lock.test.d.ts +0 -2
  51. package/dist/__tests__/resource-lock.test.d.ts.map +0 -1
  52. package/dist/__tests__/resource-lock.test.js +0 -138
  53. package/dist/__tests__/run.test.d.ts +0 -2
  54. package/dist/__tests__/run.test.d.ts.map +0 -1
  55. package/dist/__tests__/run.test.js +0 -48
  56. package/dist/__tests__/test-file-platform-filter.test.d.ts +0 -2
  57. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +0 -1
  58. package/dist/__tests__/test-file-platform-filter.test.js +0 -47
  59. package/dist/cli-args.test.d.ts +0 -2
  60. package/dist/cli-args.test.d.ts.map +0 -1
  61. package/dist/cli-args.test.js +0 -1
  62. package/dist/diagnostics/__tests__/derivers.test.d.ts +0 -2
  63. package/dist/diagnostics/__tests__/derivers.test.d.ts.map +0 -1
  64. package/dist/diagnostics/__tests__/derivers.test.js +0 -248
  65. package/dist/diagnostics/__tests__/report.test.d.ts +0 -2
  66. package/dist/diagnostics/__tests__/report.test.d.ts.map +0 -1
  67. package/dist/diagnostics/__tests__/report.test.js +0 -97
  68. package/dist/tsconfig.lib.tsbuildinfo +0 -1
  69. package/src/__tests__/bridge.test.ts +0 -224
  70. package/src/__tests__/client-log-handler.test.ts +0 -364
  71. package/src/__tests__/crash-monitor.test.ts +0 -323
  72. package/src/__tests__/environment-reset.test.ts +0 -308
  73. package/src/__tests__/errors.test.ts +0 -157
  74. package/src/__tests__/execute-run.test.ts +0 -849
  75. package/src/__tests__/format-harness-error.test.ts +0 -32
  76. package/src/__tests__/harness-cache.test.ts +0 -91
  77. package/src/__tests__/harness-session.test.ts +0 -303
  78. package/src/__tests__/hook-queue.test.ts +0 -77
  79. package/src/__tests__/index.test.ts +0 -221
  80. package/src/__tests__/metro-port.test.ts +0 -63
  81. package/src/__tests__/resource-lock.test.ts +0 -179
  82. package/src/__tests__/run.test.ts +0 -74
  83. package/src/__tests__/test-file-platform-filter.test.ts +0 -83
  84. package/src/diagnostics/__tests__/derivers.test.ts +0 -300
  85. package/src/diagnostics/__tests__/report.test.ts +0 -128
  86. package/tsconfig.tsbuildinfo +0 -1
@@ -1,323 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import path from 'node:path';
3
- import type {
4
- AppSession,
5
- AppSessionEvent,
6
- AppSessionListener,
7
- AppSessionLog,
8
- AppSessionState,
9
- } from '@react-native-harness/platforms';
10
- import {
11
- createCrashMonitor,
12
- CrashWatchCancelledError,
13
- } from '../crash-monitor.js';
14
- import { NativeCrashError, RuntimeDisconnectError } from '../errors.js';
15
-
16
- const noop = () => undefined;
17
- const waitForClassification = () =>
18
- new Promise((resolve) => setTimeout(resolve, 1600));
19
-
20
- const createAppSessionMock = (
21
- initialState: AppSessionState = { status: 'running' },
22
- logs: AppSessionLog[] = [],
23
- getCrashDetails?: AppSession['getCrashDetails']
24
- ) => {
25
- let state = initialState;
26
- let listener: AppSessionListener | null = null;
27
-
28
- const session: AppSession = {
29
- dispose: vi.fn(async () => undefined),
30
- getState: vi.fn(async () => state),
31
- getLogs: vi.fn(() => logs),
32
- addListener: vi.fn((l: AppSessionListener) => {
33
- listener = l;
34
- }),
35
- removeListener: vi.fn((l: AppSessionListener) => {
36
- if (listener === l) listener = null;
37
- }),
38
- };
39
-
40
- if (getCrashDetails) {
41
- session.getCrashDetails = getCrashDetails;
42
- }
43
-
44
- return {
45
- session,
46
- setState: (nextState: AppSessionState) => {
47
- state = nextState;
48
- },
49
- emit: (event: AppSessionEvent) => listener?.(event),
50
- };
51
- };
52
-
53
- describe('createCrashMonitor', () => {
54
- it('starts not alive and becomes alive when an app session is attached', () => {
55
- const cm = createCrashMonitor();
56
- const { session } = createAppSessionMock();
57
-
58
- expect(cm.isAlive()).toBe(false);
59
-
60
- cm.setAppSession(session);
61
-
62
- expect(cm.isAlive()).toBe(true);
63
- });
64
-
65
- it('rejects a watch with NativeCrashError when the app session exits', async () => {
66
- const { session, emit } = createAppSessionMock({
67
- status: 'exited',
68
- occurredAt: Date.now(),
69
- reason: 'observed-exit',
70
- });
71
- const cm = createCrashMonitor({ appSession: session });
72
- const watch = cm.watch('/test/example.ts', 'execution');
73
- watch.promise.catch(noop);
74
-
75
- emit({ type: 'app_exited' });
76
-
77
- const error = await watch.promise.catch((err: NativeCrashError) => err);
78
- expect(error).toBeInstanceOf(NativeCrashError);
79
- expect(error.testFilePath).toBe('/test/example.ts');
80
- expect(error.details.phase).toBe('execution');
81
- expect(cm.isAlive()).toBe(false);
82
- });
83
-
84
- it('classifies bridge disconnect plus exited app as NativeCrashError', async () => {
85
- const { session, setState } = createAppSessionMock();
86
- const cm = createCrashMonitor({ appSession: session });
87
- const watch = cm.watch('test.ts', 'execution');
88
- watch.promise.catch(noop);
89
-
90
- cm.handleBridgeDisconnect();
91
- setState({
92
- status: 'exited',
93
- occurredAt: Date.now(),
94
- reason: 'process-gone',
95
- });
96
-
97
- await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
98
- });
99
-
100
- it('classifies bridge disconnect plus running app as RuntimeDisconnectError', async () => {
101
- const { session } = createAppSessionMock({ status: 'running' });
102
- const cm = createCrashMonitor({ appSession: session });
103
- const watch = cm.watch('test.ts', 'execution');
104
- watch.promise.catch(noop);
105
-
106
- cm.handleBridgeDisconnect();
107
-
108
- await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
109
- });
110
-
111
- it('attaches matching session log evidence to native crash details', async () => {
112
- const occurredAt = Date.now();
113
- const logs = [
114
- { line: 'ordinary app log', occurredAt },
115
- { line: 'MyApp[123] fatal error: boom', occurredAt },
116
- ];
117
- const { session, setState } = createAppSessionMock(
118
- { status: 'running' },
119
- logs
120
- );
121
- const cm = createCrashMonitor({ appSession: session });
122
- const watch = cm.watch('test.ts', 'execution');
123
- watch.promise.catch(noop);
124
-
125
- cm.handleBridgeDisconnect();
126
- setState({ status: 'exited', occurredAt, reason: 'process-gone' });
127
-
128
- const error = await watch.promise.catch((err: NativeCrashError) => err);
129
- expect(error).toBeInstanceOf(NativeCrashError);
130
- expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
131
- });
132
-
133
- it('asks the app session to extract native crash details for the current test', async () => {
134
- const occurredAt = Date.now();
135
- const getCrashDetails = vi.fn(async () => ({
136
- artifactType: 'logcat' as const,
137
- artifactPath: '/tmp/.harness/crash-reports/crash-logcat.txt',
138
- processName: 'com.harnessplayground',
139
- pid: 7777,
140
- }));
141
- const { session, setState } = createAppSessionMock(
142
- { status: 'running' },
143
- [],
144
- getCrashDetails
145
- );
146
- const cm = createCrashMonitor({ appSession: session });
147
- const watch = cm.watch('/test/example.ts', 'execution');
148
- watch.promise.catch(noop);
149
-
150
- cm.handleBridgeDisconnect();
151
- setState({
152
- status: 'exited',
153
- occurredAt,
154
- pid: 7777,
155
- reason: 'process-gone',
156
- });
157
-
158
- const error = await watch.promise.catch((err: NativeCrashError) => err);
159
-
160
- expect(getCrashDetails).toHaveBeenCalledWith({
161
- occurredAt: expect.any(Number),
162
- pid: 7777,
163
- processName: undefined,
164
- testFilePath: '/test/example.ts',
165
- });
166
- expect(error.details.artifactPath).toBe(
167
- '/tmp/.harness/crash-reports/crash-logcat.txt'
168
- );
169
- expect(error.message).toContain(
170
- `Harness extracted the crash log: ${path.relative(
171
- process.cwd(),
172
- '/tmp/.harness/crash-reports/crash-logcat.txt'
173
- )}`
174
- );
175
- });
176
-
177
- it('falls back to log details when native crash extraction fails', async () => {
178
- const occurredAt = Date.now();
179
- const getCrashDetails = vi.fn(async () => {
180
- throw new Error('copy failed');
181
- });
182
- const { session, setState } = createAppSessionMock(
183
- { status: 'running' },
184
- [{ line: 'MyApp[123] fatal error: boom', occurredAt }],
185
- getCrashDetails
186
- );
187
- const cm = createCrashMonitor({ appSession: session });
188
- const watch = cm.watch('/test/example.ts', 'execution');
189
- watch.promise.catch(noop);
190
-
191
- cm.handleBridgeDisconnect();
192
- setState({ status: 'exited', occurredAt, reason: 'process-gone' });
193
-
194
- const error = await watch.promise.catch((err: NativeCrashError) => err);
195
-
196
- expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
197
- expect(error.message).toContain("Harness couldn't extract the crash log.");
198
- });
199
-
200
- it('settles the promise with CrashWatchCancelledError on cancel()', async () => {
201
- const cm = createCrashMonitor();
202
- const watch = cm.watch('test.ts', 'execution');
203
-
204
- watch.cancel();
205
-
206
- await expect(watch.promise).rejects.toBeInstanceOf(
207
- CrashWatchCancelledError
208
- );
209
- });
210
-
211
- it('ignores session events while stopped', async () => {
212
- const { session, emit } = createAppSessionMock({
213
- status: 'exited',
214
- occurredAt: Date.now(),
215
- });
216
- const cm = createCrashMonitor({ appSession: session });
217
- await cm.stop();
218
-
219
- const watch = cm.watch('test.ts', 'execution');
220
- const settled = vi.fn();
221
- watch.promise.then(settled, settled);
222
-
223
- emit({ type: 'app_exited' });
224
- await waitForClassification();
225
-
226
- expect(settled).not.toHaveBeenCalled();
227
- watch.cancel();
228
- });
229
-
230
- it('removes the app session listener on dispose', async () => {
231
- const { session } = createAppSessionMock();
232
- const cm = createCrashMonitor({ appSession: session });
233
-
234
- await cm.dispose();
235
-
236
- expect(session.removeListener).toHaveBeenCalledOnce();
237
- expect(cm.isAlive()).toBe(false);
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
- });
323
- });
@@ -1,308 +0,0 @@
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
- });