Skip to content

Commit edd6977

Browse files
committed
fix(ios): key the install hint on the devicectl tool, not the device kind
1 parent 6d55663 commit edd6977

3 files changed

Lines changed: 22 additions & 29 deletions

File tree

‎packages/platform-apple/src/__tests__/app-error.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import assert from 'node:assert/strict';
2-
import { AppError, normalizeError } from '@agent-device/kernel/errors';
2+
import { AppError, defaultHintForCode, normalizeError } from '@agent-device/kernel/errors';
33

44
type ExpectedAppError = {
55
code: string;
66
message?: RegExp;
77
/** Checked against `normalizeError(error).message`, e.g. the stderr excerpt a command failure appends. */
88
normalizedMessage?: RegExp;
9-
hint?: string | RegExp;
9+
/** `null` asserts the failure attached no hint of its own, so normalization falls back to the code default. */
10+
hint?: string | RegExp | null;
1011
};
1112

1213
function assertAppError(error: unknown, expected: ExpectedAppError): true {
@@ -19,7 +20,9 @@ function assertAppError(error: unknown, expected: ExpectedAppError): true {
1920
if (expected.normalizedMessage) {
2021
assert.match(normalizeError(error).message, expected.normalizedMessage);
2122
}
22-
if (expected.hint !== undefined) {
23+
if (expected.hint === null) {
24+
assert.equal(normalizeError(error).hint, defaultHintForCode(error.code));
25+
} else if (expected.hint !== undefined) {
2326
const { hint } = normalizeError(error);
2427
assert.ok(typeof hint === 'string', `expected a hint on ${error.code}, got ${String(hint)}`);
2528
if (typeof expected.hint === 'string') assert.equal(hint, expected.hint);

‎packages/platform-apple/src/deployment/runtime.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ test('simulator install failure surfaces the simctl stderr excerpt with no devic
390390
{
391391
code: 'COMMAND_FAILED',
392392
normalizedMessage: /Failed to install the requested application/,
393+
hint: null,
393394
},
394395
);
395396
const [request] = run.mock.calls.find(([call]) => call.args.includes('install'))!;
@@ -424,6 +425,7 @@ test('simulator push failure surfaces the simctl stderr excerpt', async () => {
424425
{
425426
code: 'COMMAND_FAILED',
426427
normalizedMessage: /Invalid device state: Booted/,
428+
hint: null,
427429
},
428430
);
429431
const [request] = run.mock.calls.find(([call]) => call.args.includes('push'))!;

‎packages/platform-apple/src/deployment/runtime.ts‎

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ async function installAppleApp(
157157
},
158158
signal,
159159
'Apple app install failed',
160-
{ hint: (result) => devicectlHint(device, result) },
161160
);
162161
}
163162

@@ -182,7 +181,6 @@ async function uninstallAppleApp(
182181
signal,
183182
`Apple app uninstall failed for ${bundleId}`,
184183
{
185-
hint: (result) => devicectlHint(device, result),
186184
tolerate: (result) =>
187185
isMissingAppErrorOutput(`${result.stdout}\n${result.stderr}`.toLowerCase()),
188186
},
@@ -223,39 +221,29 @@ async function pushAppleNotification(
223221
* The one request path this module uses to run a tolerated Apple tool call: it forces
224222
* `allowFailure`, then guards the result itself through `requireExecSuccess` so a non-zero
225223
* exit always throws the same COMMAND_FAILED shape as every other exec call site, with the
226-
* caller's curated message and, optionally, a devicectl hint. `tolerate` lets a caller accept
227-
* a specific non-zero result (uninstall's "already missing" case) without losing that guard
228-
* for every other outcome.
224+
* caller's curated message. A devicectl failure gets the same Developer Mode,
225+
* developer-disk-image, and pairing hints the other devicectl call sites attach. `tolerate`
226+
* lets a caller accept a specific non-zero result (uninstall's "already missing" case)
227+
* without losing that guard for every other outcome.
229228
*/
230229
async function runAppleTool(
231230
host: PlatformRuntimeHost,
232231
request: Omit<AppleToolRequest, 'allowFailure'>,
233232
signal: AbortSignal,
234233
message: string,
235-
options?: Readonly<{
236-
hint?: (result: HostCommandResult) => string | undefined;
237-
tolerate?: (result: HostCommandResult) => boolean;
238-
}>,
234+
options?: Readonly<{ tolerate?: (result: HostCommandResult) => boolean }>,
239235
): Promise<HostCommandResult> {
240236
const result = await host.appleTools.run({ ...request, allowFailure: true }, signal);
241237
if (options?.tolerate?.(result)) return result;
242-
return requireExecSuccess(result, message, (failed) => {
243-
const hint = options?.hint?.(failed);
244-
return { cmd: 'xcrun', args: [request.tool, ...request.args], ...(hint ? { hint } : {}) };
245-
});
246-
}
247-
248-
/**
249-
* Physical iOS install/uninstall runs through devicectl: a failure gets the same Developer
250-
* Mode, developer-disk-image, and pairing hints the other devicectl call sites attach.
251-
* Simulator installs go through simctl, which this resolver does not classify.
252-
*/
253-
function devicectlHint(
254-
device: DeviceInfo,
255-
result: Readonly<{ stdout: string; stderr: string }>,
256-
): string | undefined {
257-
if (device.kind === 'simulator') return undefined;
258-
return resolveIosDevicectlHint(result.stdout, result.stderr) ?? IOS_DEVICECTL_DEFAULT_HINT;
238+
return requireExecSuccess(result, message, (failed) => ({
239+
cmd: 'xcrun',
240+
args: [request.tool, ...request.args],
241+
...(request.tool === 'devicectl'
242+
? {
243+
hint: resolveIosDevicectlHint(failed.stdout, failed.stderr) ?? IOS_DEVICECTL_DEFAULT_HINT,
244+
}
245+
: {}),
246+
}));
259247
}
260248

261249
function appleDeployFact(device: DeviceInfo): RuntimeOperationFact {

0 commit comments

Comments
 (0)