Skip to content

Commit 6190178

Browse files
Lykhoydaclaude
andauthored
fix(#253): derive repair platform from the device session, not hardcoded ios (B197) (#259)
cdp_repair_action pinned targetPlatform='ios' (the gh-105 TODO), so an Android repair foregrounded via simctl, snapshotted through the iOS short-circuit, and bootstrapped the iOS fast-runner — Android selector drift always escalated as a hard failure. The orchestrator now resolves the platform with detectPlatform() (active session first, booted-device probes after, 'ios' only as the no-session/no-device fallback), matching the seven other tools that already use this policy. Everything downstream was already platform-aware: foreground has an adb branch, resolveBundleId reads expo.android.package, and the fast-runner bootstrap is ios-gated. Tests: android-session dispatch asserts platform:'android' reaches the snapshot (red on the old code), plus an ios regression guard. 1854/1854. Closes #253 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent ed65973 commit 6190178

4 files changed

Lines changed: 89 additions & 6 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(#253): `cdp_repair_action` no longer hardcodes `targetPlatform='ios'` — Android auto-repair works against an emulator. The repair orchestrator now derives the platform from the active device session via `detectPlatform()` (booted-device probe fallback when no session is open; `'ios'` only as the final no-session, no-device fallback). Previously an Android repair foregrounded the app via `xcrun simctl`, snapshotted through the iOS short-circuit, and bootstrapped the iOS fast-runner — so Android selector drift always escalated as a hard failure instead of self-healing.

scripts/cdp-bridge/dist/tools/repair-action.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { repairBudgetAvailable, recentRepairCount } from '../domain/reusable-act
1616
import { snapshotEnvelopeFailed } from './device-batch.js';
1717
import { resolveBundleId } from '../project-config.js';
1818
import { isAgentDeviceRunnerSentinel } from './runner-leak-recovery.js';
19+
import { detectPlatform } from './platform-utils.js';
1920
import { stopFastRunner } from '../runners/rn-fast-runner-client.js';
2021
const execFile = promisify(execFileCb);
2122
/**
@@ -146,9 +147,12 @@ export function createRepairActionHandler() {
146147
// GH #105 / B153: bring the target app to foreground before snapshot.
147148
// Without this, the snapshot lands on whichever app is frontmost — often
148149
// the Agent Device Runner (XCTest test rig) which has zero app testIDs.
149-
// Resolve bundle id from app.json via project-config; fall back to ios
150-
// platform if the connected target hasn't told us yet.
151-
const targetPlatform = 'ios'; // TODO(gh-105 follow-up): plumb platform from CDP target
150+
// GH #253 / B197: platform comes from the active device session (probe
151+
// fallback when none is open) — a hardcoded 'ios' made the whole repair
152+
// loop foreground via simctl, snapshot via the iOS short-circuit, and
153+
// bootstrap the iOS fast-runner against Android emulators. 'ios' remains
154+
// the final fallback for the no-session, no-device edge.
155+
const targetPlatform = (await detectPlatform()) ?? 'ios';
152156
const targetBundleId = resolveBundleId(targetPlatform);
153157
if (targetBundleId) {
154158
await bringTargetAppToForeground(targetPlatform, targetBundleId);

scripts/cdp-bridge/src/tools/repair-action.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { repairBudgetAvailable, recentRepairCount } from '../domain/reusable-act
2727
import { snapshotEnvelopeFailed } from './device-batch.js';
2828
import { resolveBundleId } from '../project-config.js';
2929
import { isAgentDeviceRunnerSentinel, type RunnerLeakNode } from './runner-leak-recovery.js';
30+
import { detectPlatform } from './platform-utils.js';
3031
import { stopFastRunner } from '../runners/rn-fast-runner-client.js';
3132

3233
const execFile = promisify(execFileCb);
@@ -221,9 +222,12 @@ export function createRepairActionHandler() {
221222
// GH #105 / B153: bring the target app to foreground before snapshot.
222223
// Without this, the snapshot lands on whichever app is frontmost — often
223224
// the Agent Device Runner (XCTest test rig) which has zero app testIDs.
224-
// Resolve bundle id from app.json via project-config; fall back to ios
225-
// platform if the connected target hasn't told us yet.
226-
const targetPlatform = 'ios'; // TODO(gh-105 follow-up): plumb platform from CDP target
225+
// GH #253 / B197: platform comes from the active device session (probe
226+
// fallback when none is open) — a hardcoded 'ios' made the whole repair
227+
// loop foreground via simctl, snapshot via the iOS short-circuit, and
228+
// bootstrap the iOS fast-runner against Android emulators. 'ios' remains
229+
// the final fallback for the no-session, no-device edge.
230+
const targetPlatform = (await detectPlatform()) ?? 'ios';
227231
const targetBundleId = resolveBundleId(targetPlatform);
228232
if (targetBundleId) {
229233
await bringTargetAppToForeground(targetPlatform, targetBundleId);

scripts/cdp-bridge/test/unit/repair-action-handler.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,75 @@ test('repair-action: happy path patches stale selector with fuzzy match', async
108108
assert.equal(sidecar.repairHistory[0].diff.selector.to, 'fab-create-task-btn');
109109
});
110110

111+
// ─────────────────────────────────────────────────────────────────────────────
112+
// Platform plumbing (GH #253 / B197) — the snapshot dispatch must carry the
113+
// active session's platform, not a hardcoded 'ios'. With 'ios' hardcoded, an
114+
// Android repair foregrounds via simctl, snapshots via the iOS short-circuit,
115+
// and bootstraps the iOS fast-runner — none of which work against an emulator.
116+
// ─────────────────────────────────────────────────────────────────────────────
117+
118+
test('GH #253: android session → snapshot dispatched with platform android', async () => {
119+
setActiveSession({
120+
...FAKE_SESSION,
121+
platform: 'android',
122+
deviceId: 'emulator-5554',
123+
});
124+
project.seedAction(
125+
'android-repair',
126+
fixtureYaml({ id: 'android-repair', selectors: ['fab-create-task'] }),
127+
);
128+
129+
let capturedOpts;
130+
_setRunAgentDeviceForTest(async (cliArgs, opts) => {
131+
capturedOpts = opts;
132+
return {
133+
content: [{ type: 'text', text: fakeSnapshot(['fab-create-task-btn']) }],
134+
};
135+
});
136+
137+
const handler = createRepairActionHandler();
138+
const result = await handler({
139+
actionId: 'android-repair',
140+
failedSelector: 'fab-create-task',
141+
projectRoot: project.root,
142+
});
143+
144+
assert.equal(result.isError, undefined, `expected ok, got ${result.content[0].text}`);
145+
assert.equal(
146+
capturedOpts?.platform,
147+
'android',
148+
`snapshot must dispatch with the session platform; got ${capturedOpts?.platform} — ` +
149+
`a hardcoded 'ios' routes the snapshot through the iOS short-circuit on an emulator`,
150+
);
151+
const env = JSON.parse(result.content[0].text);
152+
assert.equal(env.data.patched, true, 'repair itself must still succeed on android');
153+
});
154+
155+
test('GH #253: ios session → snapshot dispatched with platform ios (regression guard)', async () => {
156+
project.seedAction(
157+
'ios-repair',
158+
fixtureYaml({ id: 'ios-repair', selectors: ['fab-create-task'] }),
159+
);
160+
161+
let capturedOpts;
162+
_setRunAgentDeviceForTest(async (cliArgs, opts) => {
163+
capturedOpts = opts;
164+
return {
165+
content: [{ type: 'text', text: fakeSnapshot(['fab-create-task-btn']) }],
166+
};
167+
});
168+
169+
const handler = createRepairActionHandler();
170+
const result = await handler({
171+
actionId: 'ios-repair',
172+
failedSelector: 'fab-create-task',
173+
projectRoot: project.root,
174+
});
175+
176+
assert.equal(result.isError, undefined, `expected ok, got ${result.content[0].text}`);
177+
assert.equal(capturedOpts?.platform, 'ios');
178+
});
179+
111180
// ─────────────────────────────────────────────────────────────────────────────
112181
// Validation paths
113182
// ─────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)