Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/http/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,34 @@ describe("lease routes", () => {
expect(lease).not.toHaveProperty("mode");
});

it("names an Android emulator's adb serial as address, beside its AVD name as udid, on the lease object the POST, the request poll and GET /v1/leases/:id answer", async () => {
const emulator = {
address: "emulator-5554",
driverDeviceId: "simlock_3f9a2c1e",
id: "dev_and",
spec: { model: "Pixel 8", osVersion: "35", physical: false, platform: "android" },
} as const;
const { app, dispatcher, registry } = buildHarness();
const posted = postLeaseRequest(app, { device: "Pixel 8", platform: "android" });
const call = await waitForDispatch(dispatcher, "lease.request");
call.resolve(makeGrant({ device: emulator, lease: { deviceId: "dev_and", id: "lse_and" } }));
const created = (await (await posted).json()) as { request: { id: string; lease: unknown } };
registry.devices = [makeDevice(emulator)];
registry.leases = [makeLease({ deviceId: "dev_and", id: "lse_and", ownerId: "tok_agent" })];

const polled = await app.request(`/v1/lease-requests/${created.request.id}`, {
headers: agentAuth,
});
const read = await app.request("/v1/leases/lse_and", { headers: agentAuth });

const named = { address: "emulator-5554", udid: "simlock_3f9a2c1e" };
expect({
get: ((await read.json()) as { lease: unknown }).lease,
poll: ((await polled.json()) as { request: { lease: unknown } }).request.lease,
post: created.request.lease,
}).toMatchObject({ get: named, poll: named, post: named });
});

it("404s an unknown lease id", async () => {
const { app } = buildHarness();
const response = await app.request("/v1/leases/lse-missing", { headers: agentAuth });
Expand Down
1 change: 1 addition & 0 deletions src/http/lease-declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { declareRoute, firstValue, sseMessage } from "./routes.js";
const leaseParamsSchema = z.object({ id: z.string() });

const leasePayloadSchema = z.object({
address: z.string().optional(),
createdAt: z.string(),
dataPlane: z.null(),
device: z.string(),
Expand Down
4 changes: 4 additions & 0 deletions src/http/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface LeasePayload {
readonly device: string;
readonly os: string;
readonly udid: string;
// why: the serial the platform's tools take (`adb -s <address>`); absent while the device has none (not ready, or shut down).
readonly address?: string;
readonly deviceId: string;
readonly createdAt: string;
readonly expiresAt: string;
Expand Down Expand Up @@ -69,6 +71,7 @@ export function buildLeasePayload(
device: device.spec.model,
os: device.spec.osVersion,
udid: device.driverDeviceId,
...(device.address === undefined ? {} : { address: device.address }),
deviceId: device.id,
createdAt: new Date(lease.grantedAt).toISOString(),
expiresAt: new Date(lease.ttlDeadline).toISOString(),
Expand Down Expand Up @@ -376,6 +379,7 @@ type HttpLeaseProgress =
export interface HttpLeaseDevice {
readonly id: string;
readonly driverDeviceId: string;
readonly address?: string | undefined;
readonly spec: {
readonly platform: string;
readonly model: string;
Expand Down
4 changes: 2 additions & 2 deletions website/src/content/docs/docs/using-devices/remote-agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export SIMLOCK_TOKEN=slk_d2FybS1wb29sLWFnZW50LXRva2VuLWV4YW1wbGUtMDM
While it waits, `state` is `queued` (with `queuePosition`), `downloading`, `reclaiming`, `provisioning` or `booting` (with `etaSeconds`). A granted request carries the lease:

```json
{"request":{"createdAt":"2026-10-21T09:12:00.000Z","id":"req_7d1a0c4e-2b9f-4e63-8a15-c3f0d6b8e241","state":"granted","lease":{"id":"lse_3b9d6c1e-7f2a-4c55-9e0b-8a41d2f6c7e3","requestId":"req_7d1a0c4e-2b9f-4e63-8a15-c3f0d6b8e241","platform":"ios","device":"iPhone 16","os":"18.6","udid":"6F1D2A8C-3B4E-4F7A-9C2D-8E5B1A7F3C90","deviceId":"dev_8c1f2e7a-4b6d-4e0f-9a3c-5d2b7e1f0a64","createdAt":"2026-10-21T09:12:41.000Z","expiresAt":"2026-10-21T09:42:41.000Z","ttlMs":1800000,"dataPlane":null,"mode":"full"}}}
{"request":{"createdAt":"2026-10-21T09:12:00.000Z","id":"req_7d1a0c4e-2b9f-4e63-8a15-c3f0d6b8e241","state":"granted","lease":{"id":"lse_3b9d6c1e-7f2a-4c55-9e0b-8a41d2f6c7e3","requestId":"req_7d1a0c4e-2b9f-4e63-8a15-c3f0d6b8e241","platform":"ios","device":"iPhone 16","os":"18.6","udid":"6F1D2A8C-3B4E-4F7A-9C2D-8E5B1A7F3C90","address":"6F1D2A8C-3B4E-4F7A-9C2D-8E5B1A7F3C90","deviceId":"dev_8c1f2e7a-4b6d-4e0f-9a3c-5d2b7e1f0a64","createdAt":"2026-10-21T09:12:41.000Z","expiresAt":"2026-10-21T09:42:41.000Z","ttlMs":1800000,"dataPlane":null,"mode":"full"}}}
```

A failed request carries `error` with a `code` and `message`, such as `QUEUE_TIMEOUT` when `timeoutMs` ran out.
Expand Down Expand Up @@ -150,7 +150,7 @@ export SIMLOCK_TOKEN=slk_d2FybS1wb29sLWFnZW50LXRva2VuLWV4YW1wbGUtMDM
## Running commands with exec

- **Name your device in the arguments.** Use the lease's `udid` for `simctl`. Other agents' devices are on the same worker, so never use `booted`.
- **On Android, find the emulator's serial first.** The lease's `udid` is the emulator's AVD name (`simlock_…`), not its adb serial. Run `adb devices`, then `adb -s <serial> emu avd name` on each emulator until the name matches, and use that serial with `-s`.
- **On Android, use the lease's `address` as the adb serial.** The lease's `udid` is the emulator's AVD name (`simlock_…`), not its adb serial. Run `adb -s <address> …`. `address` is absent while the device has none, for example before it is ready or after it shuts down.
- **On a physical device, Simlock names the device for you.** `devicectl` gets `--device <UDID>` added, and `adb` on a physical Android phone gets the phone's serial.
- **Paths are on the worker.** `{"tool":"simctl","args":["install","<udid>","/builds/MyApp.app"]}` needs `/builds/MyApp.app` to exist on the worker. The API has no way to upload a file; get it there another way, or use [agent-device](/simlock/docs/using-devices/agent-device), which uploads apps for you.
- **No terminal.** `stdin` in the body is sent to the command once. Interactive commands, such as a bare `adb shell`, are refused.
Expand Down
Loading