| ... | @@ -7,6 +7,7 @@ import { | ... | @@ -7,6 +7,7 @@ import { |
| 7 | type DeviceServiceState, | 7 | type DeviceServiceState, |
| 8 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 9 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| | 10 | import * as Exit from "effect/Exit"; |
| 10 | import * as Deferred from "effect/Deferred"; | 11 | import * as Deferred from "effect/Deferred"; |
| 11 | import * as Fiber from "effect/Fiber"; | 12 | import * as Fiber from "effect/Fiber"; |
| 12 | import * as PubSub from "effect/PubSub"; | 13 | import * as PubSub from "effect/PubSub"; |
| ... | @@ -469,3 +470,104 @@ it.effect.each(["shutdown", "close"] as const)( | ... | @@ -469,3 +470,104 @@ it.effect.each(["shutdown", "close"] as const)( |
| 469 | Effect.scoped, | 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 | ); |