Files
software-mansion__argent/packages/tool-server/test/list-devices.test.ts
Mateusz Aliyev 6e9e45c9f0 feat: attach to devices offered by external providers (#735)
This pull request lets Argent drive a simulator or emulator another
process is already running, attaching to its simulator-server rather
than spawning a second one.

A provider writes a JSON descriptor to `~/.argent/providers/` listing
the devices it offers and the mechanisms Argent may use on each. They
appear in `list-devices` with an `ext:` id and work with the existing
tools. The file is re-read on every call, so a withdrawal or a narrowed
grant applies immediately.

`ios.additionalDeviceSets` (#600) already makes such a simulator
reachable by UDID. What changes is ownership. `boot-device` and
`stop-simulator-server` refuse these devices, anything the provider did
not grant is refused with a message naming it and Argent uses only the
endpoints its own simulator-server build serves. A grant binds to the
device rather than to one of its names, so the real udid or serial is
gated exactly like the `ext:` id. Android emulators are covered too and
a device visible to both a provider and `adb`/`simctl` is listed once.

The contract ships as `schemas/device-provider-v1.json`, validated by
`argent providers check`, and is documented in
`docs/reference/device-providers.mdx`. Includes unit tests and an e2e
phase where the harness acts as its own provider.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for discovering and connecting to externally managed
devices.
* Added `argent providers` commands for listing, validating, publishing,
withdrawing, and pruning providers.
* Added provider-aware simulator, debugger, profiler, native tools, and
device listings.
* Added capability controls, endpoint validation, revocation handling,
and provider-specific diagnostics.
  * Added automatic CLI discovery for provider integrations.
* **Bug Fixes**
  * Improved service recovery and paused-runtime error reporting.
  * Improved simulator keyboard and paste command reliability.
* **Documentation**
* Documented provider descriptors, capabilities, lifecycle rules, and
CLI usage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-04 22:02:57 +02:00

751 lines
30 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
const execFileMock = vi.fn();
// Production spawns `ps` by absolute path (PS_BIN); match on basename so the mock
// fires regardless.
const isPs = (cmd: string): boolean => cmd === "ps" || cmd.endsWith("/ps");
vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
const { EventEmitter } = await import("node:events");
return {
...actual,
execFile: (
cmd: string,
args: readonly string[],
opts: unknown,
cb?: (err: Error | null, out: { stdout: string; stderr: string }) => void
) => {
const callback = typeof opts === "function" ? opts : cb!;
const options = typeof opts === "function" ? undefined : opts;
const result = execFileMock(cmd, args, options);
if (result instanceof Error) callback(result, { stdout: "", stderr: "" });
else callback(null, result ?? { stdout: "", stderr: "" });
},
// `runVega` now uses spawn (for `detached`), so route the vega CLI calls
// through the same execFileMock and surface the result as a fake child that
// emits stdout then `close`. An Error result emits `error` (mirrors a failed /
// timed-out CLI). Only the vega path spawns here — adb/emulator/ps stay on
// execFile above.
spawn: (cmd: string, args: readonly string[]) => {
const result = execFileMock(cmd, args, undefined);
const mkStream = () => {
const s = new EventEmitter() as any;
s.setEncoding = () => {};
s.destroy = () => {};
return s;
};
const child = new EventEmitter() as any;
// 0 (not a real OS pid) on purpose: this mock only drives the happy path
// (`close`, code 0), so reapVegaGroup never runs — but if a future timeout-path
// test were ever routed through it, `pid <= 1` makes the reap a no-op rather than
// letting `process.kill(-pid)` signal a real process group.
child.pid = 0;
child.stdout = mkStream();
child.stderr = mkStream();
child.kill = () => true;
setImmediate(() => {
if (result instanceof Error) {
child.emit("error", result);
return;
}
const out = (result ?? { stdout: "", stderr: "" }) as { stdout?: string; stderr?: string };
if (out.stdout) child.stdout.emit("data", out.stdout);
if (out.stderr) child.stderr.emit("data", out.stderr);
child.emit("close", 0, null);
});
return child;
},
};
});
// `runAdb` and `listAvds` resolve adb / emulator to an absolute path before
// spawning, so a bare `cmd === "adb" / "emulator"` matcher would never fire
// on real hosts. Stub the resolver to return the bare name so existing test
// mocks keep working regardless of host SDK install state.
vi.mock("../src/utils/android-binary", () => ({
resolveAndroidBinary: vi.fn(async (name: "adb" | "emulator") => name),
__resetAndroidBinaryCacheForTesting: () => {},
}));
// Chromium discovery probes real TCP ports (9222 plus any persisted by a
// previous tool-server on this machine). A developer actually running an
// Chromium app would leak it into this test's device list — stub discovery
// so the result only contains what the simctl / adb mocks define.
vi.mock("../src/utils/chromium-discovery", async () => {
const actual = await vi.importActual<typeof import("../src/utils/chromium-discovery")>(
"../src/utils/chromium-discovery"
);
return { ...actual, discoverChromiumDevices: vi.fn(async () => []) };
});
// VVD image enumeration hits the real filesystem (~/vega SDK); stub it so `vvds`
// is deterministic. Defaults to none; the dedicated test overrides per-call.
vi.mock("../src/utils/vega-sdk", () => ({ listVvdImages: vi.fn(async () => []) }));
// Physical-device discovery shells out to `xcrun devicectl` with a JSON output
// file; mock the module seam and feed rows directly. Defaults to none; the
// physical-device test overrides per-call.
vi.mock("../src/utils/ios-device/devicectl", async () => {
const actual = await vi.importActual<typeof import("../src/utils/ios-device/devicectl")>(
"../src/utils/ios-device/devicectl"
);
return {
...actual,
listIosPhysicalDevices: vi.fn(async () => []),
};
});
import * as fs from "node:fs";
import * as http from "node:http";
import * as os from "node:os";
import * as path from "node:path";
import type { AddressInfo } from "node:net";
import { listDevicesTool } from "../src/tools/devices/list-devices";
import { listIosPhysicalDevices } from "../src/utils/ios-device/devicectl";
import {
__resetExternalDeviceCacheForTesting,
__resetProviderWarningsForTesting,
} from "../src/utils/external-devices";
import { __resetVegaBinaryCacheForTests } from "../src/utils/vega-cli";
import { listVvdImages } from "../src/utils/vega-sdk";
function simctlJson(): string {
return JSON.stringify({
devices: {
"com.apple.CoreSimulator.SimRuntime.iOS-18-2": [
{
udid: "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA",
name: "iPhone 16",
state: "Booted",
deviceTypeIdentifier: "com.apple.CoreSimulator.SimDeviceType.iPhone-16",
isAvailable: true,
},
{
udid: "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB",
name: "iPad Pro",
state: "Shutdown",
deviceTypeIdentifier: "com.apple.CoreSimulator.SimDeviceType.iPad-Pro",
isAvailable: true,
},
{
udid: "CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC",
name: "iPhone 16 (unavailable)",
state: "Shutdown",
deviceTypeIdentifier: "com.apple.CoreSimulator.SimDeviceType.iPhone-16",
isAvailable: false,
},
],
"com.apple.CoreSimulator.SimRuntime.tvOS-17-5": [
{
udid: "DDDDDDDD-DDDD-DDDD-DDDD-DDDDDDDDDDDD",
name: "Apple TV",
state: "Shutdown",
deviceTypeIdentifier: "com.apple.CoreSimulator.SimDeviceType.Apple-TV",
isAvailable: true,
},
],
},
});
}
beforeEach(() => {
execFileMock.mockReset();
});
describe("list-devices", () => {
it("merges iOS simulators and Android devices into a single tagged array", async () => {
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "xcrun" && args[0] === "simctl" && args[1] === "list") {
return { stdout: simctlJson(), stderr: "" };
}
if (cmd === "adb" && args[0] === "devices") {
return { stdout: "List of devices attached\nemulator-5554\tdevice\n", stderr: "" };
}
if (cmd === "adb" && args[0] === "-s" && args[2] === "shell") {
const shellCmd = args[3] ?? "";
if (shellCmd.includes("ro.product.model")) return { stdout: "Pixel_3a\n", stderr: "" };
if (shellCmd.includes("ro.build.version.sdk")) return { stdout: "34\n", stderr: "" };
if (shellCmd.includes("ro.kernel.qemu.avd_name"))
return { stdout: "Pixel_3a_API_34\n", stderr: "" };
}
if (cmd === "emulator" && args[0] === "-list-avds") {
return { stdout: "Pixel_3a_API_34\nPixel_7_API_34\n", stderr: "" };
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
// Every device has a `platform` discriminator; there is no separate iOS/Android
// list the caller has to merge.
for (const d of result.devices) {
expect(d.platform === "ios" || d.platform === "android").toBe(true);
}
const ios = result.devices.filter((d) => d.platform === "ios") as Array<{
platform: "ios";
udid: string;
name: string;
state: string;
runtimeKind?: "mobile" | "tv";
}>;
// Unavailable simulators are filtered out. tvOS simulators are now included
// (tagged runtimeKind "tv") so Apple TV targets are discoverable; iOS
// simulators are tagged "mobile".
expect(ios.map((d) => d.name).sort()).toEqual(["Apple TV", "iPad Pro", "iPhone 16"]);
// Booted iOS devices come before shut-down ones.
expect(ios[0]!.state).toBe("Booted");
expect(ios[0]!.name).toBe("iPhone 16");
// The tvOS simulator carries the "tv" runtime kind; iOS ones are "mobile".
expect(ios.find((d) => d.name === "Apple TV")?.runtimeKind).toBe("tv");
expect(ios.find((d) => d.name === "iPhone 16")?.runtimeKind).toBe("mobile");
const android = result.devices.filter((d) => d.platform === "android") as Array<{
platform: "android";
serial: string;
sdkLevel: number | null;
avdName: string | null;
isEmulator: boolean;
}>;
expect(android).toHaveLength(1);
expect(android[0]).toMatchObject({
serial: "emulator-5554",
sdkLevel: 34,
avdName: "Pixel_3a_API_34",
isEmulator: true,
});
// AVDs list comes from `emulator -list-avds`.
expect(result.avds).toEqual([{ name: "Pixel_3a_API_34" }, { name: "Pixel_7_API_34" }]);
});
/**
* The common case, which the suite-wide setup already models. No provider
* registered, so the result must not carry a word about them.
*/
it("says nothing about external devices when no provider is registered", async () => {
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "xcrun" && args[0] === "simctl") {
return { stderr: "", stdout: simctlJson() };
}
if (cmd === "adb" && args[0] === "devices") {
return { stderr: "", stdout: "List of devices attached\n" };
}
return { stderr: "", stdout: "" };
});
const result = await listDevicesTool.execute!({}, {});
expect(result.devices.length).toBeGreaterThan(0);
expect(result).not.toHaveProperty("hint");
});
it("readAvdName prefers the modern avd_name prop over the legacy one (now probed concurrently)", async () => {
// The two getprops run in parallel (so a wedged device costs 5s, not 10s), but
// precedence must be unchanged: `ro.boot.qemu.avd_name` (modern, emulator 30+)
// wins over `ro.kernel.qemu.avd_name` (legacy) when BOTH answer. The first test
// above covers the legacy-only fallback; this pins the both-present ordering so the
// parallelization can't silently flip it.
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "adb" && args[0] === "devices") {
return { stdout: "List of devices attached\nemulator-5554\tdevice\n", stderr: "" };
}
if (cmd === "adb" && args[0] === "-s" && args[2] === "shell") {
const shellCmd = args[3] ?? "";
if (shellCmd.includes("ro.boot.qemu.avd_name"))
return { stdout: "ModernName\n", stderr: "" };
if (shellCmd.includes("ro.kernel.qemu.avd_name"))
return { stdout: "LegacyName\n", stderr: "" };
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
const android = result.devices.filter((d) => d.platform === "android") as Array<{
avdName: string | null;
}>;
expect(android).toHaveLength(1);
expect(android[0]!.avdName).toBe("ModernName");
});
it("silently omits iOS when xcrun is unavailable — other platforms still returned", async () => {
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "xcrun") {
return new Error("xcrun: error: invalid active developer path");
}
if (cmd === "adb" && args[0] === "devices") {
return { stdout: "List of devices attached\nemulator-5554\tdevice\n", stderr: "" };
}
if (cmd === "adb" && args[0] === "-s" && args[2] === "shell") {
return { stdout: "", stderr: "" };
}
if (cmd === "emulator") {
return { stdout: "Pixel_3a_API_34\n", stderr: "" };
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
expect(result.devices.filter((d) => d.platform === "ios")).toHaveLength(0);
expect(result.devices.filter((d) => d.platform === "android")).toHaveLength(1);
expect(result.avds.length).toBeGreaterThan(0);
});
it("silently omits Android when adb is unavailable — iOS still returned", async () => {
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "xcrun" && args[0] === "simctl") {
return { stdout: simctlJson(), stderr: "" };
}
if (cmd === "adb") {
return new Error("adb: command not found");
}
if (cmd === "emulator") {
return new Error("emulator: command not found");
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
expect(result.devices.filter((d) => d.platform === "android")).toHaveLength(0);
expect(result.devices.filter((d) => d.platform === "ios").length).toBeGreaterThan(0);
expect(result.avds).toEqual([]);
});
// A running VVD surfaces on adb as `emulator-XXXX`; the dedup reads its console
// port from the `vega-virtual-device` process (mocked via the `ps` branch).
function mockVegaVvd(cmd: string, args: string[]): { stdout: string; stderr: string } | null {
if (cmd === "/bin/sh" && args[0] === "-c" && args[1]?.includes("command -v vega")) {
return { stdout: "/usr/bin/vega\n", stderr: "" };
}
if (cmd === "/bin/sh" && args[0] === "-c") return { stdout: "", stderr: "" }; // kepler probe
if (cmd.endsWith("vega") && args[0] === "device" && args[1] === "list") {
return {
stdout:
"Found the following device:\nVirtualDevice : tv - aarch64 - OS - amazon-4a27df03c9777152\n",
stderr: "",
};
}
if (cmd.endsWith("vega") && args[0] === "virtual-device" && args[1] === "status") {
return { stdout: JSON.stringify({ running: true }), stderr: "" };
}
if (cmd.endsWith("vega") && args[0] === "device" && args[1] === "info") {
return { stdout: JSON.stringify({ product: "vvrp_aarch64", simulated: true }), stderr: "" };
}
return null;
}
// A `ps` listing with a `vega-virtual-device` process per console port.
function psWithVvds(...consolePorts: number[]): { stdout: string; stderr: string } {
const lines = ["/sbin/launchd", "/usr/libexec/secd"];
for (const p of consolePorts) {
lines.push(
"/Users/me/vega/sdk/vega-sdk/main/0.22.6759/vvd/images/tv/vmtools/agent/qemu/" +
`darwin-aarch64/vega-virtual-device -avd-arch arm64 -ports ${p},${p + 1} ` +
`-qemu -qmp unix:/tmp/qmp-socket-${p}.sock,server,nowait`
);
}
return { stdout: lines.join("\n") + "\n", stderr: "" };
}
it("de-duplicates a running VVD that also auto-registers on adb (shows only as vega)", async () => {
__resetVegaBinaryCacheForTests();
execFileMock.mockImplementation((cmd: string, args: string[]) => {
const vega = mockVegaVvd(cmd, args);
if (vega) return vega;
if (isPs(cmd)) return psWithVvds(5554); // VVD on console port 5554
if (cmd === "xcrun") return { stdout: simctlJson(), stderr: "" };
if (cmd === "adb" && args[0] === "devices") {
return { stdout: "List of devices attached\nemulator-5554\tdevice\n", stderr: "" };
}
if (cmd === "adb" && args[0] === "-s" && args[2] === "shell") {
return { stdout: "", stderr: "" }; // enrichment getprops — irrelevant to dedup
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
const android = result.devices.filter((d) => d.platform === "android");
const vega = result.devices.filter((d) => d.platform === "vega");
expect(vega).toHaveLength(1);
expect((vega[0] as { serial: string }).serial).toBe("amazon-4a27df03c9777152");
// The emulator-5554 shadow row is dropped — the VVD appears exactly once.
expect(android).toHaveLength(0);
});
it("de-duplicates a VVD reached via `adb connect` 127.0.0.1:5555 (edge 1)", async () => {
__resetVegaBinaryCacheForTests();
execFileMock.mockImplementation((cmd: string, args: string[]) => {
const vega = mockVegaVvd(cmd, args);
if (vega) return vega;
if (isPs(cmd)) return psWithVvds(5554);
if (cmd === "xcrun") return { stdout: simctlJson(), stderr: "" };
if (cmd === "adb" && args[0] === "devices") {
// adb port = console + 1, so the `adb connect` serial is 127.0.0.1:5555.
return { stdout: "List of devices attached\n127.0.0.1:5555\tdevice\n", stderr: "" };
}
if (cmd === "adb" && args[0] === "-s" && args[2] === "shell") {
return { stdout: "", stderr: "" };
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
expect(result.devices.filter((d) => d.platform === "android")).toHaveLength(0);
expect(result.devices.filter((d) => d.platform === "vega")).toHaveLength(1);
});
it("keeps a real Android emulator running alongside the VVD (edge 2 / distinguishes QEMU)", async () => {
// Real emulator on 5554, VVD on 5556 (from its process). Only the VVD's row is
// dropped; a co-running emulator / QEMU is never mistaken for the VVD.
__resetVegaBinaryCacheForTests();
execFileMock.mockImplementation((cmd: string, args: string[]) => {
const vega = mockVegaVvd(cmd, args);
if (vega) return vega;
if (isPs(cmd)) return psWithVvds(5556); // VVD on console port 5556
if (cmd === "xcrun") return { stdout: simctlJson(), stderr: "" };
if (cmd === "adb" && args[0] === "devices") {
return {
stdout: "List of devices attached\nemulator-5554\tdevice\nemulator-5556\tdevice\n",
stderr: "",
};
}
if (cmd === "adb" && args[0] === "-s" && args[2] === "shell") {
const shellCmd = args[3] ?? "";
if (shellCmd.includes("ro.product.model")) return { stdout: "Pixel_7\n", stderr: "" };
if (shellCmd.includes("ro.build.version.sdk")) return { stdout: "34\n", stderr: "" };
return { stdout: "", stderr: "" };
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
const android = result.devices.filter((d) => d.platform === "android");
const vega = result.devices.filter((d) => d.platform === "vega");
expect(vega).toHaveLength(1);
// The shadow (emulator-5556) is dropped; the real emulator stays.
expect(android).toHaveLength(1);
expect((android[0] as { serial: string }).serial).toBe("emulator-5554");
});
it("does not filter an emulator when no VVD process is running (e.g. mid-boot)", async () => {
// Vega SDK present but no `vega-virtual-device` process → no running VVD. A
// standalone emulator must NOT be mistaken for a shadow.
__resetVegaBinaryCacheForTests();
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "/bin/sh" && args[0] === "-c" && args[1]?.includes("command -v vega")) {
return { stdout: "/usr/bin/vega\n", stderr: "" };
}
if (cmd === "/bin/sh" && args[0] === "-c") return { stdout: "", stderr: "" };
if (cmd.endsWith("vega") && args[0] === "device" && args[1] === "list") {
return { stdout: "Found the following device:\n", stderr: "" }; // no devices
}
if (isPs(cmd)) return { stdout: "/sbin/launchd\n", stderr: "" }; // no VVD process
if (cmd === "xcrun") return { stdout: simctlJson(), stderr: "" };
if (cmd === "adb" && args[0] === "devices") {
return { stdout: "List of devices attached\nemulator-5554\tdevice\n", stderr: "" };
}
if (cmd === "adb" && args[0] === "-s" && args[2] === "shell") {
return { stdout: "", stderr: "" };
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
const android = result.devices.filter((d) => d.platform === "android");
const vega = result.devices.filter((d) => d.platform === "vega");
expect(vega).toHaveLength(0);
expect(android).toHaveLength(1);
expect((android[0] as { serial: string }).serial).toBe("emulator-5554");
});
it("does not re-list a running VVD as a phantom stopped image when its profile is absent", async () => {
// `vega device info` omits `profile`, but an image named "tv" is installed.
// The running VVD must dedup against that image (single installed image) so it
// shows up once as running, not also as a stopped "tv".
__resetVegaBinaryCacheForTests();
vi.mocked(listVvdImages).mockResolvedValueOnce([{ name: "tv", path: "/sdk/vvd/images/tv" }]);
execFileMock.mockImplementation((cmd: string, args: string[]) => {
const vega = mockVegaVvd(cmd, args);
if (vega) return vega;
if (isPs(cmd)) return psWithVvds(5554);
if (cmd === "xcrun") return { stdout: simctlJson(), stderr: "" };
if (cmd === "adb" && args[0] === "devices") {
return { stdout: "List of devices attached\nemulator-5554\tdevice\n", stderr: "" };
}
if (cmd === "adb" && args[0] === "-s" && args[2] === "shell") {
return { stdout: "", stderr: "" };
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
const vega = result.devices.filter((d) => d.platform === "vega") as Array<{
state: string;
serial: string | null;
vvdImage: string | null;
}>;
expect(vega).toHaveLength(1);
expect(vega[0]).toMatchObject({ state: "running", vvdImage: "tv" });
});
it("lists an installed-but-stopped VVD under devices[] (state stopped, no serial)", async () => {
__resetVegaBinaryCacheForTests();
vi.mocked(listVvdImages).mockResolvedValueOnce([{ name: "tv", path: "/sdk/vvd/images/tv" }]);
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "/bin/sh" && args[0] === "-c" && args[1]?.includes("command -v vega")) {
return { stdout: "/usr/bin/vega\n", stderr: "" };
}
if (cmd === "/bin/sh" && args[0] === "-c") return { stdout: "", stderr: "" };
// No device connected and not running → the VVD is stopped.
if (cmd.endsWith("vega") && args[0] === "device" && args[1] === "list") {
return { stdout: "Found the following device:\n", stderr: "" };
}
if (cmd === "xcrun") return { stdout: simctlJson(), stderr: "" };
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
const vega = result.devices.filter((d) => d.platform === "vega") as Array<{
kind: string;
state: string;
serial: string | null;
vvdImage: string | null;
}>;
expect(vega).toHaveLength(1);
expect(vega[0]).toMatchObject({
kind: "vvd",
state: "stopped",
serial: null,
vvdImage: "tv",
});
});
it("derives physical iOS state from transportType alone and sorts connected first", async () => {
// Live-captured shapes: a cabled phone is "wired", a Wi-Fi-paired phone
// last seen days ago lists as "disconnected"/"localNetwork", and a freshly
// unplugged phone keeps tunnelState "connected" while the tunnel migrates
// to Wi-Fi (hardware-verified, devicectl 518.x). Only the wired one is reachable
// over usbmux, so only it may present as "connected".
vi.mocked(listIosPhysicalDevices).mockResolvedValueOnce([
{
udid: "00008120-000A44443333801E",
name: "Office iPhone",
model: "iPhone 14",
osVersion: "18.5",
developerModeEnabled: true,
pairingState: "paired",
transportType: "localNetwork",
tunnelState: "disconnected",
},
{
udid: "00008110-000978540290401E",
name: "Desk iPhone",
model: "iPhone 13",
osVersion: "26.0",
developerModeEnabled: true,
pairingState: "paired",
transportType: "wired",
tunnelState: "connected",
},
{
udid: "00008120-000B55554444901E",
name: "Unplugged iPhone",
model: "iPhone 15",
osVersion: "27.0",
developerModeEnabled: true,
pairingState: "paired",
transportType: "localNetwork",
tunnelState: "connected",
},
]);
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "xcrun" && args[0] === "simctl" && args[1] === "list") {
return { stdout: simctlJson(), stderr: "" };
}
return { stdout: "", stderr: "" };
});
const result = await listDevicesTool.execute!({}, {});
const byName = (name: string) =>
result.devices.findIndex((d) => "name" in d && d.name === name);
// Devices order doubles as the sort pin: the connected phone (rank 0)
// precedes the paired one (rank 1) in this filtered projection.
const physical = result.devices.filter((d) => "kind" in d && d.kind === "device") as Array<{
name: string;
state: string;
pairingState: string | null;
tunnelState: string | null;
}>;
expect(physical.map((d) => [d.name, d.state])).toEqual([
["Desk iPhone", "connected"],
["Office iPhone", "paired"],
["Unplugged iPhone", "paired"],
]);
// pairingState/tunnelState ride along so callers can see WHY a row is paired.
expect(physical[1]).toMatchObject({ pairingState: "paired", tunnelState: "disconnected" });
// The Wi-Fi-migrated tunnel reads "connected" yet usbmux cannot reach it.
expect(physical[2]).toMatchObject({ state: "paired", tunnelState: "connected" });
// The paired phone sinks below every usable device, booted simulators
// included; flow auto-bind keys on state "connected" and skips it.
expect(byName("Desk iPhone")).toBeLessThan(byName("Office iPhone"));
expect(byName("iPhone 16")).toBeLessThan(byName("Office iPhone"));
});
});
/**
* A provider's device can reach this tool twice: once as an `ext:` entry, and
* once through the platform's own discovery. `adb devices` always sees a
* provider's emulator, and `simctl` sees its simulators as soon as the
* provider's device set is listed in `ios.additionalDeviceSets`. Only the
* external entry carries the attribution and the capability grants, and
* driving the bare id would spawn a second simulator-server against a device
* already in use, so the plain row is the one that goes.
*/
describe("list-devices — external provider shadows", () => {
const CLAIMED_UDID = "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA";
let temporaryDirectory: string;
let simulatorServer: http.Server;
let descriptorPath: string;
beforeEach(async () => {
temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "argent-list-providers-"));
/**
* Discovery probes `apiUrl` for liveness, so the device needs something
* actually listening or it is dropped before dedup is even reached.
*/
simulatorServer = http.createServer((_request, response) => {
response.statusCode = 404;
response.end();
});
await new Promise<void>((resolve) => simulatorServer.listen(0, "127.0.0.1", resolve));
const { port } = simulatorServer.address() as AddressInfo;
descriptorPath = path.join(temporaryDirectory, "acme.json");
fs.writeFileSync(
descriptorPath,
JSON.stringify({
devices: [
{
capabilities: ["ax-service", "simctl", "simulator-server"],
deviceSet: "/tmp/acme/Devices/iOS",
kind: "simulator",
name: "iPhone 16",
nativeId: CLAIMED_UDID,
platform: "ios",
simulatorServer: {
apiUrl: `http://127.0.0.1:${port}`,
streamUrl: `http://127.0.0.1:${port}/stream.mjpeg`,
},
state: "Booted",
},
],
id: "acme-3f2a9c",
name: "Acme IDE",
schemaVersion: 1,
})
);
/** Opt back into discovery, which the suite-wide setup switches off. */
delete process.env.ARGENT_DISABLE_DEVICE_PROVIDERS;
process.env.ARGENT_DEVICE_PROVIDERS = descriptorPath;
__resetExternalDeviceCacheForTesting();
__resetProviderWarningsForTesting();
});
afterEach(async () => {
delete process.env.ARGENT_DEVICE_PROVIDERS;
process.env.ARGENT_DISABLE_DEVICE_PROVIDERS = "1";
await new Promise<void>((resolve) => simulatorServer.close(() => resolve()));
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
});
it("lists a claimed simulator once, as the external entry", async () => {
execFileMock.mockImplementation((cmd: string, args: string[]) => {
/**
* `simctl` sees the provider's simulator because its device set is
* configured.
*/
if (cmd === "xcrun" && args[0] === "simctl") {
return { stderr: "", stdout: simctlJson() };
}
if (cmd === "adb" && args[0] === "devices") {
return { stderr: "", stdout: "List of devices attached\n" };
}
return { stderr: "", stdout: "" };
});
const result = await listDevicesTool.execute!({}, {});
const claimed = result.devices.filter(
(device) => "udid" in device && (device.udid as string).includes(CLAIMED_UDID)
);
expect(claimed).toHaveLength(1);
expect(claimed[0]).toMatchObject({
external: true,
udid: `ext:acme-3f2a9c:${CLAIMED_UDID}`,
});
});
it("leaves an unclaimed simulator in the plain listing", async () => {
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "xcrun" && args[0] === "simctl") {
return { stderr: "", stdout: simctlJson() };
}
if (cmd === "adb" && args[0] === "devices") {
return { stderr: "", stdout: "List of devices attached\n" };
}
return { stderr: "", stdout: "" };
});
const result = await listDevicesTool.execute!({}, {});
const iPad = result.devices.filter(
(device) => "udid" in device && device.udid === "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB"
);
expect(iPad).toHaveLength(1);
expect(iPad[0]).not.toHaveProperty("external");
});
/** The rules ride on the result, so they cost nothing until one is listed. */
it("explains the provider rules in the result once an external device is listed", async () => {
execFileMock.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "xcrun" && args[0] === "simctl") {
return { stderr: "", stdout: simctlJson() };
}
if (cmd === "adb" && args[0] === "devices") {
return { stderr: "", stdout: "List of devices attached\n" };
}
return { stderr: "", stdout: "" };
});
const result = await listDevicesTool.execute!({}, {});
expect(result.hint).toBeDefined();
/** The two rules that differ from a device argent booted itself. */
expect(result.hint).toContain("boot-device");
expect(result.hint).toContain("provider.workspace.path");
});
});