Skip to content

Commit 609c825

Browse files
Lykhoydaclaude
andauthored
fix(B191,B192): post-flow lifecycle hardening — classifier coverage + benign-close precision (#247)
* fix(#243): classify startAndroidRunner startup failures as RN_ANDROID_RUNNER_DOWN (B191) startAndroidRunner has two rejection shapes the classifier missed: 'exited before readiness' (app not installed / am instrument crash) and 'Failed to spawn' (adb missing). Both are runner-down conditions thrown inside runAndroid's try{} — they escaped as raw exceptions instead of the structured retryable error #243 introduced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#244): never classify an unparseable close payload as benign (B192) The plain-text fallback ran the session-gone regex over the FULL raw content when JSON.parse failed, contradicting the documented invariant (match only the error field). A non-JSON adb error mentioning 'no active session' anywhere would be swallowed and local session state cleared while the underlying session might still be alive. Unparseable payloads now surface unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(B191,B192): changeset for post-flow lifecycle hardening Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c9d447d commit 609c825

7 files changed

Lines changed: 48 additions & 4 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"rn-dev-agent-cdp": patch
3+
"rn-dev-agent-plugin": patch
4+
---
5+
6+
fix(B191,B192): post-flow lifecycle hardening follow-ups to #243/#244. `isAndroidConnectionFailure` now also classifies `startAndroidRunner`'s startup-failure shapes (`exited before readiness`, `Failed to spawn Android runner instrumentation`) into the structured retryable `RN_ANDROID_RUNNER_DOWN` instead of letting a startup crash escape as a raw exception. And `isBenignSessionGoneError` no longer runs its session-gone regex over unparseable (non-JSON) close payloads — with no error field to scope the match to, they surface unchanged, so a real close failure whose raw text merely mentions "no active session" can't be silently swallowed.

scripts/cdp-bridge/dist/runners/rn-android-runner-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,5 +354,5 @@ function errMessage(err) {
354354
return err instanceof Error ? err.message : String(err);
355355
}
356356
export function isAndroidConnectionFailure(message) {
357-
return /fetch failed|ECONNREFUSED|ECONNRESET|socket hang up|rn-android-runner not started|did not become ready/i.test(message);
357+
return /fetch failed|ECONNREFUSED|ECONNRESET|socket hang up|rn-android-runner not started|did not become ready|Android runner instrumentation exited before readiness|Failed to spawn Android runner instrumentation/i.test(message);
358358
}

scripts/cdp-bridge/dist/tools/device-session-close.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ export function isBenignSessionGoneError(result) {
1919
envelope = JSON.parse(text);
2020
}
2121
catch {
22-
return /no active session|session not found/i.test(text);
22+
// B192: unparseable payload → no error field to scope the match to. Never
23+
// classify as benign; surface it so a real failure can't be swallowed just
24+
// because its raw text mentions the phrase.
25+
return false;
2326
}
2427
if ((envelope.meta?.code ?? envelope.code) === 'SESSION_NOT_FOUND')
2528
return true;

scripts/cdp-bridge/src/runners/rn-android-runner-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,5 +406,5 @@ function errMessage(err: unknown): string {
406406
}
407407

408408
export function isAndroidConnectionFailure(message: string): boolean {
409-
return /fetch failed|ECONNREFUSED|ECONNRESET|socket hang up|rn-android-runner not started|did not become ready/i.test(message);
409+
return /fetch failed|ECONNREFUSED|ECONNRESET|socket hang up|rn-android-runner not started|did not become ready|Android runner instrumentation exited before readiness|Failed to spawn Android runner instrumentation/i.test(message);
410410
}

scripts/cdp-bridge/src/tools/device-session-close.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ export function isBenignSessionGoneError(result: ToolResult): boolean {
2727
try {
2828
envelope = JSON.parse(text) as { error?: string; code?: string; meta?: { code?: string } };
2929
} catch {
30-
return /no active session|session not found/i.test(text);
30+
// B192: unparseable payload → no error field to scope the match to. Never
31+
// classify as benign; surface it so a real failure can't be swallowed just
32+
// because its raw text mentions the phrase.
33+
return false;
3134
}
3235
if ((envelope.meta?.code ?? envelope.code) === 'SESSION_NOT_FOUND') return true;
3336
return /no active session|session not found/i.test(envelope.error ?? '');

scripts/cdp-bridge/test/unit/gh-243-android-runner-health.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ test('#243 isAndroidConnectionFailure matches both startAndroidRunner + postComm
5353
assert.equal(isAndroidConnectionFailure('app not started'), false);
5454
});
5555

56+
// B191: startAndroidRunner has two MORE rejection shapes — instrumentation exiting before
57+
// readiness (app not installed, am instrument crash) and spawn failure (adb missing). Both
58+
// are runner-down conditions thrown inside runAndroid's try{} and must classify into the
59+
// structured RN_ANDROID_RUNNER_DOWN path, not escape as raw exceptions.
60+
test('#243/B191 isAndroidConnectionFailure matches startAndroidRunner startup-failure shapes', () => {
61+
assert.equal(isAndroidConnectionFailure('Android runner instrumentation exited before readiness (code 1)\nINSTRUMENTATION_FAILED: dev.lykhoyda.rnandroidrunner.test'), true);
62+
assert.equal(isAndroidConnectionFailure('Failed to spawn Android runner instrumentation: spawn adb ENOENT'), true);
63+
});
64+
5665
test('#243 runAndroid returns RN_ANDROID_RUNNER_DOWN (not bare "fetch failed") on connection failure', async () => {
5766
_setAndroidRunnerStateForTest({
5867
port: 22089,

scripts/cdp-bridge/test/unit/gh-244-close-session-gone.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,26 @@ test('#244 isBenignSessionGoneError matches only gone-session shapes', () => {
8181
};
8282
assert.equal(isBenignSessionGoneError(withHint), false);
8383
});
84+
85+
// B192: an UNPARSEABLE (non-JSON) payload is an unexpected upstream shape — there is no
86+
// error field to scope the match to, so it must never classify as benign, even when the
87+
// raw text mentions the phrase (e.g. a multi-line adb error). Surfacing it is the safe path.
88+
test('#244/B192 non-JSON payload mentioning the phrase is NOT benign', async () => {
89+
const plainText = {
90+
content: [{ type: 'text', text: 'adb: error: failed to close; no active session on device emulator-5554' }],
91+
isError: true,
92+
};
93+
assert.equal(isBenignSessionGoneError(plainText), false);
94+
95+
// and closeDeviceSession must surface it unchanged, leaving local state intact
96+
const calls = { clear: 0, stop: 0, release: 0 };
97+
const r = await closeDeviceSession({
98+
hasActiveSession: () => true,
99+
closeUnderlyingSession: async () => plainText,
100+
clearActiveSession: () => { calls.clear++; },
101+
stopFastRunner: () => { calls.stop++; },
102+
releaseDeviceLock: () => { calls.release++; },
103+
});
104+
assert.equal(r.isError, true);
105+
assert.deepEqual(calls, { clear: 0, stop: 0, release: 0 });
106+
});

0 commit comments

Comments
 (0)