@react-native-harness/platform-android 1.2.0 → 1.4.0-rc.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 (41) hide show
  1. package/dist/__tests__/app-monitor.test.js +180 -102
  2. package/dist/__tests__/crash-diagnostics.test.d.ts +2 -0
  3. package/dist/__tests__/crash-diagnostics.test.d.ts.map +1 -0
  4. package/dist/__tests__/crash-diagnostics.test.js +119 -0
  5. package/dist/__tests__/crash-reporter.test.d.ts +2 -0
  6. package/dist/__tests__/crash-reporter.test.d.ts.map +1 -0
  7. package/dist/__tests__/crash-reporter.test.js +150 -0
  8. package/dist/__tests__/instance.test.js +133 -24
  9. package/dist/adb.d.ts +4 -0
  10. package/dist/adb.d.ts.map +1 -1
  11. package/dist/adb.js +42 -2
  12. package/dist/app-session.d.ts +17 -0
  13. package/dist/app-session.d.ts.map +1 -0
  14. package/dist/app-session.js +209 -0
  15. package/dist/crash-diagnostics.d.ts +40 -0
  16. package/dist/crash-diagnostics.d.ts.map +1 -0
  17. package/dist/crash-diagnostics.js +256 -0
  18. package/dist/crash-reporter.d.ts +12 -0
  19. package/dist/crash-reporter.d.ts.map +1 -0
  20. package/dist/crash-reporter.js +150 -0
  21. package/dist/device-state.d.ts +6 -0
  22. package/dist/device-state.d.ts.map +1 -0
  23. package/dist/device-state.js +142 -0
  24. package/dist/instance.d.ts +1 -1
  25. package/dist/instance.d.ts.map +1 -1
  26. package/dist/instance.js +32 -59
  27. package/dist/runner.d.ts.map +1 -1
  28. package/dist/runner.js +1 -1
  29. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  30. package/package.json +4 -4
  31. package/src/__tests__/crash-diagnostics.test.ts +144 -0
  32. package/src/__tests__/crash-reporter.test.ts +167 -0
  33. package/src/__tests__/instance.test.ts +174 -31
  34. package/src/adb.ts +61 -2
  35. package/src/app-session.ts +300 -0
  36. package/src/crash-diagnostics.ts +471 -0
  37. package/src/crash-reporter.ts +263 -0
  38. package/src/instance.ts +56 -91
  39. package/src/runner.ts +7 -3
  40. package/test/setup.ts +1 -0
  41. package/src/app-monitor.ts +0 -577
@@ -1,17 +1,7 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { tmpdir } from 'node:os';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { NativeCrashError } from '@react-native-harness/platforms';
5
3
  import { createAndroidAppMonitor, createAndroidLogEvent } from '../app-monitor.js';
6
- import { createCrashArtifactWriter } from '@react-native-harness/tools';
7
4
  import * as adb from '../adb.js';
8
- const createMockSubprocess = () => ({
9
- nodeChildProcess: Promise.resolve({
10
- kill: vi.fn(),
11
- }),
12
- // eslint-disable-next-line @typescript-eslint/no-empty-function
13
- [Symbol.asyncIterator]: async function* () { },
14
- });
15
5
  const createStreamingSubprocess = (chunks) => ({
16
6
  nodeChildProcess: Promise.resolve({
17
7
  kill: vi.fn(),
@@ -25,155 +15,243 @@ const createStreamingSubprocess = (chunks) => ({
25
15
  }
26
16
  },
27
17
  });
28
- const artifactRoot = fs.mkdtempSync(path.join(tmpdir(), 'rn-harness-android-monitor-artifacts-'));
29
- afterEach(() => {
30
- fs.rmSync(artifactRoot, { recursive: true, force: true });
31
- fs.mkdirSync(artifactRoot, { recursive: true });
32
- });
33
18
  describe('createAndroidLogEvent', () => {
34
- it('extracts crash details from fatal signal log lines', () => {
35
- const event = createAndroidLogEvent('03-12 11:35:08.000 1234 1234 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) in tid 1234 (com.harnessplayground), pid 1234 (com.harnessplayground)', 'com.harnessplayground');
19
+ it('detects confirmation-grade am_crash events', () => {
20
+ const event = createAndroidLogEvent('05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,boom]', 'com.harnessplayground');
36
21
  expect(event).toMatchObject({
37
- type: 'possible_crash',
38
- source: 'logs',
22
+ type: 'crash_confirmed',
39
23
  crashDetails: {
24
+ platform: 'android',
25
+ kind: 'java-exception',
26
+ confidence: 'high',
40
27
  source: 'logs',
41
- signal: 'SIGSEGV',
42
- summary: '03-12 11:35:08.000 1234 1234 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) in tid 1234 (com.harnessplayground), pid 1234 (com.harnessplayground)',
43
28
  },
44
29
  });
45
30
  });
46
- it('extracts process and pid when AndroidRuntime reports a crash', () => {
47
- const event = createAndroidLogEvent('03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234', 'com.harnessplayground');
48
- expect(event).toMatchObject({
49
- type: 'possible_crash',
50
- pid: 1234,
51
- crashDetails: {
52
- processName: 'com.harnessplayground',
53
- pid: 1234,
54
- },
55
- });
31
+ });
32
+ describe('createAndroidAppMonitor', () => {
33
+ beforeEach(() => {
34
+ vi.restoreAllMocks();
35
+ vi.useFakeTimers();
36
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('05-20 12:00:00.000');
37
+ });
38
+ afterEach(() => {
39
+ vi.useRealTimers();
56
40
  });
57
- it('starts logcat from the current device timestamp', async () => {
58
- const getLogcatTimestampSpy = vi
59
- .spyOn(adb, 'getLogcatTimestamp')
60
- .mockResolvedValue('03-12 11:35:08.000');
61
- const startLogcatSpy = vi
41
+ it('starts logcat with crash, main, system, and events buffers', async () => {
42
+ const startLogcat = vi
62
43
  .spyOn(adb, 'startLogcat')
63
- .mockReturnValue(createMockSubprocess());
44
+ .mockReturnValue(createStreamingSubprocess([]));
64
45
  const monitor = createAndroidAppMonitor({
65
46
  adbId: 'emulator-5554',
66
47
  bundleId: 'com.harnessplayground',
67
48
  appUid: 10234,
49
+ isAppRunning: async () => true,
68
50
  });
69
51
  await monitor.start();
70
52
  await monitor.stop();
71
- expect(getLogcatTimestampSpy).toHaveBeenCalledWith('emulator-5554');
72
- expect(startLogcatSpy).toHaveBeenCalledWith('emulator-5554', [
53
+ expect(startLogcat).toHaveBeenCalledWith('emulator-5554', [
73
54
  'logcat',
74
- '-v',
75
- 'threadtime',
76
55
  '-b',
77
56
  'crash',
78
- '--uid=10234',
57
+ '-b',
58
+ 'main',
59
+ '-b',
60
+ 'system',
61
+ '-b',
62
+ 'events',
63
+ '-v',
64
+ 'threadtime',
79
65
  '-T',
80
- '03-12 11:35:08.000',
66
+ '05-20 12:00:00.000',
81
67
  ]);
82
68
  });
83
- it('hydrates crash details with stack lines that arrive after the first crash event', async () => {
84
- vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('03-12 10:44:40.000');
69
+ it('reports app lifecycle and crash events through the reporter', async () => {
70
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
71
+ {
72
+ line: '05-20 12:00:00.000 1234 1234 I ActivityManager: Start proc 1234:com.harnessplayground/u0a123 for activity',
73
+ },
74
+ {
75
+ line: '05-20 12:00:00.100 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
76
+ },
77
+ {
78
+ line: '05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,boom]',
79
+ },
80
+ ]));
81
+ const eventReporter = vi.fn();
82
+ const monitor = createAndroidAppMonitor({
83
+ adbId: 'emulator-5554',
84
+ bundleId: 'com.harnessplayground',
85
+ appUid: 10234,
86
+ isAppRunning: async () => true,
87
+ eventReporter,
88
+ });
89
+ const crashWatch = monitor.watch('example.test.ts', 'startup');
90
+ crashWatch.promise.catch(() => undefined);
91
+ monitor.launchRequested({
92
+ type: 'launch_requested',
93
+ launchId: 'launch-1',
94
+ at: Date.now(),
95
+ reason: 'start',
96
+ });
97
+ await monitor.start();
98
+ await vi.advanceTimersByTimeAsync(100);
99
+ expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
100
+ type: 'app:started',
101
+ appPlatform: 'android',
102
+ targetIdentifier: 'emulator-5554',
103
+ launchId: 'launch-1',
104
+ }));
105
+ expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
106
+ type: 'app:crash-suspected',
107
+ appPlatform: 'android',
108
+ processName: 'com.harnessplayground',
109
+ }));
110
+ expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
111
+ type: 'app:crash-confirmed',
112
+ appPlatform: 'android',
113
+ processName: 'com.harnessplayground',
114
+ }));
115
+ await monitor.stop();
116
+ });
117
+ it('rejects the watch for Java crashes when am_crash arrives even if the process still looks alive', async () => {
85
118
  vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
86
- { line: '--------- beginning of crash' },
87
119
  {
88
- line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
120
+ line: '05-20 12:00:00.000 1234 1234 I ActivityManager: Start proc 1234:com.harnessplayground/u0a123 for activity',
89
121
  },
90
122
  {
91
- line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
92
- delayMs: 25,
123
+ line: '05-20 12:00:00.100 1234 1234 E AndroidRuntime: FATAL EXCEPTION: main',
93
124
  },
94
125
  {
95
- line: '03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
96
- delayMs: 25,
126
+ line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
127
+ },
128
+ {
129
+ line: '05-20 12:00:00.102 1234 1234 E AndroidRuntime: java.lang.IllegalStateException: Intentional pre-RN startup crash',
130
+ },
131
+ {
132
+ line: '05-20 12:00:00.103 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
133
+ },
134
+ {
135
+ line: '05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,Intentional pre-RN startup crash]',
97
136
  },
98
137
  ]));
99
138
  const monitor = createAndroidAppMonitor({
100
139
  adbId: 'emulator-5554',
101
140
  bundleId: 'com.harnessplayground',
102
141
  appUid: 10234,
142
+ isAppRunning: async () => true,
103
143
  });
144
+ const crashWatch = monitor.watch('example.test.ts', 'startup');
145
+ crashWatch.promise.catch(() => undefined);
104
146
  await monitor.start();
105
- await new Promise((resolve) => setTimeout(resolve, 10));
106
- const details = await monitor.getCrashDetails({
107
- pid: 13861,
108
- occurredAt: Date.now(),
147
+ await vi.advanceTimersByTimeAsync(100);
148
+ await expect(crashWatch.promise).rejects.toMatchObject({
149
+ testFilePath: 'example.test.ts',
150
+ details: {
151
+ phase: 'startup',
152
+ platform: 'android',
153
+ kind: 'java-exception',
154
+ confidence: 'high',
155
+ processName: 'com.harnessplayground',
156
+ pid: 1234,
157
+ exceptionType: 'java.lang.IllegalStateException: Intentional pre-RN startup crash',
158
+ },
109
159
  });
160
+ await expect(crashWatch.promise).rejects.toBeInstanceOf(NativeCrashError);
110
161
  await monitor.stop();
111
- expect(details?.rawLines).toEqual([
112
- '--------- beginning of crash',
113
- '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
114
- '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
115
- '03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
116
- ]);
117
162
  });
118
- it('persists resolved Android crash blocks into .harness', async () => {
119
- vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('03-12 10:44:40.000');
163
+ it('rejects the watch when a suspected crash is followed by process exit', async () => {
120
164
  vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
121
- { line: '--------- beginning of crash' },
122
165
  {
123
- line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
166
+ line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
124
167
  },
125
168
  {
126
- line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
127
- delayMs: 20,
169
+ line: '05-20 12:00:00.102 1234 1234 E AndroidRuntime: java.lang.IllegalStateException: Intentional delayed startup crash',
128
170
  },
129
171
  ]));
172
+ const isAppRunning = vi
173
+ .fn()
174
+ .mockResolvedValueOnce(true)
175
+ .mockResolvedValueOnce(false)
176
+ .mockResolvedValue(false);
130
177
  const monitor = createAndroidAppMonitor({
131
178
  adbId: 'emulator-5554',
132
179
  bundleId: 'com.harnessplayground',
133
180
  appUid: 10234,
134
- crashArtifactWriter: createCrashArtifactWriter({
135
- runnerName: 'android',
136
- platformId: 'android',
137
- rootDir: path.join(artifactRoot, '.harness', 'crash-reports'),
138
- runTimestamp: '2026-03-12T11-35-08-000Z',
139
- }),
181
+ isAppRunning,
140
182
  });
183
+ const crashWatch = monitor.watch('example.test.ts', 'execution');
184
+ crashWatch.promise.catch(() => undefined);
141
185
  await monitor.start();
142
- await new Promise((resolve) => setTimeout(resolve, 10));
143
- const details = await monitor.getCrashDetails({
144
- pid: 13861,
145
- occurredAt: Date.now(),
186
+ await vi.advanceTimersByTimeAsync(350);
187
+ await expect(crashWatch.promise).rejects.toMatchObject({
188
+ details: {
189
+ phase: 'execution',
190
+ platform: 'android',
191
+ kind: 'java-exception',
192
+ processName: 'com.harnessplayground',
193
+ pid: 1234,
194
+ },
146
195
  });
147
196
  await monitor.stop();
148
- expect(details?.artifactPath).toContain('/.harness/crash-reports/');
149
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
150
- expect(fs.readFileSync(details.artifactPath, 'utf8')).toContain('RuntimeException: boom');
151
197
  });
152
- it('can be started again after timestamp lookup fails', async () => {
153
- const timestampError = new Error('date failed');
154
- vi.spyOn(adb, 'getLogcatTimestamp')
155
- .mockRejectedValueOnce(timestampError)
156
- .mockResolvedValueOnce('03-12 11:35:08.000');
157
- const startLogcatSpy = vi
158
- .spyOn(adb, 'startLogcat')
159
- .mockReturnValue(createMockSubprocess());
198
+ it('does not reject the watch when the process exits without a recent crash signal', async () => {
199
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([]));
200
+ const isAppRunning = vi
201
+ .fn()
202
+ .mockResolvedValueOnce(true)
203
+ .mockResolvedValueOnce(false)
204
+ .mockResolvedValue(false);
160
205
  const monitor = createAndroidAppMonitor({
161
206
  adbId: 'emulator-5554',
162
207
  bundleId: 'com.harnessplayground',
163
208
  appUid: 10234,
209
+ isAppRunning,
210
+ });
211
+ const crashWatch = monitor.watch('example.test.ts', 'execution');
212
+ let settled = false;
213
+ const watchHandled = crashWatch.promise.catch(() => {
214
+ settled = true;
164
215
  });
165
- await expect(monitor.start()).rejects.toThrow(timestampError);
166
- await expect(monitor.start()).resolves.toBeUndefined();
216
+ await monitor.start();
217
+ await vi.advanceTimersByTimeAsync(600);
218
+ expect(settled).toBe(false);
219
+ crashWatch.cancel();
167
220
  await monitor.stop();
168
- expect(startLogcatSpy).toHaveBeenCalledWith('emulator-5554', [
169
- 'logcat',
170
- '-v',
171
- 'threadtime',
172
- '-b',
173
- 'crash',
174
- '--uid=10234',
175
- '-T',
176
- '03-12 11:35:08.000',
177
- ]);
221
+ await watchHandled;
222
+ });
223
+ it('suppresses process-exit confirmation during a controlled stop window', async () => {
224
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
225
+ {
226
+ line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
227
+ },
228
+ ]));
229
+ const isAppRunning = vi
230
+ .fn()
231
+ .mockResolvedValueOnce(true)
232
+ .mockResolvedValueOnce(false)
233
+ .mockResolvedValue(false);
234
+ const monitor = createAndroidAppMonitor({
235
+ adbId: 'emulator-5554',
236
+ bundleId: 'com.harnessplayground',
237
+ appUid: 10234,
238
+ isAppRunning,
239
+ });
240
+ const crashWatch = monitor.watch('example.test.ts', 'execution');
241
+ let settled = false;
242
+ const watchHandled = crashWatch.promise.catch(() => {
243
+ settled = true;
244
+ });
245
+ await monitor.start();
246
+ monitor.stopRequested({
247
+ type: 'stop_requested',
248
+ at: Date.now(),
249
+ reason: 'manual',
250
+ });
251
+ await vi.advanceTimersByTimeAsync(400);
252
+ expect(settled).toBe(false);
253
+ crashWatch.cancel();
254
+ await monitor.stop();
255
+ await watchHandled;
178
256
  });
179
257
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=crash-diagnostics.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crash-diagnostics.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/crash-diagnostics.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { collectDropboxArtifacts, filterExitInfo, getBestDropboxArtifact, parseDropboxOutput, } from '../crash-diagnostics.js';
3
+ const javaCrashDropbox = `
4
+ Drop box contents: 1 entries
5
+ Max entries: 1000
6
+ ========================================
7
+ 2026-03-12 10:30:45.123 data_app_crash (text, 500 bytes)
8
+ Process: com.harnessplayground
9
+ PID: 7777
10
+ Package: com.harnessplayground v1 (1.0)
11
+ java.lang.RuntimeException: boom
12
+ \tat com.harnessplayground.MainActivity.onCreate(MainActivity.kt:38)
13
+ `;
14
+ const nativeCrashDropbox = `
15
+ ========================================
16
+ 2026-03-12 10:31:10.456 data_app_native_crash (text, 800 bytes)
17
+ Process: com.harnessplayground
18
+ PID: 8888
19
+ signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xdeadbeef
20
+ Abort message: 'JNI DETECTED ERROR IN APPLICATION'
21
+ backtrace:
22
+ #00 pc 00001234 /data/app/lib/libapp.so
23
+ `;
24
+ const deviceJavaCrashDropbox = `
25
+ Drop box contents: 8 entries
26
+ Searching for: data_app_crash
27
+
28
+ ========================================
29
+ 2026-05-29 10:22:34 data_app_crash (text, 1328 bytes)
30
+ SystemUptimeMs: 79467172
31
+ Process: com.harnessplayground
32
+ PID: 586
33
+ Package: com.harnessplayground v1 (1.0)
34
+ java.lang.RuntimeException: Intentional asynchronous Kotlin crash
35
+ \tat com.harnessplayground.PlaygroundCrashModule.crashFromKotlinAsync(PlaygroundCrashModule.kt:44)
36
+ `;
37
+ describe('collectDropboxArtifacts', () => {
38
+ it('matches harness playground crashes from merged per-tag dropbox output', async () => {
39
+ const artifacts = await collectDropboxArtifacts({
40
+ bundleId: 'com.harnessplayground',
41
+ getDropboxOutput: async () => deviceJavaCrashDropbox,
42
+ occurredAt: Date.now(),
43
+ pid: 586,
44
+ });
45
+ expect(artifacts).toHaveLength(1);
46
+ expect(artifacts[0]).toMatchObject({
47
+ artifactType: 'dropbox-crash',
48
+ pid: 586,
49
+ processName: 'com.harnessplayground',
50
+ });
51
+ });
52
+ it('ignores Dropbox entries older than the session start time', async () => {
53
+ const artifacts = await collectDropboxArtifacts({
54
+ bundleId: 'com.harnessplayground',
55
+ getDropboxOutput: async () => deviceJavaCrashDropbox,
56
+ minOccurredAt: Date.parse('2026-05-29T10:22:35.000Z'),
57
+ occurredAt: Date.parse('2026-05-29T10:22:36.000Z'),
58
+ });
59
+ expect(artifacts).toHaveLength(0);
60
+ });
61
+ });
62
+ describe('parseDropboxOutput', () => {
63
+ it('parses java and native dropbox entries', () => {
64
+ const entries = parseDropboxOutput(`${javaCrashDropbox}\n${nativeCrashDropbox}`);
65
+ expect(entries).toHaveLength(2);
66
+ expect(entries[0]).toMatchObject({
67
+ tag: 'data_app_crash',
68
+ pid: 7777,
69
+ });
70
+ expect(entries[1]).toMatchObject({
71
+ tag: 'data_app_native_crash',
72
+ pid: 8888,
73
+ });
74
+ expect(entries[1]?.content).toContain('SIGSEGV');
75
+ });
76
+ });
77
+ describe('getBestDropboxArtifact', () => {
78
+ it('prefers the highest-scored artifact', () => {
79
+ const entries = parseDropboxOutput(`${javaCrashDropbox}\n${nativeCrashDropbox}`);
80
+ const artifacts = entries.map((entry, index) => ({
81
+ artifactType: entry.tag === 'data_app_native_crash'
82
+ ? 'dropbox-native-crash'
83
+ : 'dropbox-crash',
84
+ dropboxTag: entry.tag,
85
+ occurredAt: Date.now(),
86
+ score: index === 0 ? 100 : 300,
87
+ summary: entry.content,
88
+ pid: entry.pid,
89
+ processName: 'com.harnessplayground',
90
+ }));
91
+ expect(getBestDropboxArtifact(artifacts)?.dropboxTag).toBe('data_app_native_crash');
92
+ });
93
+ });
94
+ describe('filterExitInfo', () => {
95
+ it('returns matching exit-info records for the target package and pid', () => {
96
+ const output = `
97
+ ApplicationExitInfo #0:
98
+ package=com.harnessplayground pid=7777 realUid=10123
99
+ reason=4 (APP CRASH(NATIVE))
100
+ timestamp=2026-03-12 10:31:10.456
101
+ ApplicationExitInfo #1:
102
+ package=com.other.app pid=9999 realUid=10199
103
+ reason=10 (USER REQUESTED)
104
+ `;
105
+ const filtered = filterExitInfo({
106
+ output,
107
+ bundleId: 'com.harnessplayground',
108
+ pid: 7777,
109
+ });
110
+ expect(filtered).toContain('com.harnessplayground pid=7777');
111
+ expect(filtered).not.toContain('com.other.app');
112
+ });
113
+ it('returns null when no exit-info records exist', () => {
114
+ expect(filterExitInfo({
115
+ output: 'No exit info records for com.harnessplayground',
116
+ bundleId: 'com.harnessplayground',
117
+ })).toBeNull();
118
+ });
119
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=crash-reporter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crash-reporter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/crash-reporter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,150 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { createAndroidCrashReporter } from '../crash-reporter.js';
3
+ const javaCrashDropbox = `
4
+ ========================================
5
+ 2026-03-12 10:30:45.123 data_app_crash (text, 500 bytes)
6
+ Process: com.harnessplayground
7
+ PID: 7777
8
+ Package: com.harnessplayground v1 (1.0)
9
+ java.lang.RuntimeException: boom
10
+ \tat com.harnessplayground.MainActivity.onCreate(MainActivity.kt:38)
11
+ `;
12
+ const nativeCrashDropbox = `
13
+ ========================================
14
+ 2026-03-12 10:31:10.456 data_app_native_crash (text, 800 bytes)
15
+ Process: com.harnessplayground
16
+ PID: 8888
17
+ signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xdeadbeef
18
+ Abort message: 'JNI DETECTED ERROR IN APPLICATION'
19
+ backtrace:
20
+ #00 pc 00001234 /data/app/lib/libapp.so
21
+ `;
22
+ describe('createAndroidCrashReporter', () => {
23
+ afterEach(() => {
24
+ vi.useRealTimers();
25
+ });
26
+ it('persists the current logcat crash block with the test file path', async () => {
27
+ vi.useFakeTimers();
28
+ const persistArtifact = vi.fn(() => '/tmp/.harness/crash-reports/logcat.txt');
29
+ const reporter = createAndroidCrashReporter({
30
+ bundleId: 'com.harnessplayground',
31
+ crashArtifactWriter: {
32
+ runTimestamp: '2026-03-12T11-35-08-000Z',
33
+ persistArtifact,
34
+ },
35
+ getLogs: () => [
36
+ { line: 'ordinary crash-buffer line', occurredAt: 1_000 },
37
+ { line: '--------- beginning of crash', occurredAt: 1_100 },
38
+ {
39
+ line: 'Process: com.harnessplayground, PID: 7777',
40
+ occurredAt: 1_100,
41
+ },
42
+ { line: 'FATAL EXCEPTION: main', occurredAt: 1_100 },
43
+ ],
44
+ });
45
+ const detailsPromise = reporter.getCrashDetails({
46
+ occurredAt: 1_100,
47
+ pid: 7777,
48
+ testFilePath: '/test/native-crash.test.ts',
49
+ });
50
+ await vi.advanceTimersByTimeAsync(100);
51
+ const details = await detailsPromise;
52
+ expect(details).toMatchObject({
53
+ artifactType: 'logcat',
54
+ artifactPath: '/tmp/.harness/crash-reports/logcat.txt',
55
+ pid: 7777,
56
+ processName: 'com.harnessplayground',
57
+ });
58
+ expect(persistArtifact).toHaveBeenCalledWith({
59
+ artifactKind: 'logcat',
60
+ testFilePath: '/test/native-crash.test.ts',
61
+ source: {
62
+ kind: 'text',
63
+ fileName: 'logcat.txt',
64
+ text: [
65
+ '--------- beginning of crash',
66
+ 'Process: com.harnessplayground, PID: 7777',
67
+ 'FATAL EXCEPTION: main',
68
+ '',
69
+ ].join('\n'),
70
+ },
71
+ });
72
+ });
73
+ it('adds dropbox artifacts as enrichment when logcat evidence is available', async () => {
74
+ vi.useFakeTimers();
75
+ const persistArtifact = vi
76
+ .fn()
77
+ .mockImplementation(({ artifactKind }) => {
78
+ return `/tmp/.harness/crash-reports/${artifactKind}.txt`;
79
+ });
80
+ const reporter = createAndroidCrashReporter({
81
+ bundleId: 'com.harnessplayground',
82
+ crashArtifactWriter: {
83
+ runTimestamp: '2026-03-12T11-35-08-000Z',
84
+ persistArtifact,
85
+ },
86
+ getLogs: () => [
87
+ { line: '--------- beginning of crash', occurredAt: 1_100 },
88
+ {
89
+ line: 'Process: com.harnessplayground, PID: 7777',
90
+ occurredAt: 1_100,
91
+ },
92
+ { line: 'FATAL EXCEPTION: main', occurredAt: 1_100 },
93
+ ],
94
+ getDropboxOutput: async () => javaCrashDropbox,
95
+ getExitInfo: async () => 'ApplicationExitInfo #0:\n package=com.harnessplayground pid=7777 reason=4 (APP CRASH(NATIVE))',
96
+ });
97
+ const detailsPromise = reporter.getCrashDetails({
98
+ occurredAt: 1_100,
99
+ pid: 7777,
100
+ testFilePath: '/test/native-crash.test.ts',
101
+ });
102
+ await vi.advanceTimersByTimeAsync(100);
103
+ const details = await detailsPromise;
104
+ expect(details).toMatchObject({
105
+ artifactType: 'logcat',
106
+ artifactPath: '/tmp/.harness/crash-reports/logcat.txt',
107
+ });
108
+ expect(details?.enrichmentArtifacts).toEqual(expect.arrayContaining([
109
+ {
110
+ artifactType: 'dropbox-crash',
111
+ artifactPath: '/tmp/.harness/crash-reports/dropbox-crash.txt',
112
+ },
113
+ {
114
+ artifactType: 'exit-info',
115
+ artifactPath: '/tmp/.harness/crash-reports/exit-info.txt',
116
+ },
117
+ ]));
118
+ });
119
+ it('falls back to native dropbox evidence when logcat is empty', async () => {
120
+ vi.useFakeTimers();
121
+ const persistArtifact = vi
122
+ .fn()
123
+ .mockImplementation(({ artifactKind }) => {
124
+ return `/tmp/.harness/crash-reports/${artifactKind}.txt`;
125
+ });
126
+ const reporter = createAndroidCrashReporter({
127
+ bundleId: 'com.harnessplayground',
128
+ crashArtifactWriter: {
129
+ runTimestamp: '2026-03-12T11-35-08-000Z',
130
+ persistArtifact,
131
+ },
132
+ getLogs: () => [],
133
+ getDropboxOutput: async () => nativeCrashDropbox,
134
+ });
135
+ const detailsPromise = reporter.getCrashDetails({
136
+ occurredAt: 1_100,
137
+ pid: 8888,
138
+ testFilePath: '/test/native-crash.test.ts',
139
+ });
140
+ await vi.advanceTimersByTimeAsync(100);
141
+ const details = await detailsPromise;
142
+ expect(details).toMatchObject({
143
+ artifactType: 'dropbox-native-crash',
144
+ artifactPath: '/tmp/.harness/crash-reports/dropbox-native-crash.txt',
145
+ pid: 8888,
146
+ signal: 'SIGSEGV',
147
+ processName: 'com.harnessplayground',
148
+ });
149
+ });
150
+ });