Skip to content
Source
Sign inSign up
Open History to see the last commit

relayClient.test.ts

862 lines32.5 KB

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