1import { expect, it } from "@effect/vitest";
2import { ThreadId } from "@t3tools/contracts";
3import * as Deferred from "effect/Deferred";
4import * as Fiber from "effect/Fiber";
5import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
6import * as Effect from "effect/Effect";
7import { HttpClient, HttpClientResponse } from "effect/http";
8import * as ServerSettings from "../serverSettings.ts";
9import * as DeviceHost from "./DeviceHost.ts";
10import * as DeviceService from "./DeviceService.ts";
11
12it.effect("keeps hosts independent when serials collide and another host fails", () =>
13 Effect.gen(function* () {
14 const host = (id: string, failed = false): DeviceHost.DeviceHost["Service"] => {
15 const ready = {
16 nodePath: process.execPath,
17 hub: { origin: `http://${id}` },
18 agentDevice: { baseUrl: `http://${id}`, token: "test", entryPath: "/agent-device" },
19 run: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }),
20 helpers: { serveSimAxSettings: null, serveSimCli: null },
21 };
22 return {
23 id,
24 summary: Effect.succeed({
25 id,
26 label: id,
27 kind: id === "b" ? "ssh" : "local",
28 hubInstalled: true,
29 agentDeviceInstalled: true,
30 platforms: id === "b" ? [] : [{ platform: "android", available: true }],
31 }),
32 platformAvailability: (platform) => Effect.succeed({ platform, available: true }),
33 ensureReady: () =>
34 failed
35 ? Effect.fail(
36 new DeviceHost.DeviceHostError({
37 hostId: id,
38 step: "connect",
39 cause: new Error("offline"),
40 }),
41 )
42 : Effect.succeed(ready),
43 ensureAgentReady: () => Effect.succeed(ready),
44 current: Effect.succeed(ready),
45 stopAgent: Effect.void,
46 stop: Effect.void,
47 };
48 };
49 const http = HttpClient.make((request) =>
50 Effect.succeed(
51 HttpClientResponse.fromWeb(
52 request,
53 Response.json({
54 simulators: [],
55 emulators: [
56 {
57 id: "emulator-5554",
58 name: "Pixel",
59 version: "36",
60 platform: "android",
61 booted: true,
62 physical: false,
63 },
64 ],
65 }),
66 ),
67 ),
68 );
69 const hosts = new Map(["a", "b", "offline"].map((id) => [id, host(id, id === "offline")]));
70 const writeStarted = yield* Deferred.make<void>();
71 const finishWrite = yield* Deferred.make<void>();
72 const order: string[] = [];
73 const service = yield* DeviceService.makeWithHosts(hosts, undefined, () =>
74 Effect.gen(function* () {
75 order.push("write started");
76 yield* Deferred.succeed(writeStarted, undefined);
77 yield* Deferred.await(finishWrite);
78 order.push("write finished");
79 return "/host-config.json";
80 }),
81 ).pipe(Effect.provide(NodeCrypto.layer), Effect.provideService(HttpClient.HttpClient, http));
82 expect(yield* service.agentReadinessIfSupported("b")).not.toBeNull();
83 const listed = yield* service.list;
84 expect(listed.devices.map((device) => device.hostId).sort()).toEqual(["a", "b"]);
85 expect(listed.hostStatuses.offline?.status).toBe("failed");
86 const threadId = ThreadId.make("thread");
87 for (const hostId of ["a", "b"])
88 yield* service.open({ threadId, hostId, deviceId: "emulator-5554", platform: "android" });
89 yield* service.close({ threadId, hostId: "a", deviceId: "emulator-5554" });
90 const state = yield* service.state;
91 expect(state.devices).toHaveLength(2);
92 expect(state.sessions.map((session) => session.hostId)).toEqual(["b"]);
93 expect(state.hostStatuses.a?.status).toBe("ready");
94 expect(state.hostStatuses.offline?.status).toBe("failed");
95 const targeting = yield* service
96 .agentTarget({ threadId, hostId: "b", deviceId: "emulator-5554" })
97 .pipe(Effect.forkChild);
98 yield* Deferred.await(writeStarted);
99 const replacing = yield* service
100 .withLifecycleLock(
101 Effect.gen(function* () {
102 order.push("replace");
103 hosts.set("b", host("b"));
104 yield* service.refreshHosts;
105 }),
106 )
107 .pipe(Effect.forkChild);
108 yield* Deferred.succeed(finishWrite, undefined);
109 yield* Fiber.join(targeting);
110 yield* Fiber.join(replacing);
111 expect(order).toEqual(["write started", "write finished", "replace"]);
112 const replaced = yield* service.state;
113 expect(replaced.sessions).toEqual([]);
114 expect(replaced.devices.map((device) => device.hostId)).toEqual(["a"]);
115 expect(replaced.hostStatuses.b).toBeUndefined();
116 yield* service.open({ threadId, hostId: "b", deviceId: "emulator-5554", platform: "android" });
117 hosts.delete("b");
118 yield* service.refreshHosts;
119 yield* service.setHostStatus("b", { status: "ready" });
120 expect((yield* service.state).hostStatuses.b).toBeUndefined();
121 expect((yield* service.state).sessions).toEqual([]);
122 yield* service.agentReadinessIfSupported("a");
123 expect((yield* service.state).hostStatuses.a?.status).toBe("ready");
124 yield* service.configure({ enabled: false });
125 expect((yield* service.state).hostStatuses).toEqual({});
126 }).pipe(
127 Effect.provide(
128 ServerSettings.layerTest({
129 enableDeviceSupport: true,
130 enableAgentDeviceAccess: true,
131 }),
132 ),
133 ),
134);