| ... | ... | @@ -7,6 +7,7 @@ import { |
| 7 | 7 | type DeviceServiceState, |
| 8 | 8 | } from "@t3tools/contracts"; |
| 9 | 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Exit from "effect/Exit"; |
| 10 | 11 | import * as Deferred from "effect/Deferred"; |
| 11 | 12 | import * as Fiber from "effect/Fiber"; |
| 12 | 13 | import * as PubSub from "effect/PubSub"; |
| ... | ... | @@ -469,3 +470,104 @@ it.effect.each(["shutdown", "close"] as const)( |
| 469 | 470 | Effect.scoped, |
| 470 | 471 | ), |
| 471 | 472 | ); |
| 473 | |
| 474 | it.effect.each([ |
| 475 | { hubReports: "off", outcome: "succeeds" }, |
| 476 | { hubReports: "booted", outcome: "fails" }, |
| 477 | { hubReports: "missing", outcome: "fails" }, |
| 478 | ] as const)( |
| 479 | "iOS shutdown $outcome when serve-sim rejects it and the hub reports the simulator $hubReports", |
| 480 | ({ hubReports, outcome }) => |
| 481 | Effect.gen(function* () { |
| 482 | const deviceId = DeviceId.make("22222222-2222-2222-2222-222222222222"); |
| 483 | const paths: string[] = []; |
| 484 | // The device list is stale until shutdown re-reads it from the hub. |
| 485 | let listed: "booted" | "off" | "missing" = "booted"; |
| 486 | const ready: DeviceHost.DeviceHostReady = { |
| 487 | nodePath: process.execPath, |
| 488 | hub: { origin: "http://device.test" }, |
| 489 | helpers: { serveSimAxSettings: null, serveSimCli: null }, |
| 490 | run: () => Effect.succeed({ code: 0, stdout: "", stderr: "" }), |
| 491 | }; |
| 492 | const host: DeviceHost.DeviceHost["Service"] = { |
| 493 | id: LOCAL_DEVICE_HOST_ID, |
| 494 | summary: Effect.succeed({ |
| 495 | id: LOCAL_DEVICE_HOST_ID, |
| 496 | kind: "local", |
| 497 | label: "Simulator host", |
| 498 | platforms: [{ platform: "ios", available: true }], |
| 499 | hubInstalled: true, |
| 500 | agentDeviceInstalled: false, |
| 501 | }), |
| 502 | platformAvailability: (platform) => Effect.succeed({ platform, available: true }), |
| 503 | ensureReady: () => Effect.succeed(ready), |
| 504 | ensureAgentReady: () => Effect.die("Agent access is not used in this test"), |
| 505 | current: Effect.succeed(ready), |
| 506 | stopAgent: Effect.void, |
| 507 | stop: Effect.void, |
| 508 | }; |
| 509 | const http = HttpClient.make((request) => |
| 510 | Effect.sync(() => { |
| 511 | const path = new URL(request.url).pathname; |
| 512 | paths.push(path); |
| 513 | if (path === "/api/devices") { |
| 514 | return HttpClientResponse.fromWeb( |
| 515 | request, |
| 516 | Response.json({ |
| 517 | emulators: [], |
| 518 | simulators: |
| 519 | listed === "missing" |
| 520 | ? [] |
| 521 | : [ |
| 522 | { |
| 523 | id: deviceId, |
| 524 | name: "iPhone", |
| 525 | platform: "ios", |
| 526 | version: "26", |
| 527 | physical: false, |
| 528 | booted: listed === "booted", |
| 529 | }, |
| 530 | ], |
| 531 | // A partial listing still decodes; it must not read as "off". |
| 532 | errors: listed === "missing" ? [{ message: "simctl list failed" }] : [], |
| 533 | }), |
| 534 | ); |
| 535 | } |
| 536 | if (path === "/vendor/serve-sim/grid/api/shutdown") { |
| 537 | // serve-sim runs `simctl shutdown` bare and returns its failure as-is. |
| 538 | listed = hubReports; |
| 539 | return HttpClientResponse.fromWeb( |
| 540 | request, |
| 541 | Response.json( |
| 542 | { ok: false, error: "Unable to shutdown device in current state: Shutdown" }, |
| 543 | { status: 500 }, |
| 544 | ), |
| 545 | ); |
| 546 | } |
| 547 | throw new Error(`Unexpected hub path: ${path}`); |
| 548 | }), |
| 549 | ); |
| 550 | const service = yield* makeWithHosts(new Map([[host.id, host]])).pipe( |
| 551 | Effect.provideService(HttpClient.HttpClient, http), |
| 552 | ); |
| 553 | yield* service.list; |
| 554 | const exit = yield* Effect.exit(service.shutdown({ deviceId, platform: "ios" })); |
| 555 | expect(paths.filter((path) => path.endsWith("shutdown"))).toEqual([ |
| 556 | "/vendor/serve-sim/grid/api/shutdown", |
| 557 | ]); |
| 558 | if (outcome === "succeeds") { |
| 559 | expect(Exit.isSuccess(exit)).toBe(true); |
| 560 | expect( |
| 561 | (yield* service.state).devices.find((device) => device.id === deviceId)?.booted, |
| 562 | ).toBe(false); |
| 563 | } else { |
| 564 | expect(Exit.isFailure(exit)).toBe(true); |
| 565 | expect( |
| 566 | (yield* service.state).devices.find((device) => device.id === deviceId)?.booted, |
| 567 | ).toBe(true); |
| 568 | } |
| 569 | }).pipe( |
| 570 | Effect.provide(ServerSettingsService.layerTest({ enableDeviceSupport: true })), |
| 571 | Effect.scoped, |
| 572 | ), |
| 573 | ); |