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