import { DEFAULT_SERVER_SETTINGS, EnvironmentId, ServerConfig, type ServerConfig as ServerConfigType, WS_METHODS, } from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import * as TestClock from "effect/testing/TestClock"; import * as Socket from "effect/unstable/socket/Socket"; import { ConnectionTransientError, PrimaryConnectionTarget, type PreparedConnection, } from "../connection/model.ts"; import * as RpcSession from "./session.ts"; type SocketEventType = "open" | "message" | "close" | "error"; type SocketEvent = { readonly code?: number; readonly data?: unknown; readonly reason?: string; readonly type: SocketEventType; }; type SocketListener = (event: SocketEvent) => void; class TestWebSocket { static readonly CONNECTING = 0; static readonly OPEN = 1; static readonly CLOSING = 2; static readonly CLOSED = 3; readyState = TestWebSocket.CONNECTING; readonly sent: string[] = []; readonly url: string; private readonly listeners = new Map>(); constructor(url: string) { this.url = url; } addEventListener(type: SocketEventType, listener: SocketListener) { const listeners = this.listeners.get(type) ?? new Set(); listeners.add(listener); this.listeners.set(type, listeners); } removeEventListener(type: SocketEventType, listener: SocketListener) { this.listeners.get(type)?.delete(listener); } send(data: string) { this.sent.push(data); } close(code = 1000, reason = "") { if (this.readyState === TestWebSocket.CLOSED) { return; } this.readyState = TestWebSocket.CLOSED; this.emit("close", { code, reason, type: "close" }); } open() { this.readyState = TestWebSocket.OPEN; this.emit("open", { type: "open" }); } serverMessage(data: string) { this.emit("message", { data, type: "message" }); } private emit(type: SocketEventType, event: SocketEvent) { for (const listener of this.listeners.get(type) ?? []) { listener(event); } } } const TARGET = new PrimaryConnectionTarget({ environmentId: EnvironmentId.make("environment-1"), label: "Test environment", httpBaseUrl: "https://environment.example.test", wsBaseUrl: "wss://environment.example.test", }); const PREPARED: PreparedConnection = { environmentId: TARGET.environmentId, label: TARGET.label, httpBaseUrl: TARGET.httpBaseUrl, socketUrl: "wss://environment.example.test/ws?wsTicket=test", httpAuthorization: null, target: TARGET, }; const SERVER_CONFIG: ServerConfigType = { environment: { environmentId: TARGET.environmentId, label: TARGET.label, platform: { os: "darwin", arch: "arm64", }, serverVersion: "0.0.0-test", capabilities: { repositoryIdentity: true, connectionProbe: true, }, }, auth: { policy: "loopback-browser", bootstrapMethods: ["one-time-token"], sessionMethods: ["browser-session-cookie", "bearer-access-token"], sessionCookieName: "t3_session", }, cwd: "/tmp/workspace", keybindingsConfigPath: "/tmp/workspace/keybindings.json", keybindings: [], issues: [], providers: [], availableEditors: [], observability: { logsDirectoryPath: "/tmp/logs", localTracingEnabled: false, otlpTracesEnabled: false, otlpMetricsEnabled: false, }, settings: DEFAULT_SERVER_SETTINGS, }; const RpcRequest = Schema.TaggedStruct("Request", { id: Schema.String, payload: Schema.Unknown, tag: Schema.String, }); const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString); const decodeRpcRequest = Schema.decodeUnknownSync(RpcRequest); const encodeJson = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); const encodeServerConfig = Schema.encodeSync(ServerConfig); const ENCODED_SERVER_CONFIG = encodeServerConfig(SERVER_CONFIG); const LEGACY_SERVER_CONFIG = { ...ENCODED_SERVER_CONFIG, environment: { ...ENCODED_SERVER_CONFIG.environment, capabilities: { repositoryIdentity: true, }, }, }; const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* () { const sockets: TestWebSocket[] = []; const constructorLayer = Layer.succeed(Socket.WebSocketConstructor, (url) => { const socket = new TestWebSocket(url); sockets.push(socket); return socket as unknown as globalThis.WebSocket; }); const layer = RpcSession.layer.pipe(Layer.provide(constructorLayer)); const factory = yield* RpcSession.RpcSessionFactory.pipe(Effect.provide(layer)); return { factory, sockets }; }); const awaitSocket = Effect.fn("TestRpcSessionFactory.awaitSocket")(function* ( sockets: ReadonlyArray, ) { for (let attempt = 0; attempt < 100; attempt += 1) { const socket = sockets[0]; if (socket) { return socket; } yield* Effect.yieldNow; } return yield* Effect.die(new Error("Expected the RPC protocol to create a websocket.")); }); const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( socket: TestWebSocket, index = 0, ) { for (let attempt = 0; attempt < 100; attempt += 1) { const request = socket.sent[index]; if (request) { return decodeRpcRequest(decodeJson(request)); } yield* Effect.yieldNow; } return yield* Effect.die(new Error("Expected the RPC protocol to send a request.")); }); const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialConfig")(function* ( socket: TestWebSocket, config: unknown = ENCODED_SERVER_CONFIG, ) { const request = yield* awaitRequest(socket); expect(request).toMatchObject({ _tag: "Request", tag: WS_METHODS.serverGetConfig, payload: {}, }); socket.serverMessage( encodeJson({ _tag: "Exit", requestId: request.id, exit: { _tag: "Success", value: config, }, }), ); }); describe("RpcSessionFactory", () => { it.effect("owns one scoped websocket attempt and exposes readiness and closure", () => Effect.gen(function* () { const { factory, sockets } = yield* makeFactory(); const session = yield* factory.connect(PREPARED); const readyFiber = yield* Effect.forkChild(session.ready); const socket = yield* awaitSocket(sockets); expect(socket.url).toBe(PREPARED.socketUrl); socket.open(); yield* completeInitialConfig(socket); yield* Fiber.join(readyFiber); const config = yield* session.initialConfig; expect(config).toEqual(SERVER_CONFIG); expect(socket.sent).toHaveLength(1); const probeFiber = yield* Effect.forkChild(session.probe); const probeRequest = yield* awaitRequest(socket, 1); expect(probeRequest).toMatchObject({ _tag: "Request", tag: WS_METHODS.serverProbe, payload: {}, }); socket.serverMessage( encodeJson({ _tag: "Exit", requestId: probeRequest.id, exit: { _tag: "Success", value: {}, }, }), ); yield* Fiber.join(probeFiber); expect(socket.sent.map((request) => decodeRpcRequest(decodeJson(request)).tag)).toEqual([ WS_METHODS.serverGetConfig, WS_METHODS.serverProbe, ]); socket.close(1012, "service restart"); const error = yield* Effect.flip(session.closed); expect(error).toBeInstanceOf(ConnectionTransientError); expect(error).toMatchObject({ reason: "transport", message: "Test environment disconnected.", }); yield* Effect.yieldNow; expect(sockets).toHaveLength(1); }), ); it.effect("closes the websocket when the session scope is released", () => Effect.gen(function* () { const { factory, sockets } = yield* makeFactory(); yield* Effect.scoped( Effect.gen(function* () { const session = yield* factory.connect(PREPARED); const readyFiber = yield* Effect.forkChild(session.ready); const socket = yield* awaitSocket(sockets); socket.open(); yield* completeInitialConfig(socket); yield* Fiber.join(readyFiber); }), ); expect(sockets[0]?.readyState).toBe(TestWebSocket.CLOSED); }), ); it.effect("uses the legacy config RPC for probes when the server lacks the capability", () => Effect.scoped( Effect.gen(function* () { const { factory, sockets } = yield* makeFactory(); const session = yield* factory.connect(PREPARED); const readyFiber = yield* Effect.forkChild(session.ready); const socket = yield* awaitSocket(sockets); socket.open(); yield* completeInitialConfig(socket, LEGACY_SERVER_CONFIG); yield* Fiber.join(readyFiber); const probeFiber = yield* Effect.forkChild(session.probe); const probeRequest = yield* awaitRequest(socket, 1); expect(probeRequest).toMatchObject({ _tag: "Request", tag: WS_METHODS.serverGetConfig, payload: {}, }); socket.serverMessage( encodeJson({ _tag: "Exit", requestId: probeRequest.id, exit: { _tag: "Success", value: LEGACY_SERVER_CONFIG, }, }), ); yield* Fiber.join(probeFiber); expect(socket.sent.map((request) => decodeRpcRequest(decodeJson(request)).tag)).toEqual([ WS_METHODS.serverGetConfig, WS_METHODS.serverGetConfig, ]); }), ), ); it.effect("fails readiness when the websocket never opens", () => Effect.gen(function* () { const { factory, sockets } = yield* makeFactory(); const error = yield* Effect.scoped( Effect.gen(function* () { const session = yield* factory.connect(PREPARED); const readyFiber = yield* Effect.forkChild(Effect.flip(session.ready)); yield* awaitSocket(sockets); yield* TestClock.adjust("15 seconds"); return yield* Fiber.join(readyFiber); }), ); expect(error).toBeInstanceOf(ConnectionTransientError); expect(error).toMatchObject({ reason: "transport", message: "Test environment could not establish a WebSocket connection.", }); expect(sockets[0]?.readyState).toBe(TestWebSocket.CLOSED); }).pipe(Effect.provide(TestClock.layer())), ); });