import { describe, expect, it } from "@effect/vitest";import { DEFAULT_SERVER_SETTINGS, DeviceId, LOCAL_DEVICE_HOST_ID, ThreadId, type DeviceServiceState,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Deferred from "effect/Deferred";import * as Fiber from "effect/Fiber";import * as PubSub from "effect/PubSub";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import { HttpClient, HttpClientResponse } from "effect/unstable/http";import { ServerSettingsService } from "../serverSettings.ts";import * as DeviceHost from "./DeviceHost.ts"; import { type DeviceService, makeWithHosts, stateStream } from "./DeviceService.ts"; const baseState: DeviceServiceState = { hosts: [], hostStatus: "idle", hostStatuses: {}, devices: [], sessions: [], onboardingCompleted: false, agentAccessEnabled: false, hubBasePath: "/api/device-hub", revision: 0,}; describe("DeviceService.stateStream", () => { it.effect("emits the current snapshot and then every published change", () => Effect.gen(function* () { const pubsub = yield* PubSub.unbounded<DeviceServiceState>(); const current = yield* Ref.make(baseState); const service: Pick<DeviceService["Service"], "state" | "subscribe"> = { state: Ref.get(current), subscribe: PubSub.subscribe(pubsub), }; const collected = yield* stateStream(service as DeviceService["Service"]).pipe( Stream.take(3), Stream.runCollect, Effect.forkChild, ); yield* Effect.yieldNow; for (const revision of [1, 2]) { const next = { ...baseState, revision, hostStatus: "ready" as const }; yield* Ref.set(current, next); yield* PubSub.publish(pubsub, next); } const seen = yield* Fiber.join(collected); expect(seen.map((state) => state.revision)).toEqual([0, 1, 2]); }), );}); const fixture = Effect.fn("fixture")(function* ( onBoot: Effect.Effect<void> = Effect.void, bootError?: string, failListAfterShutdown = false,) { const settings = yield* Ref.make(DEFAULT_SERVER_SETTINGS); const starts: string[] = []; const agentStarts: string[] = []; const agentStops: string[] = []; const requests: string[] = []; 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: "" }), }; const host: DeviceHost.DeviceHost["Service"] = { id: LOCAL_DEVICE_HOST_ID, summary: Effect.succeed({ id: LOCAL_DEVICE_HOST_ID, kind: "local", label: "Test server", platforms: [{ platform: "android", available: true }], hubInstalled: true, agentDeviceInstalled: false, }), platformAvailability: (platform) => Effect.succeed({ platform, available: true }), ensureReady: (onPhase) => Effect.gen(function* () { starts.push("start"); yield* onPhase("starting"); return ready; }), ensureAgentReady: (onPhase) => Effect.gen(function* () { agentStarts.push("start"); yield* onPhase("starting"); return { ...ready, agentDevice: { baseUrl: "http://agent.test", token: "test", entryPath: "/agent" }, }; }), current: Effect.succeed(null), stopAgent: Effect.sync(() => { agentStops.push("stop"); }), stop: Effect.sync(() => { starts.push("stop"); }), }; const service = yield* makeWithHosts(new Map([[host.id, host]])).pipe( Effect.provideService(DeviceHost.DeviceHost, host), Effect.provideService( ServerSettingsService, ServerSettingsService.of({ start: Effect.void, ready: Effect.void, getSettings: Ref.get(settings), updateSettings: (patch) => Ref.updateAndGet(settings, (current) => ({ ...current, enableDeviceSupport: patch.enableDeviceSupport ?? current.enableDeviceSupport, enableAgentDeviceAccess: patch.enableAgentDeviceAccess ?? current.enableAgentDeviceAccess, deviceOnboardingCompleted: patch.deviceOnboardingCompleted ?? current.deviceOnboardingCompleted, })), streamChanges: Stream.empty, subscribeChanges: Effect.succeed(Stream.empty), }), ), Effect.provideService( HttpClient.HttpClient, HttpClient.make((request) => Effect.gen(function* () { requests.push(request.url); if (request.url.includes("/api/screenshot")) { return HttpClientResponse.fromWeb( request, new Response(new Uint8Array([137, 80, 78, 71])), ); } if (request.url.endsWith("/shutdown")) { shutDown = true; booted = false; return HttpClientResponse.fromWeb(request, Response.json({ ok: true })); } if (shutDown && failListAfterShutdown) { return HttpClientResponse.fromWeb( request, new Response("Discovery busy", { status: 503 }), ); } if (request.url.endsWith("/boot")) { yield* onBoot; booted = true; return HttpClientResponse.fromWeb( request, Response.json( bootError ? { ok: false, error: bootError } : { ok: true, serial: "emulator-5554" }, ), ); } return HttpClientResponse.fromWeb( request, Response.json({ simulators: [], emulators: booted ? [ { id: "emulator-5554", name: "Pixel_API_35", platform: "android", version: "Android 15", booted: true, physical: false, }, ] : [], }), ); }), ), ), ); return { service, starts, agentStarts, agentStops, requests, settings };}); describe("device setup consent", () => { it.effect("listing and provider startup do not start helpers before consent", () => Effect.gen(function* () { const { service, starts, requests } = yield* fixture(); expect((yield* service.list).hostStatus).toBe("disabled"); expect(yield* service.readinessIfSupported()).toBeNull(); const readiness = yield* service.readiness().pipe(Effect.result); expect(readiness._tag).toBe("Failure"); expect(starts).toEqual([]); expect(requests).toEqual([]); }).pipe(Effect.scoped), ); it.effect( "explicit setup discovers never-booted AVDs; disabling stops helpers and blocks agents", () => Effect.gen(function* () { const { service, starts, settings } = yield* fixture(); const state = yield* service.configure({ enabled: true }); expect((yield* Ref.get(settings)).enableDeviceSupport).toBe(true); expect(state.devices.map((device) => [device.id, device.booted])).toEqual([ ["Pixel_API_35", false], ]); expect(starts).toEqual(["start"]); const disabled = yield* service.configure({ enabled: false }); expect(disabled.hostStatus).toBe("disabled"); expect(disabled.devices).toEqual([]); expect((yield* Ref.get(settings)).enableDeviceSupport).toBe(false); expect(yield* service.readinessIfSupported()).toBeNull(); expect(starts).toEqual(["start", "stop"]); }).pipe(Effect.scoped), ); it.effect("boots a stopped Android AVD and uses its emulator serial without duplicating it", () => Effect.gen(function* () { const { service, requests } = yield* fixture(); yield* service.configure({ enabled: true }); const session = yield* service.open({ threadId: ThreadId.make("thread-1"), deviceId: "Pixel_API_35", platform: "android", }); expect(session.deviceId).toBe("emulator-5554"); expect(requests.filter((url) => url.endsWith("/boot"))).toHaveLength(1); const state = yield* service.state; expect(state.devices.map((device) => device.id)).toEqual(["emulator-5554"]); expect(state.bootingDevices).toEqual([]); }).pipe(Effect.scoped), ); it.effect("installs agent support only after the separate agent permission", () => Effect.gen(function* () { const { service, agentStarts, agentStops, settings } = yield* fixture(); yield* service.configure({ enabled: true }); expect(agentStarts).toEqual([]); expect(yield* service.agentReadinessIfSupported()).toBeNull(); yield* service.configure({ agentAccessEnabled: true }); expect(agentStarts).toEqual(["start"]); expect((yield* Ref.get(settings)).enableAgentDeviceAccess).toBe(true); expect((yield* service.state).agentAccessEnabled).toBe(true); yield* service.configure({ agentAccessEnabled: false, onboardingCompleted: true }); expect(agentStops).toEqual(["stop"]); expect((yield* service.state).onboardingCompleted).toBe(true); expect((yield* Ref.get(settings)).deviceOnboardingCompleted).toBe(true); }).pipe(Effect.scoped), );}); it.effect("publishes boot progress and does not restore sessions after support is disabled", () => Effect.gen(function* () { const started = yield* Deferred.make<void>(); const finish = yield* Deferred.make<void>(); const { service } = yield* fixture( Deferred.succeed(started, undefined).pipe(Effect.andThen(Deferred.await(finish))), ); yield* service.configure({ enabled: true }); const opening = yield* service .open({ threadId: ThreadId.make("thread-1"), deviceId: "Pixel_API_35", platform: "android" }) .pipe(Effect.result, Effect.forkChild); yield* Deferred.await(started); expect((yield* service.state).bootingDevices?.map((device) => device.name)).toEqual([ "Pixel_API_35", ]); yield* service.configure({ enabled: false }); yield* Deferred.succeed(finish, undefined); expect((yield* Fiber.join(opening))._tag).toBe("Failure"); const state = yield* service.state; expect(state.hostStatus).toBe("disabled"); expect(state.devices).toEqual([]); expect(state.sessions).toEqual([]); expect(state.bootingDevices).toEqual([]); }).pipe(Effect.scoped),); describe("device discovery after server restart", () => { it.effect("captures an explicit device before any client lists devices", () => Effect.gen(function* () { const { service, settings, requests } = yield* fixture(); yield* Ref.update(settings, (current) => ({ ...current, enableDeviceSupport: true })); expect((yield* service.state).devices).toEqual([]); const capture = yield* service.screenshot({ deviceId: DeviceId.make("Pixel_API_35") }); expect(capture.device.id).toBe("Pixel_API_35"); expect(Array.from(capture.png)).toEqual([137, 80, 78, 71]); expect(requests.some((url) => url.endsWith("/api/devices"))).toBe(true); }).pipe(Effect.scoped), );}); for (const [diagnostic, reason, message] of [ ["Insufficient disk space at /private/user/path", "disk_space", "not enough free disk space"], ["Timed out spawning /private/user/command", "timeout", "did not become ready in time"], ["Unexpected failure: secret-token", "launch_failed", "could not start"],] as const) { it.effect(`normalizes boot failure: ${reason}`, () => Effect.gen(function* () { const { service } = yield* fixture(Effect.void, diagnostic); yield* service.configure({ enabled: true }); const error = yield* service .open({ threadId: ThreadId.make("boot-failure"), deviceId: "Pixel_API_35", platform: "android", }) .pipe(Effect.flip); expect(error._tag).toBe("DeviceBootError"); expect(error.message).toContain(message); expect(error.message).not.toContain(diagnostic); expect((yield* service.state).bootingDevices).toEqual([]); }).pipe(Effect.scoped), );} it.effect("keeps shutdown successful when subsequent discovery fails", () => Effect.gen(function* () { const { service } = yield* fixture(Effect.void, undefined, true); yield* service.configure({ enabled: true }); const threadId = ThreadId.make("shutdown-refresh"); const session = yield* service.open({ threadId, deviceId: "Pixel_API_35", platform: "android", }); yield* service.close({ threadId, deviceId: session.deviceId, shutdown: true }); const state = yield* service.state; expect(state.sessions).toEqual([]); expect(state.devices.find((device) => device.id === session.deviceId)?.booted).toBe(false); }).pipe(Effect.scoped),);
import { describe, expect, it } from "@effect/vitest";
import {
DEFAULT_SERVER_SETTINGS,
DeviceId,
LOCAL_DEVICE_HOST_ID,
ThreadId,
type DeviceServiceState,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Deferred from "effect/Deferred";
import * as Fiber from "effect/Fiber";
import * as PubSub from "effect/PubSub";
import * as Ref from "effect/Ref";
import * as Stream from "effect/Stream";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
import { ServerSettingsService } from "../serverSettings.ts";
import * as DeviceHost from "./DeviceHost.ts";
import { type DeviceService, makeWithHosts, stateStream } from "./DeviceService.ts";
const baseState: DeviceServiceState = {
hosts: [],
hostStatus: "idle",
hostStatuses: {},
devices: [],
sessions: [],
onboardingCompleted: false,
agentAccessEnabled: false,
hubBasePath: "/api/device-hub",
revision: 0,
};
describe("DeviceService.stateStream", () => {
it.effect("emits the current snapshot and then every published change", () =>
Effect.gen(function* () {
const pubsub = yield* PubSub.unbounded<DeviceServiceState>();
const current = yield* Ref.make(baseState);
const service: Pick<DeviceService["Service"], "state" | "subscribe"> = {
state: Ref.get(current),
subscribe: PubSub.subscribe(pubsub),
};
const collected = yield* stateStream(service as DeviceService["Service"]).pipe(
Stream.take(3),
Stream.runCollect,
Effect.forkChild,
);
yield* Effect.yieldNow;
for (const revision of [1, 2]) {
const next = { ...baseState, revision, hostStatus: "ready" as const };
yield* Ref.set(current, next);
yield* PubSub.publish(pubsub, next);
}
const seen = yield* Fiber.join(collected);
expect(seen.map((state) => state.revision)).toEqual([0, 1, 2]);
}),
);
});
const fixture = Effect.fn("fixture")(function* (
onBoot: Effect.Effect<void> = Effect.void,
bootError?: string,
failListAfterShutdown = false,
) {
const settings = yield* Ref.make(DEFAULT_SERVER_SETTINGS);
const starts: string[] = [];
const agentStarts: string[] = [];
const agentStops: string[] = [];
const requests: string[] = [];
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: "" }),
};
const host: DeviceHost.DeviceHost["Service"] = {
id: LOCAL_DEVICE_HOST_ID,
summary: Effect.succeed({
id: LOCAL_DEVICE_HOST_ID,
kind: "local",
label: "Test server",
platforms: [{ platform: "android", available: true }],
hubInstalled: true,
agentDeviceInstalled: false,
}),
platformAvailability: (platform) => Effect.succeed({ platform, available: true }),
ensureReady: (onPhase) =>
Effect.gen(function* () {
starts.push("start");
yield* onPhase("starting");
return ready;
}),
ensureAgentReady: (onPhase) =>
Effect.gen(function* () {
agentStarts.push("start");
yield* onPhase("starting");
return {
...ready,
agentDevice: { baseUrl: "http://agent.test", token: "test", entryPath: "/agent" },
};
}),
current: Effect.succeed(null),
stopAgent: Effect.sync(() => {
agentStops.push("stop");
}),
stop: Effect.sync(() => {
starts.push("stop");
}),
};
const service = yield* makeWithHosts(new Map([[host.id, host]])).pipe(
Effect.provideService(DeviceHost.DeviceHost, host),
Effect.provideService(
ServerSettingsService,
ServerSettingsService.of({
start: Effect.void,
ready: Effect.void,
getSettings: Ref.get(settings),
updateSettings: (patch) =>
Ref.updateAndGet(settings, (current) => ({
...current,
enableDeviceSupport: patch.enableDeviceSupport ?? current.enableDeviceSupport,
enableAgentDeviceAccess:
patch.enableAgentDeviceAccess ?? current.enableAgentDeviceAccess,
deviceOnboardingCompleted:
patch.deviceOnboardingCompleted ?? current.deviceOnboardingCompleted,
})),
streamChanges: Stream.empty,
subscribeChanges: Effect.succeed(Stream.empty),
}),
),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) =>
Effect.gen(function* () {
requests.push(request.url);
if (request.url.includes("/api/screenshot")) {
return HttpClientResponse.fromWeb(
request,
new Response(new Uint8Array([137, 80, 78, 71])),
);
}
if (request.url.endsWith("/shutdown")) {
shutDown = true;
booted = false;
return HttpClientResponse.fromWeb(request, Response.json({ ok: true }));
}
if (shutDown && failListAfterShutdown) {
return HttpClientResponse.fromWeb(
request,
new Response("Discovery busy", { status: 503 }),
);
}
if (request.url.endsWith("/boot")) {
yield* onBoot;
booted = true;
return HttpClientResponse.fromWeb(
request,
Response.json(
bootError ? { ok: false, error: bootError } : { ok: true, serial: "emulator-5554" },
),
);
}
return HttpClientResponse.fromWeb(
request,
Response.json({
simulators: [],
emulators: booted
? [
{
id: "emulator-5554",
name: "Pixel_API_35",
platform: "android",
version: "Android 15",
booted: true,
physical: false,
},
]
: [],
}),
);
}),
),
),
);
return { service, starts, agentStarts, agentStops, requests, settings };
});
describe("device setup consent", () => {
it.effect("listing and provider startup do not start helpers before consent", () =>
Effect.gen(function* () {
const { service, starts, requests } = yield* fixture();
expect((yield* service.list).hostStatus).toBe("disabled");
expect(yield* service.readinessIfSupported()).toBeNull();
const readiness = yield* service.readiness().pipe(Effect.result);
expect(readiness._tag).toBe("Failure");
expect(starts).toEqual([]);
expect(requests).toEqual([]);
}).pipe(Effect.scoped),
);
it.effect(
"explicit setup discovers never-booted AVDs; disabling stops helpers and blocks agents",
() =>
Effect.gen(function* () {
const { service, starts, settings } = yield* fixture();
const state = yield* service.configure({ enabled: true });
expect((yield* Ref.get(settings)).enableDeviceSupport).toBe(true);
expect(state.devices.map((device) => [device.id, device.booted])).toEqual([
["Pixel_API_35", false],
]);
expect(starts).toEqual(["start"]);
const disabled = yield* service.configure({ enabled: false });
expect(disabled.hostStatus).toBe("disabled");
expect(disabled.devices).toEqual([]);
expect((yield* Ref.get(settings)).enableDeviceSupport).toBe(false);
expect(yield* service.readinessIfSupported()).toBeNull();
expect(starts).toEqual(["start", "stop"]);
}).pipe(Effect.scoped),
);
it.effect("boots a stopped Android AVD and uses its emulator serial without duplicating it", () =>
Effect.gen(function* () {
const { service, requests } = yield* fixture();
yield* service.configure({ enabled: true });
const session = yield* service.open({
threadId: ThreadId.make("thread-1"),
deviceId: "Pixel_API_35",
platform: "android",
});
expect(session.deviceId).toBe("emulator-5554");
expect(requests.filter((url) => url.endsWith("/boot"))).toHaveLength(1);
const state = yield* service.state;
expect(state.devices.map((device) => device.id)).toEqual(["emulator-5554"]);
expect(state.bootingDevices).toEqual([]);
}).pipe(Effect.scoped),
);
it.effect("installs agent support only after the separate agent permission", () =>
Effect.gen(function* () {
const { service, agentStarts, agentStops, settings } = yield* fixture();
yield* service.configure({ enabled: true });
expect(agentStarts).toEqual([]);
expect(yield* service.agentReadinessIfSupported()).toBeNull();
yield* service.configure({ agentAccessEnabled: true });
expect(agentStarts).toEqual(["start"]);
expect((yield* Ref.get(settings)).enableAgentDeviceAccess).toBe(true);
expect((yield* service.state).agentAccessEnabled).toBe(true);
yield* service.configure({ agentAccessEnabled: false, onboardingCompleted: true });
expect(agentStops).toEqual(["stop"]);
expect((yield* service.state).onboardingCompleted).toBe(true);
expect((yield* Ref.get(settings)).deviceOnboardingCompleted).toBe(true);
}).pipe(Effect.scoped),
);
});
it.effect("publishes boot progress and does not restore sessions after support is disabled", () =>
Effect.gen(function* () {
const started = yield* Deferred.make<void>();
const finish = yield* Deferred.make<void>();
const { service } = yield* fixture(
Deferred.succeed(started, undefined).pipe(Effect.andThen(Deferred.await(finish))),
);
yield* service.configure({ enabled: true });
const opening = yield* service
.open({ threadId: ThreadId.make("thread-1"), deviceId: "Pixel_API_35", platform: "android" })
.pipe(Effect.result, Effect.forkChild);
yield* Deferred.await(started);
expect((yield* service.state).bootingDevices?.map((device) => device.name)).toEqual([
"Pixel_API_35",
]);
yield* service.configure({ enabled: false });
yield* Deferred.succeed(finish, undefined);
expect((yield* Fiber.join(opening))._tag).toBe("Failure");
const state = yield* service.state;
expect(state.hostStatus).toBe("disabled");
expect(state.devices).toEqual([]);
expect(state.sessions).toEqual([]);
expect(state.bootingDevices).toEqual([]);
}).pipe(Effect.scoped),
);
describe("device discovery after server restart", () => {
it.effect("captures an explicit device before any client lists devices", () =>
Effect.gen(function* () {
const { service, settings, requests } = yield* fixture();
yield* Ref.update(settings, (current) => ({ ...current, enableDeviceSupport: true }));
expect((yield* service.state).devices).toEqual([]);
const capture = yield* service.screenshot({ deviceId: DeviceId.make("Pixel_API_35") });
expect(capture.device.id).toBe("Pixel_API_35");
expect(Array.from(capture.png)).toEqual([137, 80, 78, 71]);
expect(requests.some((url) => url.endsWith("/api/devices"))).toBe(true);
}).pipe(Effect.scoped),
);
});
for (const [diagnostic, reason, message] of [
["Insufficient disk space at /private/user/path", "disk_space", "not enough free disk space"],
["Timed out spawning /private/user/command", "timeout", "did not become ready in time"],
["Unexpected failure: secret-token", "launch_failed", "could not start"],
] as const) {
it.effect(`normalizes boot failure: ${reason}`, () =>
Effect.gen(function* () {
const { service } = yield* fixture(Effect.void, diagnostic);
yield* service.configure({ enabled: true });
const error = yield* service
.open({
threadId: ThreadId.make("boot-failure"),
deviceId: "Pixel_API_35",
platform: "android",
})
.pipe(Effect.flip);
expect(error._tag).toBe("DeviceBootError");
expect(error.message).toContain(message);
expect(error.message).not.toContain(diagnostic);
expect((yield* service.state).bootingDevices).toEqual([]);
}).pipe(Effect.scoped),
);
}
it.effect("keeps shutdown successful when subsequent discovery fails", () =>
Effect.gen(function* () {
const { service } = yield* fixture(Effect.void, undefined, true);
yield* service.configure({ enabled: true });
const threadId = ThreadId.make("shutdown-refresh");
const session = yield* service.open({
threadId,
deviceId: "Pixel_API_35",
platform: "android",
});
yield* service.close({ threadId, deviceId: session.deviceId, shutdown: true });
const state = yield* service.state;
expect(state.sessions).toEqual([]);
expect(state.devices.find((device) => device.id === session.deviceId)?.booted).toBe(false);
}).pipe(Effect.scoped),
);