1import * as RelayClient from "@t3tools/shared/relayClient";
2import { assert, it } from "@effect/vitest";
3import * as Cause from "effect/Cause";
4import * as ConfigProvider from "effect/ConfigProvider";
5import * as Console from "effect/Console";
6import * as Effect from "effect/Effect";
7import * as Exit from "effect/Exit";
8import * as Logger from "effect/Logger";
9import * as Option from "effect/Option";
10import * as References from "effect/References";
11import * as Terminal from "effect/Terminal";
12
13import * as BootService from "../cloud/bootService.ts";
14import {
15 acquireRelayClientForLink,
16 headlessSessionConfig,
17 reportCloudDisconnectResults,
18} from "./connect.ts";
19import { recoverServiceOnboardingOffer } from "./service.ts";
20
21const readHeadlessSessionConfig = (env: Record<string, string>) =>
22 headlessSessionConfig.pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env }))));
23
24const managedExecutable = {
25 status: "available",
26 executablePath: "/tmp/cloudflared",
27 source: "managed",
28 version: RelayClient.CLOUDFLARED_VERSION,
29} as const;
30
31it.effect("detects headless operation from individual SSH config values", () =>
32 Effect.gen(function* () {
33 assert.isFalse(yield* readHeadlessSessionConfig({}));
34 assert.isFalse(yield* readHeadlessSessionConfig({ CI: "true" }));
35 assert.isTrue(yield* readHeadlessSessionConfig({ SSH_CONNECTION: "client server" }));
36 assert.isTrue(yield* readHeadlessSessionConfig({ SSH_TTY: "/dev/pts/1" }));
37 }),
38);
39
40it.effect("treats cancelling optional background setup as a successful skip", () =>
41 Effect.gen(function* () {
42 const result = yield* recoverServiceOnboardingOffer(Effect.fail(new Terminal.QuitError({})));
43 assert.isFalse(result);
44 }),
45);
46
47it.effect("keeps a successful connection when a remote service update is pending", () =>
48 Effect.gen(function* () {
49 const result = yield* recoverServiceOnboardingOffer(
50 Effect.fail(new BootService.BootServiceUpdatePendingError()),
51 );
52 assert.isFalse(result);
53 }),
54);
55
56it.effect("does not install the relay client when the user declines the managed download", () =>
57 Effect.gen(function* () {
58 let installCalls = 0;
59 const result = yield* acquireRelayClientForLink(
60 {
61 resolve: Effect.succeed({
62 status: "missing",
63 version: RelayClient.CLOUDFLARED_VERSION,
64 }),
65 install: Effect.sync(() => {
66 installCalls += 1;
67 return managedExecutable;
68 }),
69 installWithProgress: () =>
70 Effect.sync(() => {
71 installCalls += 1;
72 return managedExecutable;
73 }),
74 pruneManagedVersions: Effect.void,
75 },
76 () => Effect.succeed(false),
77 () => Effect.void,
78 );
79
80 assert.isTrue(Option.isNone(result));
81 assert.equal(installCalls, 0);
82 }),
83);
84
85it.effect("installs the relay client after the user accepts the managed download", () =>
86 Effect.gen(function* () {
87 let installCalls = 0;
88 const progress: Array<string> = [];
89 const result = yield* acquireRelayClientForLink(
90 {
91 resolve: Effect.succeed({
92 status: "missing",
93 version: RelayClient.CLOUDFLARED_VERSION,
94 }),
95 install: Effect.sync(() => {
96 installCalls += 1;
97 return managedExecutable;
98 }),
99 installWithProgress: (report) =>
100 report({ type: "progress", stage: "downloading" }).pipe(
101 Effect.andThen(
102 Effect.sync(() => {
103 installCalls += 1;
104 return managedExecutable;
105 }),
106 ),
107 ),
108 pruneManagedVersions: Effect.void,
109 },
110 () => Effect.succeed(true),
111 (event) =>
112 Effect.sync(() => {
113 if (event.type === "progress") {
114 progress.push(event.stage);
115 }
116 }),
117 );
118
119 assert.deepEqual(Option.getOrThrow(result), managedExecutable);
120 assert.equal(installCalls, 1);
121 assert.deepEqual(progress, ["downloading"]);
122 }),
123);
124
125it.effect("reuses an available relay client executable without prompting", () =>
126 Effect.gen(function* () {
127 let promptCalls = 0;
128 const result = yield* acquireRelayClientForLink(
129 {
130 resolve: Effect.succeed(managedExecutable),
131 install: Effect.die("unexpected install"),
132 installWithProgress: () => Effect.die("unexpected install"),
133 pruneManagedVersions: Effect.void,
134 },
135 () =>
136 Effect.sync(() => {
137 promptCalls += 1;
138 return false;
139 }),
140 () => Effect.void,
141 );
142
143 assert.deepEqual(Option.getOrThrow(result), managedExecutable);
144 assert.equal(promptCalls, 0);
145 }),
146);
147
148it.effect("keeps disconnect causes in structured logs and out of console warnings", () => {
149 const warnings: ReadonlyArray<unknown>[] = [];
150 const logs: Readonly<Record<string, unknown>>[] = [];
151 const testConsole = {
152 ...globalThis.console,
153 warn: (...args: ReadonlyArray<unknown>) => {
154 warnings.push(args);
155 },
156 } satisfies Console.Console;
157 const logger = Logger.make(({ fiber }) => {
158 logs.push(fiber.getRef(References.CurrentLogAnnotations));
159 });
160 const liveFailure = "live unlink private diagnostic";
161 const relayFailure = "relay revoke private diagnostic";
162
163 return reportCloudDisconnectResults({
164 clearAuthorization: true,
165 liveResult: {
166 status: "failed",
167 cause: Cause.fail(new Error(liveFailure)),
168 },
169 relayResult: Exit.failCause(Cause.die(new Error(relayFailure))),
170 }).pipe(
171 Effect.provideService(Console.Console, testConsole),
172 Effect.provide(Logger.layer([logger], { mergeWithExisting: false })),
173 Effect.tap(() =>
174 Effect.sync(() => {
175 assert.lengthOf(warnings, 2);
176 const warningText = warnings.flat().map(String).join("\n");
177 assert.include(warningText, "running server could not stop its tunnel");
178 assert.include(warningText, "Could not revoke the relay-side environment record");
179 assert.notInclude(warningText, liveFailure);
180 assert.notInclude(warningText, relayFailure);
181 assert.deepEqual(
182 logs.map(({ operation, clearAuthorization }) => ({ operation, clearAuthorization })),
183 [
184 { operation: "live-server-unlink", clearAuthorization: true },
185 { operation: "relay-environment-unlink", clearAuthorization: true },
186 ],
187 );
188 const loggedCauses = logs.map((log) => String(log.cause)).join("\n");
189 assert.include(loggedCauses, liveFailure);
190 assert.include(loggedCauses, relayFailure);
191 }),
192 ),
193 );
194});