1import { assert, describe, it } from "@effect/vitest";
2import { SshHttpBridgeError } from "@t3tools/ssh/errors";
3import * as Cause from "effect/Cause";
4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
6import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";
8import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
9
10import {
11 DesktopSshEnvironmentRequestError,
12 fetchSshEnvironmentDescriptor,
13} from "./sshEnvironment.ts";
14
15function jsonResponse(request: HttpClientRequest.HttpClientRequest, body: unknown, status = 200) {
16 return HttpClientResponse.fromWeb(
17 request,
18 new Response(JSON.stringify(body), {
19 status,
20 headers: { "content-type": "application/json" },
21 }),
22 );
23}
24
25function layerHttpClient(
26 handler: (
27 request: HttpClientRequest.HttpClientRequest,
28 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, never>,
29) {
30 return Layer.succeed(
31 HttpClient.HttpClient,
32 HttpClient.make((request) => handler(request)),
33 );
34}
35
36describe("SSH environment IPC", () => {
37 it.effect("fetches and decodes the remote environment descriptor", () => {
38 const requestUrls: string[] = [];
39 const layer = layerHttpClient((request) =>
40 Effect.sync(() => {
41 requestUrls.push(request.url);
42 return jsonResponse(request, {
43 environmentId: "remote-env",
44 label: "Remote Devbox",
45 platform: { os: "linux", arch: "x64" },
46 serverVersion: "1.2.3",
47 capabilities: { repositoryIdentity: true },
48 });
49 }),
50 );
51
52 return Effect.gen(function* () {
53 const descriptor = yield* fetchSshEnvironmentDescriptor.handler({
54 httpBaseUrl: "http://127.0.0.1:41773/",
55 });
56
57 assert.deepEqual(descriptor, {
58 environmentId: "remote-env",
59 label: "Remote Devbox",
60 platform: { os: "linux", arch: "x64" },
61 serverVersion: "1.2.3",
62 capabilities: { repositoryIdentity: true },
63 });
64 assert.deepEqual(requestUrls, ["http://127.0.0.1:41773/.well-known/t3/environment"]);
65 }).pipe(Effect.provide(layer));
66 });
67
68 it.effect("wraps schema decode failures in a typed request error", () => {
69 const layer = layerHttpClient((request) =>
70 Effect.succeed(jsonResponse(request, { environmentId: "remote-env" })),
71 );
72
73 return Effect.gen(function* () {
74 const exit = yield* Effect.exit(
75 fetchSshEnvironmentDescriptor.handler({
76 httpBaseUrl: "http://127.0.0.1:41773/",
77 }),
78 );
79 assert(Exit.isFailure(exit));
80 const failure = Cause.findErrorOption(exit.cause);
81 assert(Option.isSome(failure));
82 const error = failure.value;
83
84 assert.instanceOf(error, DesktopSshEnvironmentRequestError);
85 assert.equal(error.operation, "fetch-environment-descriptor");
86 assert.equal(error.cause instanceof SshHttpBridgeError, false);
87 }).pipe(Effect.provide(layer));
88 });
89
90 it.effect("rejects non-loopback HTTP endpoints before issuing a request", () => {
91 let requestCount = 0;
92 const layer = layerHttpClient((request) =>
93 Effect.sync(() => {
94 requestCount += 1;
95 return jsonResponse(request, {});
96 }),
97 );
98
99 return Effect.gen(function* () {
100 const exit = yield* Effect.exit(
101 fetchSshEnvironmentDescriptor.handler({
102 httpBaseUrl: "http://remote.example.com:41773/",
103 }),
104 );
105 assert(Exit.isFailure(exit));
106 const failure = Cause.findErrorOption(exit.cause);
107 assert(Option.isSome(failure));
108 const error = failure.value;
109
110 assert.instanceOf(error, DesktopSshEnvironmentRequestError);
111 assert.instanceOf(error.cause, SshHttpBridgeError);
112 assert.equal(requestCount, 0);
113 }).pipe(Effect.provide(layer));
114 });
115});