Files
callstack__agent-device/packages/kernel/src/device-selector-flags.test.ts
Nicolas Bataille 23c796c52b fix(cli): name --udid when a device identity is passed to --device (#2065)
* 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>
2026-08-27 11:19:29 +02:00

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' });
}