diff --git a/e2e/console/fixtures.ts b/e2e/console/fixtures.ts index 8b2bf0522..44618a63d 100644 --- a/e2e/console/fixtures.ts +++ b/e2e/console/fixtures.ts @@ -22,16 +22,8 @@ import { freeLoopbackPort } from "../helpers/port.js"; import { REPO_ROOT } from "../helpers/repo-root.js"; import { waitFor } from "../helpers/wait.js"; -/** - * The browser lane's daemons. Each is a real daemon process with HTTP enabled on a free - * loopback port and its own `SIMLOCK_HOME`, so nothing here touches the machine's own Simlock. - * A worker runs the end-to-end suite's fake driver; a gateway runs no drivers at all. - */ - export interface ConsoleDaemon { - /** The console's address, exactly as `simlock status --json` reports it. */ readonly url: string; - /** Runs `simlock` against this daemon, for a test that changes its state (a revoked token). */ readonly cli: (args: readonly string[]) => Promise; readonly tokens: { readonly operator: string; @@ -40,19 +32,13 @@ export interface ConsoleDaemon { }; } -/** A daemon a test drives itself: its CLI, and stopping and starting it in place. */ export interface RunningDaemon extends ConsoleDaemon { readonly port: number; - /** The daemon's unix socket, for a test that drives it with the programmatic client. */ readonly socketPath: string; cliBackground(args: readonly string[]): CliBackgroundHandle; - /** Replaces the fake driver's script. A daemon reads its host facts and catalog at start. */ writeDriverScript(script: FakeDriverScript): Promise; - /** `simlock daemon stop`, keeping the home: `start()` brings the same daemon back. */ stop(): Promise; - /** `simlock daemon start` on the same home, port and tokens. */ start(): Promise; - /** Stops the daemon and deletes its home. */ dispose(options?: { readonly preservePages?: readonly Page[] }): Promise; /** why: stats read completed minutes only, so this moves the usage rows two minutes back. */ prepareHistoricalUsage(): Promise; @@ -60,7 +46,6 @@ export interface RunningDaemon extends ConsoleDaemon { export interface StartOptions { readonly config?: Readonly>; - /** The fake driver's script, written before the daemon starts. */ readonly driverScript?: FakeDriverScript; } @@ -213,7 +198,7 @@ async function closeActivePages(preserved: ReadonlySet): Promise } /** - * Pins a worker's capacity to the flow, not the machine running it, as the end-to-end suite + * why: Pins a worker's capacity to the flow, not the machine running it, as the end-to-end suite * does: fake devices use no real memory, so a nominal per-device budget keeps the RAM gate from * refusing a lease on a small CI runner. */ @@ -417,7 +402,6 @@ export async function startDaemon( } } -/** A gateway and the workers that joined it, each labelled. */ export interface Fleet { readonly gateway: RunningDaemon; readonly workers: readonly RunningDaemon[]; @@ -432,7 +416,7 @@ export async function mintJoinToken(gateway: RunningDaemon): Promise { } /** - * Starts a gateway and one worker per entry of `workers`, each joined to it with its label, and + * why: Starts a gateway and one worker per entry of `workers`, each joined to it with its label, and * waits until the gateway lists every one of them as connected and has read it: a worker that is * still `starting`, or not yet read, has no capacity or catalog in its view, and a test that * leases through the gateway or reads a card's capacity right away would see that gap. @@ -504,15 +488,8 @@ export async function disposeFleet( await fleet.gateway.dispose(options); } -/** The protocol range the stand-in below claims: older than any this gateway speaks. */ export const OLD_PROTOCOL = { max: 3, min: 1 } as const; -/** - * A worker too old for the gateway, without building an old Simlock: it dials the gateway's - * uplink with a real join token, as a worker does, and answers the gateway's `hello` the way a - * daemon speaking only protocol {@link OLD_PROTOCOL} does. The gateway then lists it as - * `incompatible` with both ranges. Resolves once the uplink is open; call the result to hang up. - */ export async function joinOldWorker( gateway: RunningDaemon, identity: { readonly id: string; readonly label: string }, @@ -699,10 +676,6 @@ export async function startProxiedDaemon( }; } -/** - * `daemon` is a single host, shared by every test in a Playwright worker, and the base URL every - * `page.goto` resolves against. `gateway` starts only for a test that asks for it. - */ export const test = base.extend< { proxied: ProxiedDaemon }, { daemon: RunningDaemon; gateway: RunningDaemon; proxiedDaemon: ProxiedDaemon } @@ -755,7 +728,6 @@ export const test = base.extend< export { expect }; -/** Signs in through the sign-in screen, as an operator would. */ export async function signIn(page: Page, token: string): Promise { await page.getByLabel("Operator token").fill(token); await page.getByRole("button", { name: "Sign in" }).click(); diff --git a/e2e/gateway-component-install.test.ts b/e2e/gateway-component-install.test.ts index 795deb1c0..ea4ac3c9f 100644 --- a/e2e/gateway-component-install.test.ts +++ b/e2e/gateway-component-install.test.ts @@ -36,7 +36,6 @@ function idOf(workers: readonly WorkerView[], label: string): string { return worker.id; } -/** Which workers the gateway's catalog lists Android `runtime` for. */ async function androidRuntimeWorkers(gateway: TestEnv, runtime: string): Promise { const catalog = await gateway.cli(["catalog", "--platform", "android", "--json"]); expect(catalog.code, catalog.stderr).toBe(0); @@ -61,7 +60,7 @@ describe("simlock component install through a gateway", () => { availableOsVersions: ["34"], installProgress: [50], knownModels: ["Pixel 9"], - // Held open long enough for `worker list` to see it. + // why: Held open long enough for `worker list` to see it. latencyMs: { installComponent: 3_000 }, }, }; @@ -94,7 +93,6 @@ describe("simlock component install through a gateway", () => { const locked = idOf(workers, "locked"); expect(await androidRuntimeWorkers(gateway, "35")).toBeUndefined(); - // An agent token on the gateway's HTTP API is refused, and no worker starts an install. const agent = await gateway.cli(["token", "create", "--role", "agent"]); expect(agent.code, agent.stderr).toBe(0); const refused = await fetch(`http://127.0.0.1:${port}/v1/components/install`, { @@ -143,7 +141,6 @@ describe("simlock component install through a gateway", () => { outcome: "refused", workerId: locked, }); - // Progress on stderr names the worker it came from. const progress = install.stderr .trim() .split("\n") @@ -151,7 +148,7 @@ describe("simlock component install through a gateway", () => { .filter((line) => line["stage"] !== undefined); expect(progress).toContainEqual({ fraction: 0.5, stage: "downloading", workerId: open }); - // Read straight after the command, with no wait: the relay refreshed the catalog first. + // why: Read straight after the command, with no wait: the relay refreshed the catalog first. expect(await androidRuntimeWorkers(gateway, "35")).toEqual([open]); }); diff --git a/e2e/gateway-event-relay.test.ts b/e2e/gateway-event-relay.test.ts index 24acf102d..c4cae3445 100644 --- a/e2e/gateway-event-relay.test.ts +++ b/e2e/gateway-event-relay.test.ts @@ -10,12 +10,6 @@ import { SimlockWire } from "../src/simlock-client/wire.js"; import { mintJoinToken, waitFor, withDaemon, withHttpDaemon } from "./helpers/index.js"; import type { RecordedEvent } from "./helpers/events.js"; -/** - * ADR 0014 §2 and §5 with real processes: a gateway and one worker, the CLI an operator uses. - * A worker's event keeps its `id` and `timestamp` on the gateway, which is what lets the two - * event files be joined; `workerId` and the gateway's own `seq` are the only marks of the relay. - */ - async function gatewayWithWorker() { const { env: gateway, port } = await withHttpDaemon((port) => ({ configOverrides: { http: { host: "127.0.0.1", port } }, @@ -49,7 +43,6 @@ function leaseArgs(agentId: string): string[] { ]; } -/** The lines of a stream: a replayed event is printed as itself, a pushed one inside its push. */ function parse(stdout: string): RecordedEvent[] { return stdout .split("\n") @@ -281,11 +274,8 @@ describe("gateway event relay", () => { const follower = gateway.cliBackground(["events", "--follow", "--since", "1h"], { env: { SIMLOCK_ADMIN_TOKEN: adminToken, SIMLOCK_HOME: gate.home }, }); - // The follower has subscribed and asked for the replay; the gate holds that request. await waitFor(() => gate.replayHeld(), { label: "the replay request reached the gate" }); - // A lease is granted while the replay is pending: its push reaches the follower, and the - // replay, released only afterwards, is taken after the grant and holds it too. const holder = worker.cliBackground(leaseArgs("relay-follow")); const grant = JSON.parse(await holder.firstStdoutLine()) as { lease: { id: string } }; const isGrant = (event: RecordedEvent) => @@ -364,12 +354,6 @@ async function gatewayWithTwoWorkers() { const idsOf = (events: readonly RecordedEvent[]) => events.map((event) => event.id); -/** - * A unix-socket proxy in front of the gateway's daemon socket that holds the follower's - * `events.replay` request until `release()`. Everything else, pushes included, passes through, so - * an event emitted while the request is held reaches the follower as a push and is then also in - * the replay taken after the release. - */ async function gateReplay(upstreamSocket: string) { const home = await mkdtemp(join(tmpdir(), "sl-gate-")); // why: the follower runs with `--gateway`, which looks for a gateway folder under this root. @@ -424,7 +408,7 @@ async function gateReplay(upstreamSocket: string) { held?.(); }, async close() { - // `server.close` waits for every open connection; destroy them so a failed assertion in + // why: `server.close` waits for every open connection; destroy them so a failed assertion in // the test body is reported, not replaced by a timeout here. for (const socket of open) socket.destroy(); await new Promise((resolve) => server.close(() => resolve())); diff --git a/e2e/gateway-fast-fail.test.ts b/e2e/gateway-fast-fail.test.ts index 789150814..5a66ceaeb 100644 --- a/e2e/gateway-fast-fail.test.ts +++ b/e2e/gateway-fast-fail.test.ts @@ -4,16 +4,6 @@ import { mintJoinToken, waitFor, withDaemon, withHttpDaemon } from "./helpers/in import type { CliResult, TestEnv } from "./helpers/index.js"; import type { FakeDriverScript } from "./fake-driver/types.js"; -/** - * ADR 0009 §4 end to end, with real processes: a gateway daemon, worker daemons that dial it - * over a real WebSocket, and the CLI an agent would use. A request no worker can serve fails at - * once with the right code instead of waiting. - * - * Every request below has no `--no-wait` and no `--timeout`, the case that used to wait for ever. - * The CLI call carries a deadline of its own, so a request that waits fails here by name instead - * of hanging the suite. - */ - const FAST = { timeout: 20_000 }; const EXIT_NO_CAPACITY = 11; const EXIT_UNSERVABLE = 12; @@ -26,7 +16,6 @@ interface WorkerView { } async function startGateway() { - // `driver: "none"`: a gateway starts no drivers (ADR 0005 §2). const { env: gateway, port } = await withHttpDaemon((port) => ({ configOverrides: { http: { enabled: true, host: "127.0.0.1", port } }, role: "gateway", @@ -40,7 +29,7 @@ async function startGateway() { script: FakeDriverScript, extraConfig: Record = {}, ) => - // Seeded before the daemon starts: a worker reports its catalog as its uplink opens. + // why: Seeded before the daemon starts: a worker reports its catalog as its uplink opens. withDaemon({ configOverrides: { gateway: { label, token: await mintJoinToken(gateway), url: `ws://127.0.0.1:${port}` }, @@ -58,7 +47,6 @@ async function workersOf(gateway: TestEnv): Promise { return (listed.json as { workers: WorkerView[] }).workers; } -/** Waits until `labels` are all connected with a catalog read, and returns their ids by label. */ async function waitForWorkers( gateway: TestEnv, labels: readonly string[], @@ -118,7 +106,6 @@ function expectFailure(result: CliResult, code: string, exit: number): void { describe("a gateway fails a request no worker can serve at once", () => { it("fails a request for a model, a runtime, or a platform no worker has, and lets the same requester ask again", async () => { const { gateway, join } = await startGateway(); - // 27.0 is installed, but iPhone 17 pairs with 26.0 only. await join("worker-a", { ios: { ...IPHONE_17, @@ -128,20 +115,17 @@ describe("a gateway fails a request no worker can serve at once", () => { }); await waitForWorkers(gateway, ["worker-a"]); - // A model no worker lists. expectFailure( await lease(gateway, "agent-1", "iPhone 99", ["--detach"]), "UNKNOWN_MODEL", EXIT_UNSERVABLE, ); - // The same requester asks again at once: another answer, not REQUESTER_ALREADY_LEASED. expectFailure( await lease(gateway, "agent-1", "iPhone 99", ["--detach"]), "UNKNOWN_MODEL", EXIT_UNSERVABLE, ); - // A runtime no worker has, and, with --allow-download, still none: only installed runtimes count. const runtime = [ "--platform", "ios", @@ -157,7 +141,6 @@ describe("a gateway fails a request no worker can serve at once", () => { const download = await gateway.cli(["lease", ...runtime, "--detach", "--allow-download"], FAST); expectFailure(download, "RUNTIME_MISSING", EXIT_UNSERVABLE); - // A runtime a worker has, that no worker pairs with the model asked for. const unpaired = await gateway.cli( [ "lease", @@ -175,7 +158,6 @@ describe("a gateway fails a request no worker can serve at once", () => { ); expectFailure(unpaired, "RUNTIME_MISSING", EXIT_UNSERVABLE); - // A platform no worker has a driver for. const android = await gateway.cli( [ "lease", @@ -191,7 +173,6 @@ describe("a gateway fails a request no worker can serve at once", () => { ); expectFailure(android, "NO_DRIVER", EXIT_UNSERVABLE); - // The fleet still serves what it has, to the requester that was refused five times. const granted = await lease(gateway, "agent-1", "iPhone 17", ["--detach"]); expect(granted.code, granted.stderr).toBe(0); const { lease: held } = granted.json as { lease: { id: string } }; @@ -215,11 +196,9 @@ describe("a gateway fails a request no worker can serve at once", () => { FAST, ); - // No worker lists a model of the class. const watch = await ask("--class", "watch"); expectFailure(watch, "UNKNOWN_MODEL", EXIT_UNSERVABLE); expect(watch.stderr).toContain("watch"); - // Every model of the class pairs with a runtime outside the range, with or without a download. expectFailure( await ask("--class", "phone", "--os", "<=17"), "RUNTIME_MISSING", @@ -230,7 +209,6 @@ describe("a gateway fails a request no worker can serve at once", () => { "RUNTIME_MISSING", EXIT_UNSERVABLE, ); - // A request that names neither model nor class means phone, and the fleet has one. const granted = await ask(); expect(granted.code, granted.stderr).toBe(0); const { lease: held } = granted.json as { lease: { id: string } }; @@ -268,7 +246,6 @@ describe("a gateway fails a request no worker can serve at once", () => { const away = await join("worker-16", { ios: IPHONE_16 }); const ids = await waitForWorkers(gateway, ["worker-17", "worker-16"]); - // worker-17 drained: the only worker that lists iPhone 17 is not taking requests. expect((await gateway.cli(["worker", "drain", ids["worker-17"] ?? ""])).code).toBe(0); expectFailure( await lease(gateway, "agent-1", "iPhone 17", ["--detach"]), @@ -277,8 +254,6 @@ describe("a gateway fails a request no worker can serve at once", () => { ); expect((await gateway.cli(["worker", "undrain", ids["worker-17"] ?? ""])).code).toBe(0); - // worker-16 gone: a request only it lists fails with NO_CAPACITY, not UNKNOWN_MODEL, while - // worker-17 still takes requests. await away.killDaemon(); await waitFor( async () => @@ -322,7 +297,6 @@ describe("a gateway fails a request no worker can serve at once", () => { const held = await lease(gateway, "agent-holder", "iPhone 17", ["--detach"]); expect(held.code, held.stderr).toBe(0); - // A second requester waits: the worker can serve it and is busy. const waiting = gateway.cliBackground([ "lease", "--platform", diff --git a/e2e/gateway-fleet.test.ts b/e2e/gateway-fleet.test.ts index 4d58c4bc4..da305f2bd 100644 --- a/e2e/gateway-fleet.test.ts +++ b/e2e/gateway-fleet.test.ts @@ -9,16 +9,6 @@ import { } from "./helpers/index.js"; import type { TestEnv } from "./helpers/env.js"; -/** - * ADR 0005 #117 end to end, with real processes: a gateway daemon, two worker daemons that dial - * it over a real WebSocket, and the CLI an operator would actually use. - * - * What this flow proves that the unit suites cannot: the uplink is a real network connection on - * the gateway's own HTTP port, the join token is a real credential minted on the gateway and - * read from a worker's config file, and `simlock status` against the gateway aggregates two - * separate machines' registries. - */ - interface WorkerView { readonly id: string; readonly label?: string; @@ -63,7 +53,6 @@ async function listWorkers(gateway: TestEnv): Promise { return (result.json as { workers: WorkerView[] }).workers; } -/** Polls the *gateway*, never a worker: the gateway's own view is what this flow is about. */ async function waitForWorkers( gateway: TestEnv, predicate: (workers: WorkerView[]) => boolean, @@ -80,13 +69,11 @@ async function waitForWorkers( return workers; } -/** A worker's per-platform limit, or 0 if the view has no capacity yet. Out here so the test - * body below stays a sequence of assertions rather than a chain of fallbacks. */ function iosLimit(worker: WorkerView | undefined): number { return worker?.capacity?.ios.limit ?? 0; } -/** The id of the worker an operator would recognize by `label`. Throws rather than degrading +/** why: The id of the worker an operator would recognize by `label`. Throws rather than degrading * to `""`, which would silently turn "no such worker" into a request for one. */ function idOf(workers: readonly WorkerView[], label: string): string { const worker = workers.find((candidate) => candidate.label === label); @@ -103,8 +90,6 @@ function iosModels(json: unknown): readonly string[] { return platforms.find((entry) => entry.platform === "ios")?.models ?? []; } -/** A worker's iOS catalog entry, by the worker's label. */ -/** The label of the worker with the smallest id: the one whose catalog wins a disagreement. */ function firstWorkerLabel(workers: readonly WorkerView[]): string { const first = [...workers].sort((left, right) => (left.id < right.id ? -1 : 1))[0]; return first?.label ?? ""; @@ -116,14 +101,12 @@ function iosEntryOf(views: readonly WorkerView[], label: string) { ?.catalog.find((entry) => entry.platform === "ios"); } -/** The Xcode entry in a worker's host facts, by the worker's label. */ function xcodeOf(views: readonly WorkerView[], label: string) { return views .find((view) => view.label === label) ?.host?.tools.find((tool) => tool.name === "xcode"); } -/** The worker named `label` has the same host facts and catalog in both lists. */ function expectSameView( actual: readonly WorkerView[], expected: readonly WorkerView[], @@ -138,7 +121,7 @@ function expectSameView( describe("gateway fleet", () => { it("two workers join, status aggregates them, a kill disconnects one, drain flags the other", async () => { - // `driver: "none"`: a gateway starts no drivers and warns about every worker-only key in + // why: `driver: "none"`: a gateway starts no drivers and warns about every worker-only key in // its config (ADR 0005 §2), so it gets no fake-driver wiring and no capacity seeding. const { env: gateway, port } = await withHttpDaemon((port) => ({ configOverrides: { http: { host: "127.0.0.1", port } }, @@ -146,8 +129,6 @@ describe("gateway fleet", () => { driver: "none", })); - // 1. The gateway mints a join token. `--role worker` is what makes it a join token and - // nothing else: it opens an uplink and no `/v1` route. const minted = await gateway.cli(["token", "create", "--role", "worker"]); expect(minted.code).toBe(0); const { secret, token } = minted.json as { secret: string; token: { role: string } }; @@ -158,7 +139,7 @@ describe("gateway fleet", () => { const url = `ws://127.0.0.1:${port}`; const workerA = await withDaemon({ configOverrides: { gateway: { label: "worker-a", token: secret, url } }, - // Seeded before the daemon starts: the worker reports its catalog as its uplink opens, and + // why: Seeded before the daemon starts: the worker reports its catalog as its uplink opens, and // a script written afterwards would only reach the gateway on its 30s periodic refresh. driverScript: { ios: { knownModels: ["iPhone 16"], availableOsVersions: ["18.4"] } }, }); @@ -171,17 +152,14 @@ describe("gateway fleet", () => { gateway, (workers) => workers.length === 2 && - // The gateway marks a worker connected before its first refresh builds the view, so + // why: The gateway marks a worker connected before its first refresh builds the view, so // "connected" alone can still be a view with no capacity: wait for the capacity too. workers.every((worker) => worker.connection === "connected" && iosLimit(worker) > 0), "both workers connected to the gateway, each with its capacity", ); expect(joined.map((worker) => worker.label).sort()).toEqual(["worker-a", "worker-b"]); - // Two machines, two identities: the ids are the workers' own `instance.json`, never a name - // the gateway invented or a label an operator chose. expect(new Set(joined.map((worker) => worker.id)).size).toBe(2); - // 3. `simlock status` against the gateway is the same shape one machine returns, summed. const status = await gateway.cli(["status", "--json"]); expect(status.code).toBe(0); const statusView = status.json as StatusView; @@ -190,7 +168,6 @@ describe("gateway fleet", () => { expect(iosLimit(joined[0])).toBeGreaterThan(0); expect(statusView.capacity.ios.limit).toBe(iosLimit(joined[0]) + iosLimit(joined[1])); - // 4. The catalog is the union, and says which worker each model came from. await waitFor( async () => { const models = iosModels((await gateway.cli(["catalog", "--json"])).json); @@ -206,7 +183,7 @@ describe("gateway fleet", () => { ).platforms.find((entry) => entry.platform === "ios"); expect(iosCatalog?.modelWorkers?.["iPhone 16"]).toEqual([idOf(joined, "worker-a")]); - // 5. Killing a worker flips its view without anyone polling it: the uplink closing *is* the + // why: 5. Killing a worker flips its view without anyone polling it: the uplink closing *is* the // signal (ADR 0005 §6). The gateway's own backstop tick is 30s, and this must not need // it -- so a short deadline here is the assertion. await workerA.killDaemon(); @@ -215,12 +192,9 @@ describe("gateway fleet", () => { (workers) => connectionOf(workers, "worker-a") === "disconnected", "the killed worker's view flipped to disconnected", ); - // The other worker is untouched, and the dead one's view is kept rather than dropped. expect(surviving).toHaveLength(2); expect(connectionOf(surviving, "worker-b")).toBe("connected"); - // 6. Draining flags the surviving worker (what #118's dispatch will honour), and the flag - // is on the view an operator reads back. const workerBId = idOf(surviving, "worker-b"); const drain = await gateway.cli(["worker", "drain", workerBId]); expect(drain.code).toBe(0); @@ -229,7 +203,6 @@ describe("gateway fleet", () => { true, ); - // 7. Removing a *connected* worker is refused; the disconnected one is forgotten. const refused = await gateway.cli(["worker", "remove", workerBId]); expect(refused.error?.code).toBe("WORKER_CONNECTED"); const workerAId = idOf(surviving, "worker-a"); @@ -237,17 +210,6 @@ describe("gateway fleet", () => { expect(removed.code).toBe(0); expect(await listWorkers(gateway)).toHaveLength(1); - // 8. A worker's own *business* events -- not just the gateway's own connect/disconnect - // facts, which carry `workerId` too and would pass this check even with republishing - // (`WorkerLink#onWorkerEvent`) deleted outright -- reach the gateway's buffer with - // `workerId` attached, which is what makes `simlock events` against a gateway a - // fleet-wide view (ADR 0005 §22). Driven deterministically: a real `lease.granted` on - // worker B's own daemon, requested directly against its socket (routing across the - // fleet is #118's, not this PR's), well after its `events.subscribe` was established at - // connect. `module` is asserted as `lease-lifecycle` -- the emitting engine's own name, - // unchanged by republishing -- specifically because it is not one of the six subjects - // `docs/internal/EVENTS.md` calls "the gateway's own" (`worker.*`), so this cannot be satisfied by - // a gateway-authored fact that merely happens to name worker B. const events = await gateway.events(); const connectedFacts = events.filter((event) => event.event === "worker.connected"); expect(connectedFacts).toHaveLength(2); @@ -292,9 +254,6 @@ describe("gateway fleet", () => { driver: "none", })); const url = `ws://127.0.0.1:${port}`; - // Both workers have the same model and the same two runtimes installed, and pair the model - // with a different one of them. Each also accepts its own other name for it and reports its - // own image of one shared runtime. const installed = ["18.4", "26.0"]; await withDaemon({ configOverrides: { gateway: { label: "worker-a", token: await mintJoinToken(gateway), url } }, @@ -352,8 +311,6 @@ describe("gateway fleet", () => { modelRuntimes: { "iPhone 16": ["26.0"] }, }); - // Each worker's host facts sit beside its catalog, tools included once the worker has read - // them; `GET /v1/workers` serves the same views. const withTools = await waitForWorkers( gateway, (views) => @@ -387,8 +344,6 @@ describe("gateway fleet", () => { ); expect(ios?.modelRuntimes).toEqual({ "iPhone 16": ["18.4", "26.0"] }); expect(ios?.modelAliases).toEqual({ "iPhone 16": ["iphone-16-a", "iphone-16-b"] }); - // The two workers class the model differently; the first worker in id order wins, so the - // fleet's class is the one the worker with the smaller id reported. const classOfLabel: Record = { "worker-a": "phone", "worker-b": "tablet", @@ -407,7 +362,6 @@ describe("gateway fleet", () => { driver: "none", })); const url = `ws://127.0.0.1:${port}`; - // Both workers list My Tablet; only worker-a has it as a custom profile. await withDaemon({ configOverrides: { gateway: { label: "worker-a", token: await mintJoinToken(gateway), url } }, driverScript: { @@ -460,8 +414,6 @@ describe("gateway fleet", () => { driver: "none", })); const url = `ws://127.0.0.1:${port}`; - // One model per worker, so the model alone decides where a request lands. Both workers can - // slim the one runtime they have. await withDaemon({ configOverrides: { gateway: { label: "default-full", token: await mintJoinToken(gateway), url }, @@ -536,8 +488,6 @@ describe("gateway fleet", () => { })); const minted = await gateway.cli(["token", "create", "--role", "worker"]); const { secret } = minted.json as { secret: string }; - // The fake driver resolves model names exactly, so a grant proves the gateway sent the - // worker its own name for the model. await withDaemon({ configOverrides: { gateway: { label: "worker", token: secret, url: `ws://127.0.0.1:${port}` }, @@ -608,7 +558,6 @@ describe("gateway fleet", () => { "both workers connected with their catalogs", ); - // Warm one device on the first worker through its own socket, and let it go idle. const warmed = await warmWorker.cli([ "lease", "--platform", @@ -624,7 +573,7 @@ describe("gateway fleet", () => { }; await warmWorker.cli(["release", warmLease.id]); await waitForDeviceState(warmWorker, warmDevice.driverDeviceId, "ready"); - // Until the gateway's view shows that device ready, a request could miss it. + // why: Until the gateway's view shows that device ready, a request could miss it. await waitFor( async () => { const status = await gateway.cli(["status", "--json"]); @@ -674,7 +623,6 @@ describe("gateway fleet", () => { }, fakeDriverPlatforms: ["ios"], }); - // Sorted either way by id, only the range can send the request to the newer worker. await join("worker-old", "17.5"); await join("worker-new", "26.0"); await waitForWorkers( @@ -705,9 +653,6 @@ describe("gateway fleet", () => { role: "gateway", driver: "none", })); - // An operator token is a real credential that opens no uplink (ADR 0005 §25). The worker - // keeps retrying at its backoff cap rather than giving up, so what this asserts is the - // gateway's side: it never becomes a worker. const operator = await gateway.cli(["token", "create", "--role", "operator"]); const { secret } = operator.json as { secret: string }; await withDaemon({ @@ -725,9 +670,6 @@ describe("gateway fleet", () => { }, { label: "the gateway reported the refused uplink", timeout: 20_000 }, ); - // M3: `workerId` is whatever the dial claimed in its headers over the real WebSocket - // adapter, not just the in-memory one the unit suite scripts -- an operator otherwise had - // no way to tell *which* machine's misconfigured worker keeps knocking. expect(rejected?.workerId).toBeDefined(); expect(await listWorkers(gateway)).toEqual([]); diff --git a/e2e/gateway-smoke.test.ts b/e2e/gateway-smoke.test.ts index 7a3aae25f..b46eb1631 100644 --- a/e2e/gateway-smoke.test.ts +++ b/e2e/gateway-smoke.test.ts @@ -8,25 +8,6 @@ import { withHttpDaemon, } from "./helpers/index.js"; -/** - * ADR 0005's happy path, over real transports: a real gateway process, real worker processes - * dialling it over a real WebSocket, and a client reaching the fleet the way a user actually - * does -- the CLI on the gateway's own machine, and HTTP from anywhere else. - * - * Why this file exists at all. `src/daemon/gateway-fleet.e2e.test.ts` already proves lease - * routing and a real `device.exec` across two worker processes, but it drives them through the - * daemon's own `dispatch()` in-process. Everything between that dispatcher and a user was - * unproven for gateway mode: the CLI's own gateway/worker branch, the HTTP lease and exec - * routes against a gateway, SSE streaming of proxied output, and exit-code propagation. The one - * out-of-process lease that existed (`e2e/gateway-fleet.test.ts`) deliberately went straight to - * a worker's own socket, because fleet routing belonged to a later PR than the one that wrote - * it -- that PR has since landed, and nothing came back to close the gap. - * - * These are smoke tests. They prove the layers are wired to each other, not that the routing - * policy, the queue's ordering rules or the refusal lists are correct -- `src/gateway`'s own - * suites own that, and duplicating it here would buy nothing for a great deal of wall clock. - */ - interface WorkerView { readonly id: string; readonly label?: string; @@ -45,7 +26,6 @@ interface Grant { interface WorkerSpec { readonly label: string; readonly models: readonly string[]; - /** Seeded straight into the worker's config; used to pin a one-device worker for queueing. */ readonly limits?: Record; } @@ -56,7 +36,7 @@ interface Fleet { } /** - * A gateway plus its workers, all as separate daemons with their own `SIMLOCK_HOME` (and so + * why: A gateway plus its workers, all as separate daemons with their own `SIMLOCK_HOME` (and so * their own instance identity), joined over a real uplink. * * Returns only once the gateway can actually *serve* each worker's models, not merely once the @@ -65,7 +45,7 @@ interface Fleet { * these tests are about. */ async function startFleet(specs: readonly WorkerSpec[], agentId: string): Promise { - // `driver: "none"`: a gateway starts no drivers and warns about every worker-only key in its + // why: `driver: "none"`: a gateway starts no drivers and warns about every worker-only key in its // config (ADR 0005 §2), so it gets no fake-driver wiring and no capacity seeding. const { env: gateway, port } = await withHttpDaemon((port) => ({ agentId, @@ -86,7 +66,7 @@ async function startFleet(specs: readonly WorkerSpec[], agentId: string): Promis }, ...(spec.limits === undefined ? {} : { limits: spec.limits }), }, - // Before the daemon starts, not after: the worker reports its catalog as its uplink + // why: Before the daemon starts, not after: the worker reports its catalog as its uplink // opens, and the gateway would otherwise only see these models on its 30s refresh. driverScript: { android: { knownModels: [...spec.models], availableOsVersions: ["35"] } }, }); @@ -123,8 +103,6 @@ interface SseFrame { readonly data: unknown; } -/** Reads a finite SSE body, skipping keepalive comments -- the exec route ends the stream on - * its terminal `exit`/`error` event. Same shape as `device-exec.test.ts`'s own reader. */ async function readSse(response: Response): Promise { const frames: SseFrame[] = []; for (const raw of (await response.text()).split("\n\n")) { @@ -149,7 +127,6 @@ function chunksOf(frames: readonly SseFrame[], stream: "stdout" | "stderr"): str .join(""); } -/** Mints an agent token on the gateway and returns the headers an HTTP client would send. */ async function agentAuth(gateway: TestEnv): Promise> { const created = await gateway.cli(["token", "create", "--role", "agent"]); expect(created.code).toBe(0); @@ -159,8 +136,6 @@ async function agentAuth(gateway: TestEnv): Promise> { }; } -/** A `queued` progress push at a given position, as `contention.test.ts` reads them on one - * machine -- ADR §10 requires a gateway to report the same states a worker does. */ function isQueuedAt(event: unknown, position: number): boolean { return ( typeof event === "object" && @@ -186,16 +161,10 @@ describe("gateway smoke", () => { expect(leased.code).toBe(0); const grant = leased.json as Grant; expect(grant.lease.worker?.label).toBe("worker-a"); - // ADR §16: the gateway lease id names its worker. Asserted as a prefix rather than parsed, - // which is exactly what the docs tell a *client* not to do -- this is the fleet's own - // invariant, not a client reading it. expect(grant.lease.id.startsWith(`${grant.lease.worker?.id ?? ""}.`)).toBe(true); - // 2. §27a: the worker sees the requester namespaced by the gateway, so a local `smoke-agent` - // on that machine and this one cannot collide on its one-lease rule. This is the seam the - // in-process test cannot see, because it never asks the worker for its own view. const worker = workers[0]; - // Thrown rather than optional-chained: a fleet with no worker means the harness is broken, + // why: Thrown rather than optional-chained: a fleet with no worker means the harness is broken, // and reading through `undefined` would surface that as a TypeError three lines later // instead of as this sentence. if (worker === undefined) throw new Error("expected the fleet to have a worker"); @@ -228,8 +197,6 @@ describe("gateway smoke", () => { ); expect(exec.code).toBe(3); expect(exec.stderr).toContain("a warning"); - // The *worker's* driver resolved the command -- its own device root is prepended, on a - // machine the CLI never talked to directly. expect(JSON.parse(exec.stdout)).toEqual({ argv: ["/fake/android", "devices", "--fake-exec-stderr=a warning", "--fake-exec-exit=3"], platform: "android", @@ -257,7 +224,6 @@ describe("gateway smoke", () => { { label: "the gateway's HTTP port accepting connections" }, ); - // 1. The same request resource a worker serves, answered by a gateway that owns no devices. const created = await fetch(`${baseUrl}/v1/lease-requests`, { body: JSON.stringify({ device: "Pixel 8", platform: "android" }), headers: auth, @@ -282,8 +248,6 @@ describe("gateway smoke", () => { ); expect(leaseId).not.toBe(""); - // 2. A command that really runs: the gateway proxies it to the worker and relays the - // worker's own chunks and exit code back down this SSE stream. const streamed = await fetch(`${baseUrl}/v1/leases/${leaseId}/exec`, { body: JSON.stringify({ args: ["devices", "--fake-exec-exit=5"], tool: "adb" }), headers: auth, @@ -295,18 +259,6 @@ describe("gateway smoke", () => { expect(frames.at(-1)).toEqual({ data: { exitCode: 5 }, event: "exit" }); expect(JSON.parse(chunksOf(frames, "stdout"))).toMatchObject({ platform: "android" }); - // 3. A **silent, slow** command: the one shape that isolates ADR §19a's `started` push end - // to end. `http/app.ts` races the command settling against `started`, and a command that - // merely succeeds reaches the same committed `200` either way -- just not until it - // exits. So what `started` actually buys is timing, and the assertion has to be about - // timing: the response head must arrive while the command is still running. Relaying it - // is the gateway's own job (it holds no driver refusal list and cannot know a process - // exists any other way), so this is the assertion that fails if that relay is lost. - // - // Verified by mutation: neutering `FleetLeaseCoordinator#exec`'s `onStarted` relay - // leaves every other assertion in this file green and fails only the deadline below. - // Both weaker versions of this test -- a short silent command, and the refusal in 4 -- - // passed against that mutation, which is why neither of them is the one that guards it. const slowStartedAt = Date.now(); const silent = await fetch(`${baseUrl}/v1/leases/${leaseId}/exec`, { body: JSON.stringify({ @@ -319,18 +271,12 @@ describe("gateway smoke", () => { const headHeadersMs = Date.now() - slowStartedAt; expect(silent.status).toBe(200); expect(silent.headers.get("content-type")).toContain("text/event-stream"); - // Generously below the command's own 4s: the point is "well before it exits", not a precise - // latency budget, so this stays honest on a loaded machine while still failing outright if - // the route waits for the process to finish before answering. expect(headHeadersMs).toBeLessThan(2_000); const silentFrames = await readSse(silent); expect(chunksOf(silentFrames, "stdout")).toBe(""); expect(silentFrames.at(-1)).toEqual({ data: { exitCode: 0 }, event: "exit" }); - // 4. A refusal that happens *before* any process exists keeps its own status instead: the - // route has not committed to a stream, so §19a′'s `PASSTHROUGH_REFUSED` arrives as a - // real `422` rather than buried inside an already-committed `200`. const refused = await fetch(`${baseUrl}/v1/leases/${leaseId}/exec`, { body: JSON.stringify({ args: ["kill-server"], tool: "adb" }), headers: auth, @@ -485,7 +431,7 @@ describe("gateway smoke", () => { const first = await submit(); const firstRead = await readGranted(first.request.id); await release(firstRead); - // The release answers before the worker has reclaimed the device. The second request is + // why: The release answers before the worker has reclaimed the device. The second request is // granted inside its own `201` only once the gateway's view shows that device ready again. await waitFor( async () => { @@ -523,10 +469,6 @@ describe("gateway smoke", () => { }); it("queues a fleet-wide FIFO behind a full worker, refuses --no-wait, and serves the waiter on release", async () => { - // One worker that can run exactly one device, so the fleet's whole capacity is one lease. - // ADR §10: the gateway keeps a single fleet-wide queue and reports the same codes and - // progress states a worker's own queue does -- which is what this asserts, over the real - // CLI, against a daemon that owns no devices at all. const { gateway } = await startFleet( [ { @@ -543,13 +485,11 @@ describe("gateway smoke", () => { expect(first.code).toBe(0); const firstGrant = first.json as Grant; - // §14: one lease per requester is fleet-wide, not per machine. const repeat = await gateway.cli([...leaseArgs, "--agent-id", "agent-a", "--detach"]); expect(repeat.code).toBe(13); expect(repeat.error).toMatchObject({ code: "REQUESTER_ALREADY_LEASED" }); expect(repeat.error?.message).toContain(firstGrant.lease.id); - // A second requester with nowhere to go is refused rather than queued. const noWait = await gateway.cli([ ...leaseArgs, "--agent-id", @@ -560,13 +500,11 @@ describe("gateway smoke", () => { expect(noWait.code).toBe(11); expect(noWait.error).toMatchObject({ code: "NO_CAPACITY" }); - // A waiter queues, and says so with the same progress push a worker sends. const waiter = gateway.cliBackground([...leaseArgs, "--agent-id", "agent-c"]); await waitFor(() => waiter.progressEvents().some((event) => isQueuedAt(event, 1)), { label: "agent-c queued at position 1 in the gateway's fleet queue", }); - // Releasing the only device in the fleet is what dispatches it. expect((await gateway.cli(["release", firstGrant.lease.id])).code).toBe(0); const served = JSON.parse(await waiter.firstStdoutLine(30_000)) as Grant; expect(served.lease.worker?.label).toBe("worker-a"); @@ -577,8 +515,6 @@ describe("gateway smoke", () => { }); it("renews and releases a gateway-issued lease by the id the gateway handed out, and caps its own TTL", async () => { - // The gateway's own cap applies before anything is dispatched (§15), so it is the - // gateway's value that decides -- not the worker's, which is the 4h default here. const { gateway } = await startFleet( [{ label: "worker-a", models: ["Pixel 8"] }], "smoke-agent", @@ -610,9 +546,6 @@ describe("gateway smoke", () => { expect(leased.code).toBe(0); const grant = leased.json as Grant; - // The id an operator reads back has to be the id every other verb accepts. Round 6 found a - // projection that broke exactly this for `status.get`, caught only by a unit test -- here it - // is over the socket, forwarded to the worker and back. const listed = await gateway.cli(["status", "--json"]); expect(listed.code).toBe(0); const fleetLeases = (listed.json as { leases: { id: string }[] }).leases; @@ -625,7 +558,6 @@ describe("gateway smoke", () => { expect(renewedView.ttlDeadline).toBeGreaterThanOrEqual(grant.lease.ttlDeadline); expect((await gateway.cli(["release", grant.lease.id])).code).toBe(0); - // Released on the worker too, not merely forgotten by the gateway. await waitFor( async () => { const after = await gateway.cli(["status", "--json"]); diff --git a/e2e/helpers/index.ts b/e2e/helpers/index.ts index 2e8c73228..d73664460 100644 --- a/e2e/helpers/index.ts +++ b/e2e/helpers/index.ts @@ -1,7 +1,3 @@ -// Public surface for flow test files. Kept to exactly what a flow currently imports -// through this barrel -- add an export here (from its owning helper module) the -// moment a flow needs it, rather than pre-declaring the full internal surface, so an -// unused re-export is always a real signal. export { withDaemon, withSharedDaemon } from "./env.js"; export { assertPinnedAgentDevice, @@ -16,8 +12,6 @@ export { freeLoopbackPort } from "./port.js"; export { deviceStreamUrl } from "./device-stream.js"; export { withHttpDaemon } from "./http-daemon.js"; export { mintJoinToken } from "./join-token.js"; -// A flow that factors its own setup out of a test body needs to name what `withDaemon` hands -// back; every other flow keeps it inferred. export type { SharedTestEnv, TestEnv } from "./env.js"; export { DEVICE_READY_MS, waitFor } from "./wait.js"; export { events } from "./events.js"; diff --git a/scripts/slow-e2e.sh b/scripts/slow-e2e.sh index b4b1819ff..b020e4b21 100755 --- a/scripts/slow-e2e.sh +++ b/scripts/slow-e2e.sh @@ -222,7 +222,7 @@ log="${TMPDIR:-/tmp}/simlock-slow-e2e-$(date +%Y%m%d-%H%M%S).log" if ! mkdir "$lock" 2>/dev/null; then holder=$(cat "$lock/pid" 2>/dev/null || echo "") - # No pid yet means another caller is still starting its runner, unless that was long ago. + # why: No pid yet means another caller is still starting its runner, unless that was long ago. if { [ -n "$holder" ] && kill -0 "$holder" 2>/dev/null; } \ || { [ -z "$holder" ] && [ -n "$(find "$lock" -maxdepth 0 -mmin -1)" ]; }; then echo "busy: slow lane running as pid $holder from $(cat "$lock/root" 2>/dev/null), log $(cat "$lock/log" 2>/dev/null)" >&2 @@ -240,19 +240,17 @@ echo "$log" > "$lock/log" echo "$log" > "$lock.last" mkdir -p "$lane_tmp" -# The runner owns the lock from here: it records its pid, runs the lane, writes the exit code, -# and removes the lock however the lane ends. TMPDIR="$lane_tmp/" python3 - "$lock" "$log" "$root" "$script" "$@" <<'EOF' import os, subprocess, sys lock, log, root, script, args = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5:] child = os.fork() if child != 0: - # Written before this returns, so a second caller never sees a lock without a live pid. + # why: Written before this returns, so a second caller never sees a lock without a live pid. with open(os.path.join(lock, "pid"), "w") as f: f.write(str(child)) sys.exit(0) os.setsid() -# Let go of the caller's stdout and stderr, or `$(scripts/slow-e2e.sh)` waits for the whole run. +# why: Let go of the caller's stdout and stderr, or `$(scripts/slow-e2e.sh)` waits for the whole run. null = os.open(os.devnull, os.O_RDWR) for fd in (0, 1, 2): os.dup2(null, fd) diff --git a/src/source-hygiene.test.ts b/src/source-hygiene.test.ts index 9ef5705eb..35ccb94c9 100644 --- a/src/source-hygiene.test.ts +++ b/src/source-hygiene.test.ts @@ -86,7 +86,7 @@ const isWebsiteAsset = (name: string): boolean => describe("tracked files", () => { const trackedFiles = lsFiles([]) .filter((name) => !isCommittedBinary(name)) - // A submodule or a path deleted from the working tree is still listed by + // why: A submodule or a path deleted from the working tree is still listed by // `git ls-files`; only regular files can be read. .filter((name) => { try { @@ -97,7 +97,7 @@ describe("tracked files", () => { }); it("were enumerated from the git index", () => { - // Without this, an `execFileSync` that returned nothing would leave `it.each` with an + // why: Without this, an `execFileSync` that returned nothing would leave `it.each` with an // empty list and the suite would pass while checking no file at all. expect(trackedFiles.length).toBeGreaterThan(100); });