1import { sha256 } from "@noble/hashes/sha2";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { describe, expect, it } from "@effect/vitest";
4import * as ConfigProvider from "effect/ConfigProvider";
5import * as Deferred from "effect/Deferred";
6import * as Effect from "effect/Effect";
7import * as Fiber from "effect/Fiber";
8import * as Hex from "effect/encoding/Hex";
9import * as FileSystem from "effect/FileSystem";
10import * as Layer from "effect/Layer";
11import * as PlatformError from "effect/PlatformError";
12import * as Sink from "effect/Sink";
13import * as Stream from "effect/Stream";
14import * as TestClock from "effect/testing/TestClock";
15import { HttpClient, HttpClientResponse } from "effect/http";
16import { ChildProcess, ChildProcessSpawner } from "effect/process";
17import * as HostProcess from "./HostProcess.ts";
18
19import * as RelayClient from "./relayClient.ts";
20
21// The suite runs the linux code path against the real filesystem, checking
22// POSIX exec bits that NTFS never reports; the win32 branch skips that check.
23const windowsHost = HostProcess.Platform.defaultValue() === "win32";
24
25const layerHostRuntime = (env: Record<string, string> = {}, platform: NodeJS.Platform = "linux") =>
26 Layer.mergeAll(
27 Layer.succeed(HostProcess.Platform, platform),
28 Layer.succeed(HostProcess.Architecture, "x64"),
29 ConfigProvider.layer(ConfigProvider.fromEnv({ env })),
30 );
31
32function makeHandle(exitCode = 0, output = "") {
33 const stdout = output ? Stream.make(new TextEncoder().encode(output)) : Stream.empty;
34 return ChildProcessSpawner.makeHandle({
35 pid: ChildProcessSpawner.ProcessId(100),
36 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
37 isRunning: Effect.succeed(false),
38 kill: () => Effect.void,
39 unref: Effect.succeed(Effect.void),
40 stdin: Sink.drain,
41 stdout,
42 stderr: Stream.empty,
43 all: stdout,
44 getInputFd: () => Sink.drain,
45 getOutputFd: () => Stream.empty,
46 });
47}
48
49const layerHttpClient = (bytes: Uint8Array) =>
50 Layer.succeed(
51 HttpClient.HttpClient,
52 HttpClient.make((request) =>
53 Effect.succeed(
54 HttpClientResponse.fromWeb(request, new Response(bytes.buffer as ArrayBuffer)),
55 ),
56 ),
57 );
58
59// Records each request and never responds, simulating a wedged endpoint.
60const layerStalledHttpClient = (requests: Array<unknown>) =>
61 Layer.succeed(
62 HttpClient.HttpClient,
63 HttpClient.make((request) => {
64 requests.push(request);
65 return Effect.never;
66 }),
67 );
68
69// Answers `cloudflared version` with the version recorded for that path, which
70// defaults to the pinned release; tests change it to simulate other binaries.
71const layerSpawner = (commands: Array<string>, versions: Record<string, string> = {}) =>
72 Layer.succeed(
73 ChildProcessSpawner.ChildProcessSpawner,
74 ChildProcessSpawner.make((command) =>
75 Effect.sync(() => {
76 if (!ChildProcess.isStandardCommand(command)) {
77 commands.push("piped-command");
78 return makeHandle();
79 }
80 commands.push(command.command);
81 if (command.args[0] === "version") {
82 const version = versions[command.command] ?? RelayClient.CLOUDFLARED_VERSION;
83 return makeHandle(0, `cloudflared version ${version} (built 2026-01-01-00:00 UTC)\n`);
84 }
85 return makeHandle();
86 }),
87 ),
88 );
89
90const writeExecutable = (filePath: string, contents = "cloudflared") =>
91 Effect.gen(function* () {
92 const fileSystem = yield* FileSystem.FileSystem;
93 yield* fileSystem.makeDirectory(filePath.slice(0, filePath.lastIndexOf("/")), {
94 recursive: true,
95 });
96 yield* fileSystem.writeFileString(filePath, contents);
97 yield* fileSystem.chmod(filePath, 0o755);
98 });
99
100type RelayClientTestServices =
101 | Layer.Success<ReturnType<typeof layerHostRuntime> | typeof NodeServices.layer>
102 | HttpClient.HttpClient
103 | ChildProcessSpawner.ChildProcessSpawner;
104
105const managedPathFor = (baseDir: string, version: string) =>
106 `${baseDir}/tools/cloudflared/${version}/linux-x64/cloudflared`;
107
108const renameError = (code: string, path: string) =>
109 PlatformError.systemError({
110 _tag: code === "EBUSY" ? "Busy" : code === "EACCES" ? "PermissionDenied" : "Unknown",
111 module: "FileSystem",
112 method: "rename",
113 pathOrDescriptor: path,
114 cause: Object.assign(new Error(code), { code }),
115 });
116
117type InstallRename = "staging" | "activation";
118
119/**
120 * Fails the first `failures` staging or activation renames with `code`, as Windows does while
121 * another process holds the file. Staging moves the binary to a `.tmp` name beside its final path.
122 */
123const makeLockedRenames = (
124 fileSystem: FileSystem.FileSystem,
125 input: { readonly rename: InstallRename; readonly failures: number; readonly code: string },
126) =>
127 Effect.gen(function* () {
128 const firstAttempt = yield* Deferred.make<void>();
129 const renames = { attempts: 0 };
130 const locked = FileSystem.make({
131 ...fileSystem,
132 rename: (from, to) =>
133 Effect.suspend(() => {
134 if ((to.endsWith(".tmp") ? "staging" : "activation") !== input.rename) {
135 return fileSystem.rename(from, to);
136 }
137 renames.attempts += 1;
138 return renames.attempts <= input.failures
139 ? Deferred.succeed(firstAttempt, undefined).pipe(
140 Effect.andThen(Effect.fail(renameError(input.code, to))),
141 )
142 : fileSystem.rename(from, to);
143 }),
144 });
145 return { locked, renames, firstAttempt };
146 });
147
148const testBinary = new TextEncoder().encode("test-cloudflared-binary");
149const testReleaseAsset = {
150 url: "https://example.test/cloudflared",
151 sha256: Hex.encode(sha256(testBinary)),
152 archive: "binary",
153} as const;
154
155const layerInstallRuntime = (platform: NodeJS.Platform) =>
156 Layer.mergeAll(
157 NodeServices.layer,
158 layerHttpClient(testBinary),
159 layerSpawner([]),
160 layerHostRuntime({ PATH: "" }, platform),
161 );
162
163const managedDirectory = (baseDir: string, platform: NodeJS.Platform) =>
164 `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/${platform}-x64`;
165
166describe("RelayClient", () => {
167 it.effect("cancels a contended install without removing the other installer's lock", () =>
168 Effect.gen(function* () {
169 const fileSystem = yield* FileSystem.FileSystem;
170 const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" });
171 const directory = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64`;
172 const lockPath = `${directory}/cloudflared.lock`;
173 yield* fileSystem.makeDirectory(directory, { recursive: true });
174 yield* fileSystem.writeFileString(lockPath, "other-installer");
175 const contended = yield* Deferred.make<void>();
176 const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir }).pipe(
177 Effect.provideService(FileSystem.FileSystem, {
178 ...fileSystem,
179 writeFileString: (path, contents, options) =>
180 fileSystem
181 .writeFileString(path, contents, options)
182 .pipe(
183 Effect.tapError(() =>
184 path === lockPath ? Deferred.succeed(contended, undefined) : Effect.void,
185 ),
186 ),
187 }),
188 );
189 const installing = yield* manager.install.pipe(Effect.forkChild);
190 yield* Deferred.await(contended);
191 yield* Fiber.interrupt(installing);
192 expect(yield* fileSystem.readFileString(lockPath)).toBe("other-installer");
193 }).pipe(
194 Effect.scoped,
195 Effect.provide(
196 Layer.mergeAll(
197 NodeServices.layer,
198 layerHttpClient(new Uint8Array()),
199 layerSpawner([]),
200 layerHostRuntime({ PATH: "" }),
201 ),
202 ),
203 ),
204 );
205
206 it.effect("releases the install lock when a download is cancelled", () =>
207 Effect.gen(function* () {
208 const fileSystem = yield* FileSystem.FileSystem;
209 const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" });
210 const lockPath = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`;
211 const downloading = yield* Deferred.make<void>();
212 const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir });
213 const installing = yield* manager
214 .installWithProgress((event) =>
215 event.type === "progress" && event.stage === "downloading"
216 ? Deferred.succeed(downloading, undefined).pipe(Effect.andThen(Effect.never))
217 : Effect.void,
218 )
219 .pipe(Effect.forkChild);
220 yield* Deferred.await(downloading);
221 expect(yield* fileSystem.exists(lockPath)).toBe(true);
222 yield* Fiber.interrupt(installing);
223 expect(yield* fileSystem.exists(lockPath)).toBe(false);
224 }).pipe(
225 Effect.scoped,
226 Effect.provide(
227 Layer.mergeAll(
228 NodeServices.layer,
229 layerHttpClient(new Uint8Array()),
230 layerSpawner([]),
231 layerHostRuntime({ PATH: "" }),
232 ),
233 ),
234 ),
235 );
236
237 it.effect.skipIf(windowsHost)("releases a lock acquired while installation is cancelled", () =>
238 Effect.gen(function* () {
239 const fileSystem = yield* FileSystem.FileSystem;
240 const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" });
241 const lockPath = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`;
242 const acquired = yield* Deferred.make<void>();
243 const completeWrite = yield* Deferred.make<void>();
244 let pauseWrite = true;
245 const manager = yield* RelayClient.makeCloudflaredRelayClient({
246 baseDir,
247 releaseAsset: {
248 url: "https://example.test/cloudflared",
249 sha256: Hex.encode(sha256(new TextEncoder().encode("test-binary"))),
250 archive: "binary",
251 },
252 }).pipe(
253 Effect.provideService(FileSystem.FileSystem, {
254 ...fileSystem,
255 writeFileString: (path, contents, options) =>
256 fileSystem
257 .writeFileString(path, contents, options)
258 .pipe(
259 Effect.tap(() =>
260 path === lockPath && pauseWrite
261 ? Deferred.succeed(acquired, undefined).pipe(
262 Effect.andThen(Deferred.await(completeWrite)),
263 )
264 : Effect.void,
265 ),
266 ),
267 }),
268 );
269
270 const installing = yield* manager.install.pipe(Effect.forkChild);
271 yield* Deferred.await(acquired);
272 const cancelling = yield* Fiber.interrupt(installing).pipe(
273 Effect.forkChild({ startImmediately: true }),
274 );
275 yield* Deferred.succeed(completeWrite, undefined);
276 yield* Fiber.join(cancelling);
277 expect(yield* fileSystem.exists(lockPath)).toBe(false);
278
279 pauseWrite = false;
280 expect(yield* manager.install).toMatchObject({ status: "available" });
281 expect(yield* fileSystem.exists(lockPath)).toBe(false);
282 }).pipe(
283 Effect.scoped,
284 Effect.provide(
285 Layer.mergeAll(
286 NodeServices.layer,
287 layerHttpClient(new TextEncoder().encode("test-binary")),
288 layerSpawner([]),
289 layerHostRuntime({ PATH: "" }),
290 ),
291 ),
292 ),
293 );
294 it.effect.skipIf(windowsHost)(
295 "resolves explicit overrides before managed and PATH executables",
296 () =>
297 Effect.gen(function* () {
298 const fileSystem = yield* FileSystem.FileSystem;
299 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
300 prefix: "t3-cloudflared-test-",
301 });
302 const overridePath = `${baseDir}/override-cloudflared`;
303 yield* fileSystem.writeFileString(overridePath, "override");
304 yield* fileSystem.chmod(overridePath, 0o755);
305 const manager = yield* RelayClient.makeCloudflaredRelayClient({
306 baseDir,
307 });
308
309 expect(
310 yield* manager.resolve.pipe(
311 Effect.provideService(
312 ConfigProvider.ConfigProvider,
313 ConfigProvider.fromEnv({
314 env: { PATH: "", T3CODE_CLOUDFLARED_PATH: overridePath },
315 }),
316 ),
317 ),
318 ).toEqual({
319 status: "available",
320 executablePath: overridePath,
321 source: "override",
322 version: RelayClient.CLOUDFLARED_VERSION,
323 });
324 }).pipe(
325 Effect.scoped,
326 Effect.provide(
327 Layer.mergeAll(
328 NodeServices.layer,
329 layerHttpClient(new Uint8Array()),
330 layerSpawner([]),
331 layerHostRuntime(),
332 ),
333 ),
334 ),
335 );
336
337 it.effect.skipIf(windowsHost)(
338 "downloads, verifies, validates, and atomically installs the managed executable",
339 () =>
340 Effect.gen(function* () {
341 const fileSystem = yield* FileSystem.FileSystem;
342 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
343 prefix: "t3-cloudflared-test-",
344 });
345 const bytes = new TextEncoder().encode("test-cloudflared-binary");
346 const manager = yield* RelayClient.makeCloudflaredRelayClient({
347 baseDir,
348 releaseAsset: {
349 url: "https://example.test/cloudflared",
350 sha256: Hex.encode(sha256(bytes)),
351 archive: "binary",
352 },
353 });
354
355 const progress: Array<string> = [];
356 const installed = yield* manager.installWithProgress((event) =>
357 Effect.sync(() => {
358 if (event.type === "progress") {
359 progress.push(event.stage);
360 }
361 }),
362 );
363 const managedPath = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared`;
364 expect(installed).toEqual({
365 status: "available",
366 executablePath: managedPath,
367 source: "managed",
368 version: RelayClient.CLOUDFLARED_VERSION,
369 });
370 expect(new TextDecoder().decode(yield* fileSystem.readFile(managedPath))).toBe(
371 "test-cloudflared-binary",
372 );
373 expect(progress).toEqual([
374 "checking",
375 "waiting_for_lock",
376 "downloading",
377 "verifying",
378 "installing",
379 "validating",
380 "activating",
381 ]);
382 expect(yield* manager.resolve).toEqual(installed);
383 }).pipe(
384 Effect.scoped,
385 Effect.provide(
386 Layer.mergeAll(
387 NodeServices.layer,
388 layerHttpClient(new TextEncoder().encode("test-cloudflared-binary")),
389 layerSpawner([]),
390 layerHostRuntime(),
391 ),
392 ),
393 ),
394 );
395
396 it.effect("rejects downloads whose checksum does not match the pinned manifest", () =>
397 Effect.gen(function* () {
398 const fileSystem = yield* FileSystem.FileSystem;
399 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
400 prefix: "t3-cloudflared-test-",
401 });
402 const manager = yield* RelayClient.makeCloudflaredRelayClient({
403 baseDir,
404 releaseAsset: {
405 url: "https://example.test/cloudflared",
406 sha256: Hex.encode(sha256(new TextEncoder().encode("expected"))),
407 archive: "binary",
408 },
409 });
410
411 const error = yield* manager.install.pipe(Effect.flip);
412 expect(error).toBeInstanceOf(RelayClient.RelayClientInstallError);
413 expect(error.reason).toBe("invalid_checksum");
414 expect(
415 yield* fileSystem.exists(
416 `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`,
417 ),
418 ).toBe(false);
419 }).pipe(
420 Effect.scoped,
421 Effect.provide(
422 Layer.mergeAll(
423 NodeServices.layer,
424 layerHttpClient(new TextEncoder().encode("tampered")),
425 layerSpawner([]),
426 layerHostRuntime(),
427 ),
428 ),
429 ),
430 );
431
432 it.effect.skipIf(windowsHost)("serializes concurrent installs within one runtime", () => {
433 const commands: Array<string> = [];
434 const bytes = new TextEncoder().encode("test-cloudflared-binary");
435 return Effect.gen(function* () {
436 const fileSystem = yield* FileSystem.FileSystem;
437 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
438 prefix: "t3-cloudflared-test-",
439 });
440 const manager = yield* RelayClient.makeCloudflaredRelayClient({
441 baseDir,
442 releaseAsset: {
443 url: "https://example.test/cloudflared",
444 sha256: Hex.encode(sha256(bytes)),
445 archive: "binary",
446 },
447 });
448
449 const [first, second] = yield* Effect.all([manager.install, manager.install], {
450 concurrency: "unbounded",
451 });
452 expect(second).toEqual(first);
453 // The first install validates the download once; the second, after waiting
454 // its turn, probes the activated binary and reuses it instead of downloading.
455 expect(commands).toHaveLength(2);
456 }).pipe(
457 Effect.scoped,
458 Effect.provide(
459 Layer.mergeAll(
460 NodeServices.layer,
461 layerHttpClient(bytes),
462 layerSpawner(commands),
463 layerHostRuntime(),
464 ),
465 ),
466 );
467 });
468
469 it.effect.skipIf(windowsHost)(
470 "observes PATH changes after the manager has been constructed",
471 () => {
472 const env = { PATH: "" };
473 return Effect.gen(function* () {
474 const fileSystem = yield* FileSystem.FileSystem;
475 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
476 prefix: "t3-cloudflared-test-",
477 });
478 const binDir = `${baseDir}/bin`;
479 const executablePath = `${binDir}/cloudflared`;
480 const manager = yield* RelayClient.makeCloudflaredRelayClient({
481 baseDir,
482 });
483
484 expect(yield* manager.resolve).toEqual({
485 status: "missing",
486 version: RelayClient.CLOUDFLARED_VERSION,
487 });
488
489 yield* fileSystem.makeDirectory(binDir);
490 yield* fileSystem.writeFileString(executablePath, "cloudflared");
491 yield* fileSystem.chmod(executablePath, 0o755);
492 env.PATH = binDir;
493
494 expect(yield* manager.resolve).toEqual({
495 status: "available",
496 executablePath,
497 source: "path",
498 version: RelayClient.CLOUDFLARED_VERSION,
499 });
500 }).pipe(
501 Effect.scoped,
502 Effect.provide(
503 Layer.mergeAll(
504 NodeServices.layer,
505 layerHttpClient(new Uint8Array()),
506 layerSpawner([]),
507 layerHostRuntime(env),
508 ),
509 ),
510 );
511 },
512 );
513
514 describe("version selection", () => {
515 const run = <A, E>(
516 body: (baseDir: string) => Effect.Effect<A, E, RelayClientTestServices>,
517 options: { readonly versions?: Record<string, string> } = {},
518 ) =>
519 Effect.gen(function* () {
520 const fileSystem = yield* FileSystem.FileSystem;
521 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
522 prefix: "t3-cloudflared-test-",
523 });
524 return yield* body(baseDir);
525 }).pipe(
526 Effect.scoped,
527 Effect.provide(
528 Layer.mergeAll(
529 NodeServices.layer,
530 layerHttpClient(new Uint8Array()),
531 layerSpawner([], options.versions),
532 layerHostRuntime({ PATH: "" }),
533 ),
534 ),
535 );
536
537 it.effect.skipIf(windowsHost)("skips a PATH binary older than the minimum version", () => {
538 const versions: Record<string, string> = {};
539 return run(
540 (baseDir) =>
541 Effect.gen(function* () {
542 const oldPath = `${baseDir}/old/cloudflared`;
543 const newPath = `${baseDir}/new/cloudflared`;
544 yield* writeExecutable(oldPath);
545 yield* writeExecutable(newPath);
546 versions[oldPath] = "2023.8.2";
547 versions[newPath] = RelayClient.CLOUDFLARED_MIN_VERSION;
548 const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir });
549 const env = { PATH: `${baseDir}/old:${baseDir}/new` };
550 expect(
551 yield* manager.resolve.pipe(
552 Effect.provideService(
553 ConfigProvider.ConfigProvider,
554 ConfigProvider.fromEnv({ env }),
555 ),
556 ),
557 ).toEqual({
558 status: "available",
559 executablePath: newPath,
560 source: "path",
561 version: RelayClient.CLOUDFLARED_MIN_VERSION,
562 });
563 }),
564 { versions },
565 );
566 });
567
568 it.effect.skipIf(windowsHost)("reports an outdated override as missing", () => {
569 const versions: Record<string, string> = {};
570 return run(
571 (baseDir) =>
572 Effect.gen(function* () {
573 const overridePath = `${baseDir}/override/cloudflared`;
574 yield* writeExecutable(overridePath);
575 versions[overridePath] = "2025.6.0";
576 const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir });
577 const env = { PATH: "", T3CODE_CLOUDFLARED_PATH: overridePath };
578 const withEnv = Effect.provideService(
579 ConfigProvider.ConfigProvider,
580 ConfigProvider.fromEnv({ env }),
581 );
582 expect(yield* manager.resolve.pipe(withEnv)).toEqual({
583 status: "missing",
584 version: RelayClient.CLOUDFLARED_VERSION,
585 });
586 const error = yield* manager.install.pipe(withEnv, Effect.flip);
587 expect(error.reason).toBe("override_missing");
588 }),
589 { versions },
590 );
591 });
592
593 it.effect.skipIf(windowsHost)(
594 "prefers the pinned release, then the newest older managed release, then PATH",
595 () => {
596 const versions: Record<string, string> = {};
597 return run(
598 (baseDir) =>
599 Effect.gen(function* () {
600 const fileSystem = yield* FileSystem.FileSystem;
601 const pathBinary = `${baseDir}/bin/cloudflared`;
602 const older = managedPathFor(baseDir, "2025.9.0");
603 const newer = managedPathFor(baseDir, "2026.1.0");
604 const pinned = managedPathFor(baseDir, RelayClient.CLOUDFLARED_VERSION);
605 // A newer server sharing this base dir owns this folder.
606 const newerServer = managedPathFor(baseDir, "2099.1.0");
607 versions[newerServer] = "2099.1.0";
608 yield* writeExecutable(newerServer);
609 versions[pathBinary] = "2026.9.3";
610 versions[older] = "2025.9.0";
611 versions[newer] = "2026.1.0";
612 yield* writeExecutable(pathBinary);
613 yield* writeExecutable(older);
614 yield* writeExecutable(newer);
615 const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir });
616 const resolveWith = manager.resolve.pipe(
617 Effect.provideService(
618 ConfigProvider.ConfigProvider,
619 ConfigProvider.fromEnv({ env: { PATH: `${baseDir}/bin` } }),
620 ),
621 );
622
623 expect(yield* resolveWith).toMatchObject({ source: "managed", version: "2026.1.0" });
624
625 yield* writeExecutable(pinned);
626 expect(yield* resolveWith).toMatchObject({
627 executablePath: pinned,
628 source: "managed",
629 version: RelayClient.CLOUDFLARED_VERSION,
630 });
631
632 yield* manager.pruneManagedVersions;
633 expect(
634 (yield* fileSystem.readDirectory(`${baseDir}/tools/cloudflared`)).sort(),
635 ).toEqual(["2026.1.0", RelayClient.CLOUDFLARED_VERSION, "2099.1.0"]);
636
637 // The kept older release still outranks PATH; PATH is the last resort.
638 yield* fileSystem.remove(pinned);
639 expect(yield* resolveWith).toMatchObject({ source: "managed", version: "2026.1.0" });
640 yield* fileSystem.remove(`${baseDir}/tools/cloudflared/2026.1.0`, {
641 recursive: true,
642 });
643 expect(yield* resolveWith).toMatchObject({ source: "path", version: "2026.9.3" });
644 }),
645 { versions },
646 );
647 },
648 );
649
650 it.effect.skipIf(windowsHost)(
651 "keeps a self-updated managed binary as a fallback but still installs the pin",
652 () => {
653 const versions: Record<string, string> = {};
654 return run(
655 (baseDir) =>
656 Effect.gen(function* () {
657 const pinned = managedPathFor(baseDir, RelayClient.CLOUDFLARED_VERSION);
658 versions[pinned] = "2026.9.3";
659 yield* writeExecutable(pinned);
660 const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir });
661 const resolved = yield* manager.resolve;
662 expect(resolved).toEqual({
663 status: "available",
664 executablePath: pinned,
665 source: "managed",
666 version: "2026.9.3",
667 });
668 expect(
669 resolved.status === "available" && RelayClient.isPinnedManagedRelayClient(resolved),
670 ).toBe(false);
671 }),
672 { versions },
673 );
674 },
675 );
676 });
677
678 it("orders and parses cloudflared versions", () => {
679 expect(
680 RelayClient.parseCloudflaredVersionOutput(
681 "cloudflared version 2026.5.2 (built 2026-05-20-10:00 UTC)",
682 ),
683 ).toBe("2026.5.2");
684 expect(RelayClient.parseCloudflaredVersionOutput("Incorrect Usage")).toBeNull();
685 expect(RelayClient.compareCloudflaredVersions("2025.10.0", "2025.6.1")).toBeGreaterThan(0);
686 expect(RelayClient.compareCloudflaredVersions("2023.8.2", "2025.6.1")).toBeLessThan(0);
687 });
688
689 it.effect.each([
690 ["staging", "EBUSY"],
691 ["activation", "EACCES"],
692 ] as const)("retries %s on Windows while another process holds the binary", ([rename, code]) =>
693 Effect.gen(function* () {
694 const fileSystem = yield* FileSystem.FileSystem;
695 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
696 prefix: "t3-cloudflared-test-",
697 });
698 const { locked, renames, firstAttempt } = yield* makeLockedRenames(fileSystem, {
699 rename,
700 failures: 2,
701 code,
702 });
703 const manager = yield* RelayClient.makeCloudflaredRelayClient({
704 baseDir,
705 releaseAsset: testReleaseAsset,
706 }).pipe(Effect.provideService(FileSystem.FileSystem, locked));
707
708 const installing = yield* manager.install.pipe(Effect.forkChild);
709 yield* Deferred.await(firstAttempt);
710 yield* TestClock.adjust("1 second");
711 const installed = yield* Fiber.join(installing);
712
713 expect(renames.attempts).toBe(3);
714 expect(new TextDecoder().decode(yield* fileSystem.readFile(installed.executablePath))).toBe(
715 "test-cloudflared-binary",
716 );
717 expect(yield* fileSystem.readDirectory(managedDirectory(baseDir, "win32"))).toEqual([
718 "cloudflared.exe",
719 ]);
720 }).pipe(Effect.scoped, Effect.provide(layerInstallRuntime("win32"))),
721 );
722
723 it.effect.each([
724 ["staging", "EPERM", "Could not stage the relay client."],
725 ["activation", "EBUSY", "Could not activate the relay client."],
726 ] as const)("gives up on a Windows %s lock that does not clear", ([rename, code, message]) =>
727 Effect.gen(function* () {
728 const fileSystem = yield* FileSystem.FileSystem;
729 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
730 prefix: "t3-cloudflared-test-",
731 });
732 const { locked, renames, firstAttempt } = yield* makeLockedRenames(fileSystem, {
733 rename,
734 failures: Infinity,
735 code,
736 });
737 const manager = yield* RelayClient.makeCloudflaredRelayClient({
738 baseDir,
739 releaseAsset: testReleaseAsset,
740 }).pipe(Effect.provideService(FileSystem.FileSystem, locked));
741
742 const installing = yield* manager.install.pipe(Effect.flip, Effect.forkChild);
743 yield* Deferred.await(firstAttempt);
744 yield* TestClock.adjust("1 minute");
745 const error = yield* Fiber.join(installing);
746
747 expect(error.message).toBe(message);
748 expect(renames.attempts).toBe(41);
749 // Nothing is installed, and the staged copy, download folder, and install lock are gone.
750 expect(yield* fileSystem.readDirectory(managedDirectory(baseDir, "win32"))).toEqual([]);
751 }).pipe(Effect.scoped, Effect.provide(layerInstallRuntime("win32"))),
752 );
753
754 it.effect.each([
755 ["linux", "EBUSY"],
756 ["win32", "EXDEV"],
757 ] as const)("fails staging at once on %s for %s", ([platform, code]) =>
758 Effect.gen(function* () {
759 const fileSystem = yield* FileSystem.FileSystem;
760 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
761 prefix: "t3-cloudflared-test-",
762 });
763 const { locked, renames } = yield* makeLockedRenames(fileSystem, {
764 rename: "staging",
765 failures: Infinity,
766 code,
767 });
768 const manager = yield* RelayClient.makeCloudflaredRelayClient({
769 baseDir,
770 releaseAsset: testReleaseAsset,
771 }).pipe(Effect.provideService(FileSystem.FileSystem, locked));
772
773 const error = yield* manager.install.pipe(Effect.flip);
774
775 expect(error.message).toBe("Could not stage the relay client.");
776 expect(renames.attempts).toBe(1);
777 }).pipe(Effect.scoped, Effect.provide(layerInstallRuntime(platform))),
778 );
779
780 it.effect("fails a stalled download after the download timeout", () => {
781 const requests: Array<unknown> = [];
782 return Effect.gen(function* () {
783 const fileSystem = yield* FileSystem.FileSystem;
784 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
785 prefix: "t3-cloudflared-test-",
786 });
787 const manager = yield* RelayClient.makeCloudflaredRelayClient({
788 baseDir,
789 releaseAsset: {
790 url: "https://example.test/cloudflared",
791 sha256: "00".repeat(32),
792 archive: "binary",
793 },
794 });
795
796 const child = yield* Effect.forkChild(manager.install);
797 // Spin until the wedged download is in flight, so the clock
798 // adjustment below cannot run before the timeout is armed.
799 while (requests.length === 0) {
800 yield* Effect.yieldNow;
801 }
802 // The download timeout is 10 minutes; advance past it.
803 yield* TestClock.adjust("11 minutes");
804 const error = yield* Fiber.join(child).pipe(Effect.flip);
805 expect(error).toBeInstanceOf(RelayClient.RelayClientInstallError);
806 expect(error.reason).toBe("download_failed");
807 }).pipe(
808 Effect.scoped,
809 Effect.provide(
810 Layer.mergeAll(
811 NodeServices.layer,
812 layerStalledHttpClient(requests),
813 layerSpawner([]),
814 layerHostRuntime({ PATH: "" }),
815 ),
816 ),
817 );
818 });
819});