Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@effect/vitest": "catalog:",
"@t3tools/contracts": "workspace:*",
"@t3tools/shared": "workspace:*",
"@t3tools/ssh": "workspace:*",
"@t3tools/tailscale": "workspace:*",
"@t3tools/web": "workspace:*",
"@types/bun": "1.3.14",
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.subscribePreviewEvents]: AuthOrchestrationReadScope,
[WS_METHODS.subscribeDiscoveredLocalServers]: AuthOrchestrationReadScope,
[WS_METHODS.deviceConfigure]: AuthOrchestrationOperateScope,
[WS_METHODS.deviceTestHost]: AuthOrchestrationOperateScope,
[WS_METHODS.deviceList]: AuthOrchestrationReadScope,
[WS_METHODS.deviceOpen]: AuthOrchestrationOperateScope,
[WS_METHODS.deviceClose]: AuthOrchestrationOperateScope,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/device/DeviceActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const makeReady = (
) => {
const calls: Call[] = [];
const ready: DeviceHostReady = {
nodePath: process.execPath,
hub: { origin: "http://127.0.0.1:1" },
helpers,
run: (command, args, options) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/device/DeviceActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ const serveSimPermissions = (
reason: "helper_missing",
});
yield* ready
.run(process.execPath, [
.run(ready.nodePath, [
cli,
"permissions",
input.decision,
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/device/DeviceHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ export interface DeviceHubEndpoint {
export interface AgentDeviceEndpoint {
readonly baseUrl: string;
readonly token: string;
/** Absolute path of the agent-device entry script for the provider PATH shim. */
/** Host-local path of the agent-device entry script. The provider uses a separate local CLI install. */
readonly entryPath: string;
}

export interface DeviceHostReady {
readonly nodePath: string;
readonly hub: DeviceHubEndpoint;
/**
* Runs a host command (`xcrun`, `adb`, or a helper bundled with the hub)
Expand Down
54 changes: 47 additions & 7 deletions apps/server/src/device/DeviceMultiHost.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect, it } from "@effect/vitest";
import { ThreadId } from "@t3tools/contracts";
import * as Deferred from "effect/Deferred";
import * as Fiber from "effect/Fiber";
import * as Effect from "effect/Effect";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
import { ServerSettingsService } from "../serverSettings.ts";
Expand All @@ -10,6 +12,7 @@ it.effect("keeps hosts independent when serials collide and another host fails",
Effect.gen(function* () {
const host = (id: string, failed = false): DeviceHost["Service"] => {
const ready = {
nodePath: process.execPath,
hub: { origin: `http://${id}` },
agentDevice: { baseUrl: `http://${id}`, token: "test", entryPath: "/agent-device" },
run: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }),
Expand All @@ -20,10 +23,10 @@ it.effect("keeps hosts independent when serials collide and another host fails",
summary: Effect.succeed({
id,
label: id,
kind: "local",
kind: id === "b" ? "ssh" : "local",
hubInstalled: true,
agentDeviceInstalled: true,
platforms: [{ platform: "android", available: true }],
platforms: id === "b" ? [] : [{ platform: "android", available: true }],
}),
platformAvailability: (platform) => Effect.succeed({ platform, available: true }),
ensureReady: () =>
Expand Down Expand Up @@ -59,9 +62,19 @@ it.effect("keeps hosts independent when serials collide and another host fails",
),
);
const hosts = new Map(["a", "b", "offline"].map((id) => [id, host(id, id === "offline")]));
const service = yield* makeWithHosts(hosts).pipe(
Effect.provideService(HttpClient.HttpClient, http),
);
const writeStarted = yield* Deferred.make<void>();
const finishWrite = yield* Deferred.make<void>();
const order: string[] = [];
const service = yield* makeWithHosts(hosts, undefined, () =>
Effect.gen(function* () {
order.push("write started");
yield* Deferred.succeed(writeStarted, undefined);
yield* Deferred.await(finishWrite);
order.push("write finished");
return "/host-config.json";
}),
).pipe(Effect.provideService(HttpClient.HttpClient, http));
expect(yield* service.agentReadinessIfSupported("b")).not.toBeNull();
const listed = yield* service.list;
expect(listed.devices.map((device) => device.hostId).sort()).toEqual(["a", "b"]);
expect(listed.hostStatuses.offline?.status).toBe("failed");
Expand All @@ -74,8 +87,35 @@ it.effect("keeps hosts independent when serials collide and another host fails",
expect(state.sessions.map((session) => session.hostId)).toEqual(["b"]);
expect(state.hostStatuses.a?.status).toBe("ready");
expect(state.hostStatuses.offline?.status).toBe("failed");
yield* service.agentReadinessIfSupported("b");
expect((yield* service.state).hostStatuses.b?.status).toBe("ready");
const targeting = yield* service
.agentTarget({ threadId, hostId: "b", deviceId: "emulator-5554" })
.pipe(Effect.forkChild);
yield* Deferred.await(writeStarted);
const replacing = yield* service
.withLifecycleLock(
Effect.gen(function* () {
order.push("replace");
hosts.set("b", host("b"));
yield* service.refreshHosts;
}),
)
.pipe(Effect.forkChild);
yield* Deferred.succeed(finishWrite, undefined);
yield* Fiber.join(targeting);
yield* Fiber.join(replacing);
expect(order).toEqual(["write started", "write finished", "replace"]);
const replaced = yield* service.state;
expect(replaced.sessions).toEqual([]);
expect(replaced.devices.map((device) => device.hostId)).toEqual(["a"]);
expect(replaced.hostStatuses.b).toBeUndefined();
yield* service.open({ threadId, hostId: "b", deviceId: "emulator-5554", platform: "android" });
hosts.delete("b");
yield* service.refreshHosts;
yield* service.setHostStatus("b", { status: "ready" });
expect((yield* service.state).hostStatuses.b).toBeUndefined();
expect((yield* service.state).sessions).toEqual([]);
yield* service.agentReadinessIfSupported("a");
expect((yield* service.state).hostStatuses.a?.status).toBe("ready");
yield* service.configure({ enabled: false });
expect((yield* service.state).hostStatuses).toEqual({});
}).pipe(
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/device/DeviceService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const fixture = Effect.fn("fixture")(function* (
let booted = false;
let shutDown = false;
const ready: DeviceHost.DeviceHostReady = {
nodePath: process.execPath,
hub: { origin: "http://device.test" },
helpers: { serveSimAxSettings: null, serveSimCli: null },
run: () => Effect.succeed({ code: 0, stdout: "Pixel_API_35\n", stderr: "" }),
Expand Down
Loading
Loading