mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
23c796c52b
* fix(cli): name --udid when a device identity is passed to --device `--device <udid>` failed with "No device named <udid>" and the generic booted/connected hint, and `help commands` listed no device selector at all, so `--udid` — the only flag that pins one simulator among several sharing a name — was reachable only from `help device`'s usage line. Two changes, both discoverability: - `resolveDeviceByName` now answers the flag mistake instead of the literal lookup. When the `--device` value is the id of a listed device it names that device and the flag the value belongs to (`--udid` for Apple, `--serial` for serial-addressable platforms); when it merely has UDID shape it names `--udid`. An ordinary unknown name keeps the generic hint. This mirrors the answer `assertSelectorFlagMatchesPlatform` already gives for a mismatched identity flag. - `help commands` renders a `Device Selection` section for the selectors every command accepts (`--platform`, `--device`, `--udid`, `--serial`, `--session`). They stay in `COMMON_COMMAND_SUPPORTED_FLAG_KEYS`, not `GLOBAL_FLAG_KEYS`, so the option schema and `explain` output are unchanged. Closes #2064 * fix(kernel): ground the wrong-flag hint in observed device identity only The UDID-shape fallback reconstructed Apple identity syntax with a UUID-only regex: it missed modern physical-device UDIDs (00008150-001849640CF8401C), could misclassify a UUID-shaped device name, and justified itself with an inventory state (a shut-down device) the exact-candidate match already covers wherever inventory lists it. Only a listed candidate's actual id now earns the hint; an unlisted value keeps the generic device-not-found answer. Also corrects help physical-device, which claimed --device accepts a UDID (it never did): it now names --udid/--serial for name collisions. Addresses the P1 review on #2065. * fix(kernel,cli): answer the adversarial review on the wrong-flag hint - The hint is emitted only for platforms that HAVE an identity flag: web and linux device ids got 'Did you mean --udid <id>?' even though --udid resolves only Apple devices — a specific, provably dead instruction where the generic hint at least did no harm. - The section header no longer claims every device command accepts all five selectors (connect has no --udid/--serial, device no --session); it points at help <command> for each command's own flags. - The MCP/JSON input schema carries the same discoverability fix the CLI help got: 'Device name selector (a UDID belongs in udid, a serial in serial)' and the sharpened udid description, instead of the bare 'iOS device UDID selector.' the issue was filed about. - --serial's description names HarmonyOS, which the resolver has always accepted alongside Android and Vega. * fix(commands): align serial's schema description with the resolver; pin the selector descriptions The structured/MCP input schema still described serial as Android or Vega only while the resolver and the CLI guidance accept HarmonyOS. A metadata test now pins the three device-selector descriptions on every emitted command schema, so a cross-surface drift fails instead of shipping. Addresses the P2 review on #2065. * fix(commands): annotate the pinned selector property lookup for tsc --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
118 lines
4.7 KiB
TypeScript
118 lines
4.7 KiB
TypeScript
import { test } from 'vitest';
|
|
import assert from 'node:assert/strict';
|
|
import { AppError } from './errors.ts';
|
|
import { resolveDevice, type DeviceInfo } from './device.ts';
|
|
|
|
// `--udid` is Apple, `--serial` is Android/HarmonyOS. Pairing one with the other platform used to
|
|
// reach resolution and answer about the WRONG platform ("No Apple device with UDID emulator-5580"
|
|
// for an explicitly --platform android request), which reads as a missing device rather than a
|
|
// mistyped flag.
|
|
|
|
const ANDROID: DeviceInfo = {
|
|
platform: 'android',
|
|
target: 'mobile',
|
|
id: 'emulator-5580',
|
|
name: 'Pixel 7',
|
|
kind: 'emulator',
|
|
booted: true,
|
|
};
|
|
|
|
const APPLE: DeviceInfo = {
|
|
platform: 'apple',
|
|
target: 'mobile',
|
|
id: 'SIM-001',
|
|
name: 'iPhone 16',
|
|
kind: 'simulator',
|
|
booted: true,
|
|
};
|
|
|
|
test('--udid with an Android platform is rejected as a flag mistake, naming --serial', async () => {
|
|
const error = await resolveError([ANDROID], { platform: 'android', udid: 'emulator-5580' });
|
|
assert.equal(error.code, 'INVALID_ARGS');
|
|
assert.match(error.message, /--udid selects Apple devices/);
|
|
assert.match(
|
|
String(error.details?.hint ?? ''),
|
|
/Use --serial emulator-5580 for android devices\./,
|
|
);
|
|
});
|
|
|
|
test('--serial with an Apple platform is rejected as a flag mistake, naming --udid', async () => {
|
|
const error = await resolveError([APPLE], { platform: 'ios', serial: 'SIM-001' });
|
|
assert.equal(error.code, 'INVALID_ARGS');
|
|
assert.match(error.message, /--serial selects Android and HarmonyOS devices/);
|
|
assert.match(String(error.details?.hint ?? ''), /Use --udid SIM-001 for Apple devices\./);
|
|
});
|
|
|
|
test('matching selector/platform pairs still resolve', async () => {
|
|
assert.equal(
|
|
(await resolveDevice([ANDROID], { platform: 'android', serial: 'emulator-5580' })).id,
|
|
'emulator-5580',
|
|
);
|
|
assert.equal((await resolveDevice([APPLE], { platform: 'ios', udid: 'SIM-001' })).id, 'SIM-001');
|
|
});
|
|
|
|
test('an unspecified platform keeps the existing device-not-found behavior', async () => {
|
|
const error = await resolveError([ANDROID], { udid: 'emulator-5580' });
|
|
assert.equal(error.code, 'DEVICE_NOT_FOUND');
|
|
});
|
|
|
|
// `--device` takes a NAME. Passing a UDID there answered "No device named <udid>" with the generic
|
|
// booted/connected hint (#2064) — true, unactionable, and silent about `--udid` existing at all.
|
|
|
|
test('a UDID passed to --device names --udid and the device it identifies', async () => {
|
|
const error = await resolveError([APPLE, ANDROID], { deviceName: 'SIM-001' });
|
|
assert.equal(error.code, 'DEVICE_NOT_FOUND');
|
|
assert.match(error.message, /No device named SIM-001/);
|
|
assert.match(
|
|
String(error.details?.hint ?? ''),
|
|
/SIM-001 is the id of "iPhone 16", not its name\. Did you mean --udid SIM-001\?/,
|
|
);
|
|
});
|
|
|
|
test('a serial passed to --device names --serial, not --udid', async () => {
|
|
const error = await resolveError([APPLE, ANDROID], { deviceName: 'emulator-5580' });
|
|
assert.equal(error.code, 'DEVICE_NOT_FOUND');
|
|
assert.match(String(error.details?.hint ?? ''), /Did you mean --serial emulator-5580\?/);
|
|
});
|
|
|
|
test('an id passed to --device on a platform with no identity flag stays hint-free', async () => {
|
|
// `--udid` resolves only Apple devices and `--serial` only serial-addressable ones; a web or
|
|
// linux device's id has no flag that could take it, so a hint naming one would be unrunnable.
|
|
const WEB: DeviceInfo = {
|
|
platform: 'web',
|
|
target: 'desktop',
|
|
id: 'agent-browser-chrome',
|
|
name: 'Agent Browser Chrome',
|
|
kind: 'device',
|
|
booted: true,
|
|
};
|
|
const error = await resolveError([WEB], { deviceName: 'agent-browser-chrome' });
|
|
assert.equal(error.code, 'DEVICE_NOT_FOUND');
|
|
assert.equal(error.details?.hint, undefined);
|
|
});
|
|
|
|
test('an unknown --device value gets the generic hint, even when it looks like a UDID', async () => {
|
|
// The hint is grounded in observed identity: only a candidate's actual id earns the
|
|
// wrong-flag answer. A UDID-shaped value that no listed device carries stays a plain
|
|
// unknown name — guessing from shape would misclassify UUID-named devices and miss
|
|
// every identity syntax the shape guess does not cover.
|
|
for (const deviceName of ['204BFFD9-9644-4830-B2C1-1B946597A07C', 'iPhone 99']) {
|
|
const error = await resolveError([APPLE], { platform: 'ios', deviceName });
|
|
assert.equal(error.code, 'DEVICE_NOT_FOUND');
|
|
assert.equal(error.details?.hint, undefined);
|
|
}
|
|
});
|
|
|
|
async function resolveError(
|
|
devices: DeviceInfo[],
|
|
selector: Parameters<typeof resolveDevice>[1],
|
|
): Promise<AppError> {
|
|
try {
|
|
await resolveDevice(devices, selector);
|
|
} catch (error) {
|
|
assert.ok(error instanceof AppError, `expected AppError, got ${String(error)}`);
|
|
return error;
|
|
}
|
|
throw new assert.AssertionError({ message: 'expected resolution to fail' });
|
|
}
|