diff --git a/src/http/app.test.ts b/src/http/app.test.ts index c0fe5e91..71e97362 100644 --- a/src/http/app.test.ts +++ b/src/http/app.test.ts @@ -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 }); diff --git a/src/http/lease-declarations.ts b/src/http/lease-declarations.ts index 138a7f98..4698ebc8 100644 --- a/src/http/lease-declarations.ts +++ b/src/http/lease-declarations.ts @@ -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(), diff --git a/src/http/tracker.ts b/src/http/tracker.ts index ffdd76a0..27627293 100644 --- a/src/http/tracker.ts +++ b/src/http/tracker.ts @@ -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
`); absent while the device has none (not ready, or shut down). + readonly address?: string; readonly deviceId: string; readonly createdAt: string; readonly expiresAt: string; @@ -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(), @@ -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; diff --git a/website/src/content/docs/docs/using-devices/remote-agents.mdx b/website/src/content/docs/docs/using-devices/remote-agents.mdx index aadf8007..6d189584 100644 --- a/website/src/content/docs/docs/using-devices/remote-agents.mdx +++ b/website/src/content/docs/docs/using-devices/remote-agents.mdx @@ -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. @@ -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 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` 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 ` added, and `adb` on a physical Android phone gets the phone's serial. - **Paths are on the worker.** `{"tool":"simctl","args":["install","","/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.