1import { describe, expect, it } from "@effect/vitest";
2import { vi } from "vite-plus/test";
3import * as Deferred from "effect/Deferred";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Fiber from "effect/Fiber";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as PlatformError from "effect/PlatformError";
11import * as Queue from "effect/Queue";
12import * as Sink from "effect/Sink";
13import * as Stream from "effect/Stream";
14import * as TestClock from "effect/testing/TestClock";
15import { ChildProcess, ChildProcessSpawner } from "effect/process";
16import { HttpClient, HttpClientError, HttpClientResponse } from "effect/http";
17import type { RelayManagedEndpointRuntimeConfig } from "@t3tools/contracts/relay";
18import * as RelayClient from "@t3tools/shared/relayClient";
19
20import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
21import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
22
23const layerRelayClientAvailable = Layer.succeed(
24 RelayClient.RelayClient,
25 RelayClient.RelayClient.of({
26 resolve: Effect.succeed({
27 status: "available",
28 executablePath: "cloudflared",
29 source: "path",
30 version: RelayClient.CLOUDFLARED_VERSION,
31 }),
32 install: Effect.die("unused"),
33 installWithProgress: () => Effect.die("unused"),
34 pruneManagedVersions: Effect.void,
35 }),
36);
37
38// Only connectors that log a metrics address are polled for readiness.
39const unusedHttpClient = HttpClient.make(() => Effect.die("unused"));
40
41const runtimeDependencies = (
42 spawner: ReturnType<typeof ChildProcessSpawner.make>,
43 relayClientLayer = layerRelayClientAvailable,
44 httpClient = unusedHttpClient,
45) =>
46 Layer.mergeAll(
47 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
48 Layer.succeed(HttpClient.HttpClient, httpClient),
49 relayClientLayer,
50 Layer.mock(ServerSecretStore.ServerSecretStore)({
51 get: () => Effect.succeedNone,
52 }),
53 );
54
55const buildCloudManagedEndpointRuntime = (
56 spawner: ReturnType<typeof ChildProcessSpawner.make>,
57 relayClientLayer = layerRelayClientAvailable,
58 httpClient = unusedHttpClient,
59) =>
60 Effect.gen(function* () {
61 const context = yield* Layer.build(
62 ManagedEndpointRuntime.layer.pipe(
63 Layer.provide(runtimeDependencies(spawner, relayClientLayer, httpClient)),
64 ),
65 );
66 return yield* Effect.service(ManagedEndpointRuntime.CloudManagedEndpointRuntime).pipe(
67 Effect.provide(context),
68 );
69 });
70
71function makeHandle(input: {
72 readonly pid: number;
73 readonly onKill: () => void;
74 readonly isRunning?: () => boolean;
75 readonly exitCode?: Effect.Effect<ChildProcessSpawner.ExitCode>;
76 readonly output?: Stream.Stream<Uint8Array>;
77}) {
78 return ChildProcessSpawner.makeHandle({
79 pid: ChildProcessSpawner.ProcessId(input.pid),
80 exitCode: input.exitCode ?? Effect.never,
81 isRunning: Effect.sync(() => input.isRunning?.() ?? true),
82 kill: () =>
83 Effect.sync(() => {
84 input.onKill();
85 }),
86 unref: Effect.succeed(Effect.void),
87 stdin: Sink.drain,
88 stdout: Stream.empty,
89 stderr: Stream.empty,
90 all: input.output ?? Stream.empty,
91 getInputFd: () => Sink.drain,
92 getOutputFd: () => Stream.empty,
93 });
94}
95
96describe("CloudManagedEndpointRuntime", () => {
97 it("retries connector startup failures but stops for unsupported runtimes", () => {
98 expect(
99 ManagedEndpointRuntime.isRetryableManagedEndpointRuntimeStatus({
100 status: "failed",
101 failure: "not-installed",
102 reason: "The relay client is not installed.",
103 }),
104 ).toBe(true);
105 expect(
106 ManagedEndpointRuntime.isRetryableManagedEndpointRuntimeStatus({
107 status: "failed",
108 failure: "spawn-failed",
109 reason: "spawn failed",
110 }),
111 ).toBe(true);
112 expect(
113 ManagedEndpointRuntime.isRetryableManagedEndpointRuntimeStatus({
114 status: "failed",
115 failure: "unsupported-platform",
116 reason: "Relay client is unsupported on linux-arm.",
117 }),
118 ).toBe(false);
119 expect(
120 ManagedEndpointRuntime.isRetryableManagedEndpointRuntimeStatus({ status: "unsupported" }),
121 ).toBe(false);
122 });
123
124 it.effect("serializes updates to persisted cloud link state", () =>
125 Effect.gen(function* () {
126 const firstEntered = yield* Deferred.make<void>();
127 const releaseFirst = yield* Deferred.make<void>();
128 const secondEntered = yield* Deferred.make<void>();
129 const runtime = yield* buildCloudManagedEndpointRuntime(
130 ChildProcessSpawner.make(() => Effect.die("unused")),
131 );
132
133 const first = yield* runtime
134 .withLinkStateLock(
135 Deferred.succeed(firstEntered, undefined).pipe(
136 Effect.andThen(Deferred.await(releaseFirst)),
137 ),
138 )
139 .pipe(Effect.forkChild);
140 yield* Deferred.await(firstEntered);
141
142 const second = yield* runtime
143 .withLinkStateLock(Deferred.succeed(secondEntered, undefined))
144 .pipe(Effect.forkChild);
145 expect(yield* Deferred.isDone(secondEntered)).toBe(false);
146
147 yield* Deferred.succeed(releaseFirst, undefined);
148 yield* Fiber.join(first);
149 yield* Fiber.join(second);
150 expect(yield* Deferred.isDone(secondEntered)).toBe(true);
151 }),
152 );
153
154 it("classifies Cloudflare connection and warning output", () => {
155 expect(
156 ManagedEndpointRuntime.classifyRelayClientOutput(
157 "2026-06-17T02:00:00Z INF Registered tunnel connection connIndex=0",
158 ),
159 ).toBe("connected");
160 expect(
161 ManagedEndpointRuntime.classifyRelayClientOutput(
162 "2026-06-17T02:00:00Z ERR Failed to serve tunnel connection",
163 ),
164 ).toBe("warning");
165 expect(
166 ManagedEndpointRuntime.classifyRelayClientOutput(
167 "2026-06-17T02:00:00Z INF Starting metrics server",
168 ),
169 ).toBe("debug");
170 // FTL (fatal) and PNC (panic) are more severe than ERR and must surface.
171 expect(
172 ManagedEndpointRuntime.classifyRelayClientOutput(
173 "2026-06-17T02:00:00Z FTL Cannot determine default origin certificate path",
174 ),
175 ).toBe("warning");
176 expect(
177 ManagedEndpointRuntime.classifyRelayClientOutput("2026-06-17T02:00:00Z PNC runtime panic"),
178 ).toBe("warning");
179 });
180
181 it("recognizes tunnel authorization failures without matching ordinary transport errors", () => {
182 expect(
183 ManagedEndpointRuntime.isRejectedRelayClientTunnelOutput(
184 '2026-09-15T06:30:43Z ERR Register tunnel error from server side error="Failed to get tunnel" connIndex=0 event=0 ip=198.41.200.23',
185 ),
186 ).toBe(true);
187 expect(
188 ManagedEndpointRuntime.isRejectedRelayClientTunnelOutput(
189 '2026-06-17T02:00:00Z ERR Register tunnel error from server side error="Unauthorized: Record for tunnel not found" connIndex=0',
190 ),
191 ).toBe(true);
192 expect(
193 ManagedEndpointRuntime.isRejectedRelayClientTunnelOutput(
194 '2026-06-17T02:00:00Z ERR Register tunnel error from server side error="Unauthorized: Invalid tunnel secret" connIndex=0',
195 ),
196 ).toBe(true);
197 // Seen in production on 2026-10-06 after the relay deleted an idle tunnel.
198 expect(
199 ManagedEndpointRuntime.isRejectedRelayClientTunnelOutput(
200 '2026-10-06T12:00:00Z ERR Register tunnel error from server side error="Unauthorized: Tunnel not found" connIndex=0 event=0 ip=198.41.200.23',
201 ),
202 ).toBe(true);
203 expect(
204 ManagedEndpointRuntime.isRejectedRelayClientTunnelOutput(
205 '2026-10-06T12:00:00Z ERR Register tunnel error from server side error="Tunnel not found" connIndex=0',
206 ),
207 ).toBe(true);
208 expect(
209 ManagedEndpointRuntime.isRejectedRelayClientTunnelOutput(
210 '2026-06-17T02:00:00Z ERR Register tunnel error from server side error="connection timed out" connIndex=0',
211 ),
212 ).toBe(false);
213 expect(
214 ManagedEndpointRuntime.isRejectedRelayClientTunnelOutput(
215 '2026-06-17T02:00:00Z ERR Failed to serve tunnel connection error="Unauthorized: Tunnel not found" connIndex=0',
216 ),
217 ).toBe(false);
218 });
219
220 it.effect("keeps recovery requests sent before the server starts consuming them", () =>
221 Effect.gen(function* () {
222 const runtime = yield* buildCloudManagedEndpointRuntime(
223 ChildProcessSpawner.make(() => Effect.die("unused")),
224 );
225 const config = {
226 providerKind: "cloudflare_tunnel" as const,
227 connectorToken: "token",
228 tunnelId: "tunnel-1",
229 };
230
231 yield* runtime.requestRecovery(config);
232
233 expect(Option.getOrNull(yield* Stream.runHead(runtime.recoveryRequests))).toEqual(config);
234 }),
235 );
236
237 it.effect("signals each registered tunnel connection", () =>
238 Effect.gen(function* () {
239 const output = yield* Queue.unbounded<Uint8Array>();
240 const spawner = ChildProcessSpawner.make(() =>
241 Effect.gen(function* () {
242 const handle = makeHandle({
243 pid: 700,
244 onKill: () => {},
245 output: Stream.fromQueue(output),
246 });
247 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
248 return handle;
249 }),
250 );
251 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
252 yield* runtime.applyConfig({
253 providerKind: "cloudflare_tunnel",
254 connectorToken: "token",
255 tunnelId: "tunnel-1",
256 });
257 yield* Queue.offer(
258 output,
259 new TextEncoder().encode(
260 "2026-10-04T06:30:43Z INF Registered tunnel connection connIndex=0 event=0\n",
261 ),
262 );
263 expect(Option.isSome(yield* Stream.runHead(runtime.tunnelConnected))).toBe(true);
264 }),
265 );
266
267 it.effect("recovers a rejected tunnel without waiting for the connector to exit", () =>
268 Effect.gen(function* () {
269 const output = yield* Queue.unbounded<Uint8Array>();
270 const firstBatchObserved = yield* Deferred.make<void>();
271 const secondBatchObserved = yield* Deferred.make<void>();
272 const recoveryRequested = yield* Deferred.make<RelayManagedEndpointRuntimeConfig>();
273 const recoveryRetried = yield* Deferred.make<RelayManagedEndpointRuntimeConfig>();
274 let recoveryRequestCount = 0;
275 const spawned: Array<number> = [];
276 const encoder = new TextEncoder();
277 const connectorOutput = Stream.fromQueue(output).pipe(
278 Stream.tap((chunk) => {
279 const line = new TextDecoder().decode(chunk);
280 if (line === "first checkpoint\n") {
281 return Deferred.succeed(firstBatchObserved, undefined).pipe(Effect.asVoid);
282 }
283 if (line === "second checkpoint\n") {
284 return Deferred.succeed(secondBatchObserved, undefined).pipe(Effect.asVoid);
285 }
286 return Effect.void;
287 }),
288 );
289 const spawner = ChildProcessSpawner.make(() =>
290 Effect.gen(function* () {
291 const pid = 600;
292 spawned.push(pid);
293 const handle = makeHandle({ pid, onKill: () => {}, output: connectorOutput });
294 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
295 return handle;
296 }),
297 );
298 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
299 const config = {
300 providerKind: "cloudflare_tunnel" as const,
301 connectorToken: "token",
302 tunnelId: "deleted-tunnel",
303 };
304 const rejectedLine =
305 '2026-09-15T06:30:43Z ERR Register tunnel error from server side error="Failed to get tunnel" connIndex=0 event=0 ip=198.41.200.23\n';
306
307 yield* runtime.recoveryRequests.pipe(
308 Stream.runForEach((requested) => {
309 recoveryRequestCount += 1;
310 return Deferred.succeed(
311 recoveryRequestCount === 1 ? recoveryRequested : recoveryRetried,
312 requested,
313 ).pipe(Effect.asVoid);
314 }),
315 Effect.forkChild,
316 );
317 yield* runtime.applyConfig(config);
318
319 yield* Queue.offer(output, encoder.encode(rejectedLine.repeat(3)));
320 yield* Queue.offer(output, encoder.encode("first checkpoint\n"));
321 yield* Deferred.await(firstBatchObserved);
322 expect(yield* Deferred.isDone(recoveryRequested)).toBe(false);
323
324 yield* Queue.offer(
325 output,
326 encoder.encode(
327 "2026-06-17T02:00:00Z INF Registered tunnel connection connIndex=0\n" +
328 rejectedLine.repeat(3),
329 ),
330 );
331 yield* Queue.offer(output, encoder.encode("second checkpoint\n"));
332 yield* Deferred.await(secondBatchObserved);
333 expect(yield* Deferred.isDone(recoveryRequested)).toBe(false);
334
335 yield* Queue.offer(output, encoder.encode(rejectedLine));
336
337 expect(yield* Deferred.await(recoveryRequested)).toEqual(config);
338
339 yield* Queue.offer(output, encoder.encode(rejectedLine.repeat(4)));
340
341 expect(yield* Deferred.await(recoveryRetried)).toEqual(config);
342 expect(spawned).toEqual([600]);
343 }),
344 );
345
346 it.effect("requests recovery while the connector never registers a connection", () =>
347 Effect.gen(function* () {
348 const output = yield* Queue.unbounded<Uint8Array>();
349 const spawner = ChildProcessSpawner.make(() =>
350 Effect.gen(function* () {
351 const handle = makeHandle({
352 pid: 800,
353 onKill: () => {},
354 output: Stream.fromQueue(output),
355 });
356 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
357 return handle;
358 }),
359 );
360 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
361 const requests = yield* Queue.unbounded<RelayManagedEndpointRuntimeConfig>();
362 yield* runtime.recoveryRequests.pipe(
363 Stream.runForEach((config) => Queue.offer(requests, config)),
364 Effect.forkChild,
365 );
366 const config = {
367 providerKind: "cloudflare_tunnel" as const,
368 connectorToken: "token",
369 tunnelId: "silently-deleted",
370 };
371 yield* runtime.applyConfig(config);
372
373 yield* TestClock.adjust(Duration.minutes(2));
374 expect(yield* Queue.size(requests)).toBe(0);
375 yield* TestClock.adjust(Duration.minutes(1));
376 expect(yield* Queue.take(requests)).toEqual(config);
377 // Still unconnected, so it asks again.
378 yield* TestClock.adjust(Duration.minutes(3));
379 expect(yield* Queue.take(requests)).toEqual(config);
380 }).pipe(Effect.provide(TestClock.layer())),
381 );
382
383 it.effect("does not request recovery once the connector has connected", () =>
384 Effect.gen(function* () {
385 const output = yield* Queue.unbounded<Uint8Array>();
386 const spawner = ChildProcessSpawner.make(() =>
387 Effect.gen(function* () {
388 const handle = makeHandle({
389 pid: 801,
390 onKill: () => {},
391 output: Stream.fromQueue(output),
392 });
393 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
394 return handle;
395 }),
396 );
397 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
398 const requests = yield* Queue.unbounded<RelayManagedEndpointRuntimeConfig>();
399 yield* runtime.recoveryRequests.pipe(
400 Stream.runForEach((config) => Queue.offer(requests, config)),
401 Effect.forkChild,
402 );
403 yield* runtime.applyConfig({
404 providerKind: "cloudflare_tunnel",
405 connectorToken: "token",
406 tunnelId: "tunnel-1",
407 });
408 yield* Queue.offer(
409 output,
410 new TextEncoder().encode(
411 "2026-10-06T00:00:00Z INF Registered tunnel connection connIndex=0\n",
412 ),
413 );
414 yield* Stream.runHead(runtime.tunnelConnected);
415
416 yield* TestClock.adjust(Duration.minutes(10));
417 expect(yield* Queue.size(requests)).toBe(0);
418 }).pipe(Effect.provide(TestClock.layer())),
419 );
420
421 it.effect(
422 "restarts a running connector whose readiness check keeps reporting no connections",
423 () =>
424 Effect.gen(function* () {
425 const spawned: Array<number> = [];
426 const killed: Array<number> = [];
427 const exits: Array<Deferred.Deferred<ChildProcessSpawner.ExitCode>> = [];
428 const secondSpawned = yield* Deferred.make<void>();
429 const spawner = ChildProcessSpawner.make(() =>
430 Effect.gen(function* () {
431 const index = spawned.length;
432 const pid = 1000 + index;
433 const exit = yield* Deferred.make<ChildProcessSpawner.ExitCode>();
434 spawned.push(pid);
435 exits.push(exit);
436 if (index === 1) yield* Deferred.succeed(secondSpawned, undefined);
437 const handle = makeHandle({
438 pid,
439 exitCode: Deferred.await(exit),
440 onKill: () => {
441 if (!killed.includes(pid)) killed.push(pid);
442 Deferred.doneUnsafe(exit, Exit.succeed(ChildProcessSpawner.ExitCode(143)));
443 },
444 output: Stream.concat(
445 Stream.make(
446 new TextEncoder().encode(
447 `2026-10-14T08:49:04Z INF Starting metrics server on 127.0.0.1:${40000 + index}/metrics\n`,
448 ),
449 ),
450 Stream.never,
451 ),
452 });
453 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
454 return handle;
455 }),
456 );
457 // Each readiness check takes the next scripted answer, then 503. It
458 // records which connectors were killed before it was sent.
459 const answers: Array<number | "refused"> = [];
460 const checks = yield* Queue.unbounded<{ readonly url: string; readonly killed: string }>();
461 const httpClient = HttpClient.make((request) => {
462 Queue.offerUnsafe(checks, { url: request.url, killed: killed.join(",") });
463 const status = answers.shift() ?? 503;
464 return status === "refused"
465 ? Effect.fail(
466 new HttpClientError.HttpClientError({
467 reason: new HttpClientError.TransportError({ request }),
468 }),
469 )
470 : Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status })));
471 });
472 // Advances the test clock until the watchdog checks readiness again.
473 const nextCheck = Effect.gen(function* () {
474 for (let second = 0; second < 60; second += 1) {
475 yield* TestClock.adjust(Duration.seconds(1));
476 const check = yield* Queue.poll(checks);
477 if (Option.isSome(check)) return check.value;
478 }
479 return yield* Effect.die("No readiness check within a minute");
480 });
481 const runtime = yield* buildCloudManagedEndpointRuntime(
482 spawner,
483 layerRelayClientAvailable,
484 httpClient,
485 );
486 yield* runtime.applyConfig({
487 providerKind: "cloudflare_tunnel",
488 connectorToken: "token",
489 tunnelId: "tunnel-1",
490 });
491
492 // Five 503s, a failed request that does not count, a ready answer that
493 // clears the streak, then six 503s in a row.
494 answers.push(503, 503, 503, 503, 503, "refused", 200);
495 for (let check = 0; check < 13; check += 1) {
496 expect(yield* nextCheck).toEqual({ url: "http://127.0.0.1:40000/ready", killed: "" });
497 }
498 yield* Deferred.await(secondSpawned);
499 expect(killed).toEqual([1000]);
500 expect(spawned).toEqual([1000, 1001]);
501
502 // The replacement follows a restart that never got a ready answer, so
503 // it is killed only after twelve 503s.
504 for (let check = 0; check < 12; check += 1) {
505 expect(yield* nextCheck).toEqual({ url: "http://127.0.0.1:40001/ready", killed: "1000" });
506 }
507 yield* Deferred.await(exits[1]!);
508 expect(killed).toEqual([1000, 1001]);
509 }).pipe(Effect.provide(TestClock.layer())),
510 );
511
512 it.effect("starts, deduplicates, rotates, and stops the Cloudflare connector", () =>
513 Effect.gen(function* () {
514 const spawned: Array<ChildProcess.StandardCommand> = [];
515 const killed: Array<number> = [];
516 let nextPid = 100;
517 const spawner = ChildProcessSpawner.make((command) =>
518 Effect.gen(function* () {
519 if (!ChildProcess.isStandardCommand(command)) {
520 throw new Error("Expected standard command.");
521 }
522 spawned.push(command);
523 const pid = nextPid;
524 nextPid += 1;
525 const handle = makeHandle({
526 pid,
527 onKill: () => {
528 killed.push(pid);
529 },
530 });
531 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
532 return handle;
533 }),
534 );
535 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
536
537 yield* runtime.applyConfig({
538 providerKind: "cloudflare_tunnel",
539 connectorToken: "token-1",
540 tunnelId: "tunnel-1",
541 tunnelName: "t3-code-env-1",
542 });
543 yield* runtime.applyConfig({
544 providerKind: "cloudflare_tunnel",
545 connectorToken: "token-1",
546 tunnelId: "tunnel-1",
547 tunnelName: "t3-code-env-1",
548 });
549 yield* runtime.applyConfig({
550 providerKind: "cloudflare_tunnel",
551 connectorToken: "token-2",
552 tunnelId: "tunnel-1",
553 tunnelName: "t3-code-env-1",
554 });
555 const stopped = yield* runtime.applyConfig(null);
556
557 expect(spawned.map((command) => command.command)).toEqual(["cloudflared", "cloudflared"]);
558 expect(spawned.map((command) => command.args)).toEqual([
559 [
560 "tunnel",
561 "--no-autoupdate",
562 "--loglevel",
563 "info",
564 "--output",
565 "default",
566 "--metrics",
567 "127.0.0.1:0",
568 "run",
569 ],
570 [
571 "tunnel",
572 "--no-autoupdate",
573 "--loglevel",
574 "info",
575 "--output",
576 "default",
577 "--metrics",
578 "127.0.0.1:0",
579 "run",
580 ],
581 ]);
582 expect(spawned.map((command) => command.options.env?.TUNNEL_TOKEN)).toEqual([
583 "token-1",
584 "token-2",
585 ]);
586 expect(spawned.map((command) => command.options.stdout)).toEqual(["pipe", "pipe"]);
587 expect(spawned.map((command) => command.options.stderr)).toEqual(["pipe", "pipe"]);
588 expect(spawned.map((command) => command.options.detached)).toEqual([false, false]);
589 expect(spawned.map((command) => command.options.shell)).toEqual([false, false]);
590 expect(killed).toEqual([100, 101]);
591 expect(stopped).toEqual({ status: "disabled" });
592 }),
593 );
594
595 it.effect("stops an active connector when a non-Cloudflare runtime config is applied", () =>
596 Effect.gen(function* () {
597 const killed: Array<number> = [];
598 const spawner = ChildProcessSpawner.make(() =>
599 Effect.gen(function* () {
600 const handle = makeHandle({
601 pid: 200,
602 onKill: () => {
603 killed.push(200);
604 },
605 });
606 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
607 return handle;
608 }),
609 );
610 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
611
612 const started = yield* runtime.applyConfig({
613 providerKind: "cloudflare_tunnel",
614 connectorToken: "token",
615 });
616 const unsupported = yield* runtime.applyConfig({
617 providerKind: "manual",
618 connectorToken: "manual-token",
619 });
620
621 expect(started.status).toBe("running");
622 expect(unsupported).toEqual({ status: "unsupported", providerKind: "manual" });
623 expect(killed).toEqual([200]);
624 }),
625 );
626
627 it.effect("restarts the connector when the active process has exited", () =>
628 Effect.gen(function* () {
629 const spawned: Array<number> = [];
630 const killed: Array<number> = [];
631 let firstRunning = true;
632 const spawner = ChildProcessSpawner.make(() =>
633 Effect.gen(function* () {
634 const pid = spawned.length === 0 ? 300 : 301;
635 spawned.push(pid);
636 const handle = makeHandle({
637 pid,
638 isRunning: () => (pid === 300 ? firstRunning : true),
639 onKill: () => {
640 killed.push(pid);
641 },
642 });
643 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
644 return handle;
645 }),
646 );
647 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
648 const config = {
649 providerKind: "cloudflare_tunnel" as const,
650 connectorToken: "token",
651 tunnelId: "tunnel-1",
652 };
653
654 const first = yield* runtime.applyConfig(config);
655 firstRunning = false;
656 const second = yield* runtime.applyConfig(config);
657
658 expect(first).toMatchObject({ status: "running", pid: 300 });
659 expect(second).toMatchObject({ status: "running", pid: 301 });
660 expect(spawned).toEqual([300, 301]);
661 expect(killed).toEqual([300]);
662 }),
663 );
664
665 it.effect("supervises the active connector and restarts it after process exit", () =>
666 Effect.gen(function* () {
667 const spawned: Array<number> = [];
668 const killed: Array<number> = [];
669 const firstExit = yield* Deferred.make<ChildProcessSpawner.ExitCode>();
670 const secondSpawned = yield* Deferred.make<void>();
671 const spawner = ChildProcessSpawner.make(() =>
672 Effect.gen(function* () {
673 const pid = spawned.length === 0 ? 400 : 401;
674 spawned.push(pid);
675 if (pid === 401) {
676 yield* Deferred.succeed(secondSpawned, undefined);
677 }
678 const handle = makeHandle({
679 pid,
680 exitCode:
681 pid === 400
682 ? Deferred.await(firstExit)
683 : (Effect.never as Effect.Effect<ChildProcessSpawner.ExitCode>),
684 onKill: () => {
685 killed.push(pid);
686 },
687 });
688 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
689 return handle;
690 }),
691 );
692 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
693
694 const started = yield* runtime.applyConfig({
695 providerKind: "cloudflare_tunnel",
696 connectorToken: "token",
697 tunnelId: "tunnel-1",
698 });
699 yield* Deferred.succeed(firstExit, ChildProcessSpawner.ExitCode(1));
700 yield* Deferred.await(secondSpawned);
701
702 expect(started).toMatchObject({ status: "running", pid: 400 });
703 expect(spawned).toEqual([400, 401]);
704 expect(killed).toEqual([400]);
705 }),
706 );
707
708 const makeCrashLoopSpawner = (basePid: number, slots: number) =>
709 Effect.gen(function* () {
710 const spawned: Array<number> = [];
711 const exits: Array<Deferred.Deferred<ChildProcessSpawner.ExitCode>> = [];
712 const spawnSignals: Array<Deferred.Deferred<void>> = [];
713 for (let index = 0; index < slots; index += 1) {
714 exits.push(yield* Deferred.make<ChildProcessSpawner.ExitCode>());
715 spawnSignals.push(yield* Deferred.make<void>());
716 }
717 const spawner = ChildProcessSpawner.make(() =>
718 Effect.gen(function* () {
719 const index = spawned.length;
720 spawned.push(basePid + index);
721 yield* Deferred.succeed(spawnSignals[index]!, undefined);
722 const handle = makeHandle({
723 pid: basePid + index,
724 exitCode: Deferred.await(exits[index]!),
725 onKill: () => {},
726 });
727 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
728 return handle;
729 }),
730 );
731 return { spawner, spawned, exits, spawnSignals };
732 });
733
734 it.effect("backs off restarts while the connector crash-loops", () =>
735 Effect.gen(function* () {
736 const { spawner, spawned, exits, spawnSignals } = yield* makeCrashLoopSpawner(600, 4);
737 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
738
739 yield* runtime.applyConfig({
740 providerKind: "cloudflare_tunnel",
741 connectorToken: "token",
742 tunnelId: "tunnel-1",
743 });
744 expect(spawned).toEqual([600]);
745
746 // The first crash restarts immediately.
747 yield* Deferred.succeed(exits[0]!, ChildProcessSpawner.ExitCode(1));
748 yield* Deferred.await(spawnSignals[1]!);
749 expect(spawned).toEqual([600, 601]);
750
751 // The second rapid crash waits out the base delay before restarting.
752 yield* Deferred.succeed(exits[1]!, ChildProcessSpawner.ExitCode(1));
753 yield* TestClock.adjust(Duration.millis(999));
754 expect(spawned).toEqual([600, 601]);
755 yield* TestClock.adjust(Duration.millis(1));
756 yield* Deferred.await(spawnSignals[2]!);
757 expect(spawned).toEqual([600, 601, 602]);
758
759 // The third rapid crash doubles the delay.
760 yield* Deferred.succeed(exits[2]!, ChildProcessSpawner.ExitCode(1));
761 yield* TestClock.adjust(Duration.millis(1999));
762 expect(spawned).toEqual([600, 601, 602]);
763 yield* TestClock.adjust(Duration.millis(1));
764 yield* Deferred.await(spawnSignals[3]!);
765 expect(spawned).toEqual([600, 601, 602, 603]);
766 }).pipe(Effect.provide(TestClock.layer())),
767 );
768
769 it.effect("resets the backoff after the connector runs stably", () =>
770 Effect.gen(function* () {
771 const { spawner, spawned, exits, spawnSignals } = yield* makeCrashLoopSpawner(700, 5);
772 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
773
774 yield* runtime.applyConfig({
775 providerKind: "cloudflare_tunnel",
776 connectorToken: "token",
777 });
778
779 // One rapid crash arms the backoff.
780 yield* Deferred.succeed(exits[0]!, ChildProcessSpawner.ExitCode(1));
781 yield* Deferred.await(spawnSignals[1]!);
782
783 // The replacement stays up past the stable-uptime window, so its exit
784 // restarts immediately and the backoff starts over.
785 yield* TestClock.adjust(Duration.millis(30_000));
786 yield* Deferred.succeed(exits[1]!, ChildProcessSpawner.ExitCode(1));
787 yield* Deferred.await(spawnSignals[2]!);
788 yield* Deferred.succeed(exits[2]!, ChildProcessSpawner.ExitCode(1));
789 yield* Deferred.await(spawnSignals[3]!);
790
791 // The next rapid crash waits the base delay again, not a doubled one.
792 yield* Deferred.succeed(exits[3]!, ChildProcessSpawner.ExitCode(1));
793 yield* TestClock.adjust(Duration.millis(999));
794 expect(spawned).toEqual([700, 701, 702, 703]);
795 yield* TestClock.adjust(Duration.millis(1));
796 yield* Deferred.await(spawnSignals[4]!);
797 expect(spawned).toEqual([700, 701, 702, 703, 704]);
798 }).pipe(Effect.provide(TestClock.layer())),
799 );
800
801 it.effect("a recovery that returns the same config keeps the crash backoff", () =>
802 Effect.gen(function* () {
803 const { spawner, spawned, exits, spawnSignals } = yield* makeCrashLoopSpawner(900, 4);
804 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
805 const config = {
806 providerKind: "cloudflare_tunnel" as const,
807 connectorToken: "same-token",
808 tunnelId: "same-tunnel",
809 };
810 // The startup consumer re-applies whatever the relay hands back. When the
811 // relay confirms the current tunnel, that must not look like a config change.
812 yield* runtime.recoveryRequests.pipe(
813 Stream.runForEach((requested) => runtime.applyConfig(requested).pipe(Effect.asVoid)),
814 Effect.forkChild,
815 );
816
817 yield* runtime.applyConfig(config);
818 yield* Deferred.succeed(exits[0]!, ChildProcessSpawner.ExitCode(1));
819 yield* Deferred.await(spawnSignals[1]!);
820 expect(spawned).toEqual([900, 901]);
821
822 // Second rapid crash still waits out the base delay.
823 yield* Deferred.succeed(exits[1]!, ChildProcessSpawner.ExitCode(1));
824 yield* TestClock.adjust(Duration.millis(999));
825 expect(spawned).toEqual([900, 901]);
826 yield* TestClock.adjust(Duration.millis(1));
827 yield* Deferred.await(spawnSignals[2]!);
828 expect(spawned).toEqual([900, 901, 902]);
829 }).pipe(Effect.provide(TestClock.layer())),
830 );
831
832 it.effect("an explicit config change clears the backoff and preempts a delayed restart", () =>
833 Effect.gen(function* () {
834 const { spawner, spawned, exits, spawnSignals } = yield* makeCrashLoopSpawner(800, 3);
835 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
836
837 yield* runtime.applyConfig({
838 providerKind: "cloudflare_tunnel",
839 connectorToken: "token-1",
840 });
841 yield* Deferred.succeed(exits[0]!, ChildProcessSpawner.ExitCode(1));
842 yield* Deferred.await(spawnSignals[1]!);
843
844 // Leave the supervisor sleeping on the base delay, then change config.
845 yield* Deferred.succeed(exits[1]!, ChildProcessSpawner.ExitCode(1));
846 const status = yield* runtime.applyConfig({
847 providerKind: "cloudflare_tunnel",
848 connectorToken: "token-2",
849 });
850 expect(status).toMatchObject({ status: "running", pid: 802 });
851 expect(spawned).toEqual([800, 801, 802]);
852
853 // The preempted supervisor wakes later and must not spawn a duplicate.
854 yield* TestClock.adjust(Duration.millis(60_000));
855 expect(spawned).toEqual([800, 801, 802]);
856 }).pipe(Effect.provide(TestClock.layer())),
857 );
858
859 it.effect("serializes concurrent connector config changes", () =>
860 Effect.gen(function* () {
861 const spawned: Array<number> = [];
862 const killed: Array<number> = [];
863 const firstSpawnEntered = yield* Deferred.make<void>();
864 const releaseFirstSpawn = yield* Deferred.make<void>();
865 const spawner = ChildProcessSpawner.make(() =>
866 Effect.gen(function* () {
867 const pid = 500 + spawned.length;
868 spawned.push(pid);
869 if (pid === 500) {
870 yield* Deferred.succeed(firstSpawnEntered, undefined);
871 yield* Deferred.await(releaseFirstSpawn);
872 }
873 const handle = makeHandle({
874 pid,
875 onKill: () => {
876 killed.push(pid);
877 },
878 });
879 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
880 return handle;
881 }),
882 );
883 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
884
885 const first = yield* runtime
886 .applyConfig({
887 providerKind: "cloudflare_tunnel",
888 connectorToken: "token-1",
889 })
890 .pipe(Effect.forkChild);
891 yield* Deferred.await(firstSpawnEntered);
892 const second = yield* runtime
893 .applyConfig({
894 providerKind: "cloudflare_tunnel",
895 connectorToken: "token-2",
896 })
897 .pipe(Effect.forkChild);
898 yield* Deferred.succeed(releaseFirstSpawn, undefined);
899
900 yield* Fiber.join(first);
901 const status = yield* Fiber.join(second);
902
903 expect(status).toMatchObject({ status: "running", pid: 501 });
904 expect(spawned).toEqual([500, 501]);
905 expect(killed).toEqual([500]);
906 }),
907 );
908
909 it.effect("reports connector spawn failures", () =>
910 Effect.gen(function* () {
911 const spawner = ChildProcessSpawner.make(() =>
912 Effect.fail(
913 PlatformError.systemError({
914 _tag: "NotFound",
915 module: "ChildProcess",
916 method: "spawn",
917 description: "cloudflared missing",
918 }),
919 ),
920 );
921 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
922
923 const status = yield* runtime.applyConfig({
924 providerKind: "cloudflare_tunnel",
925 connectorToken: "token",
926 tunnelId: "tunnel-1",
927 });
928
929 expect(status).toMatchObject({
930 status: "failed",
931 providerKind: "cloudflare_tunnel",
932 tunnelId: "tunnel-1",
933 });
934 }),
935 );
936
937 it.effect("reports a missing relay client executable without spawning", () =>
938 Effect.gen(function* () {
939 const spawn = vi.fn();
940 const spawner = ChildProcessSpawner.make(spawn);
941 const runtime = yield* buildCloudManagedEndpointRuntime(
942 spawner,
943 Layer.succeed(
944 RelayClient.RelayClient,
945 RelayClient.RelayClient.of({
946 resolve: Effect.succeed({
947 status: "missing",
948 version: RelayClient.CLOUDFLARED_VERSION,
949 }),
950 install: Effect.die("unused"),
951 installWithProgress: () => Effect.die("unused"),
952 pruneManagedVersions: Effect.void,
953 }),
954 ),
955 );
956
957 const status = yield* runtime.applyConfig({
958 providerKind: "cloudflare_tunnel",
959 connectorToken: "token",
960 });
961
962 expect(status).toEqual({
963 status: "failed",
964 providerKind: "cloudflare_tunnel",
965 failure: "not-installed",
966 reason: "The relay client is not installed.",
967 });
968 expect(spawn).not.toHaveBeenCalled();
969 }),
970 );
971
972 it.effect(
973 "runs an older relay client while the pinned one installs, then hands off to it and prunes",
974 () =>
975 Effect.gen(function* () {
976 const olderClient = {
977 status: "available",
978 executablePath: "/managed/2025.9.0/cloudflared",
979 source: "managed",
980 version: "2025.9.0",
981 } as const;
982 const pinnedClient = {
983 status: "available",
984 executablePath: "/managed/pinned/cloudflared",
985 source: "managed",
986 version: RelayClient.CLOUDFLARED_VERSION,
987 } as const;
988 let installed = false;
989 const releaseInstall = yield* Deferred.make<void>();
990 const pruned = yield* Deferred.make<void>();
991 const spawned = yield* Queue.unbounded<string>();
992 const olderStopped = yield* Deferred.make<void>();
993 const output = yield* Queue.unbounded<Uint8Array>();
994 const spawner = ChildProcessSpawner.make((command) =>
995 Effect.gen(function* () {
996 const executable = ChildProcess.isStandardCommand(command) ? command.command : "";
997 yield* Queue.offer(spawned, executable);
998 const handle = makeHandle({
999 pid: executable === pinnedClient.executablePath ? 2 : 1,
1000 onKill: () => {
1001 if (executable === olderClient.executablePath) {
1002 Deferred.doneUnsafe(olderStopped, Effect.void);
1003 }
1004 },
1005 ...(executable === pinnedClient.executablePath
1006 ? { output: Stream.fromQueue(output) }
1007 : {}),
1008 });
1009 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1010 return handle;
1011 }),
1012 );
1013 const runtime = yield* buildCloudManagedEndpointRuntime(
1014 spawner,
1015 Layer.succeed(
1016 RelayClient.RelayClient,
1017 RelayClient.RelayClient.of({
1018 resolve: Effect.sync(() => (installed ? pinnedClient : olderClient)),
1019 install: Deferred.await(releaseInstall).pipe(
1020 Effect.andThen(
1021 Effect.sync(() => {
1022 installed = true;
1023 return pinnedClient;
1024 }),
1025 ),
1026 ),
1027 installWithProgress: () => Effect.die("unused"),
1028 pruneManagedVersions: Deferred.succeed(pruned, undefined).pipe(Effect.asVoid),
1029 }),
1030 ),
1031 );
1032
1033 const status = yield* runtime.applyConfig({
1034 providerKind: "cloudflare_tunnel",
1035 connectorToken: "token",
1036 });
1037 expect(status).toMatchObject({ status: "running", pid: 1 });
1038 expect(yield* Queue.take(spawned)).toBe(olderClient.executablePath);
1039
1040 yield* Deferred.succeed(releaseInstall, undefined);
1041 expect(yield* Queue.take(spawned)).toBe(pinnedClient.executablePath);
1042 // The older connector keeps serving until the pinned one registers.
1043 expect(Option.isNone(yield* Deferred.poll(olderStopped))).toBe(true);
1044
1045 yield* Queue.offer(
1046 output,
1047 new TextEncoder().encode(
1048 "2026-10-08T00:00:00Z INF Registered tunnel connection connIndex=0\n",
1049 ),
1050 );
1051 yield* Deferred.await(olderStopped);
1052 yield* Deferred.await(pruned);
1053 }),
1054 );
1055
1056 it.effect("installs a missing relay client in the background and then starts it", () =>
1057 Effect.gen(function* () {
1058 const pinnedClient = {
1059 status: "available",
1060 executablePath: "/managed/pinned/cloudflared",
1061 source: "managed",
1062 version: RelayClient.CLOUDFLARED_VERSION,
1063 } as const;
1064 let installed = false;
1065 const spawned = yield* Deferred.make<string>();
1066 const spawner = ChildProcessSpawner.make((command) =>
1067 Effect.gen(function* () {
1068 yield* Deferred.succeed(
1069 spawned,
1070 ChildProcess.isStandardCommand(command) ? command.command : "",
1071 );
1072 const handle = makeHandle({ pid: 3, onKill: () => {} });
1073 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1074 return handle;
1075 }),
1076 );
1077 const runtime = yield* buildCloudManagedEndpointRuntime(
1078 spawner,
1079 Layer.succeed(
1080 RelayClient.RelayClient,
1081 RelayClient.RelayClient.of({
1082 resolve: Effect.sync(() =>
1083 installed
1084 ? pinnedClient
1085 : { status: "missing" as const, version: RelayClient.CLOUDFLARED_VERSION },
1086 ),
1087 install: Effect.sync(() => {
1088 installed = true;
1089 return pinnedClient;
1090 }),
1091 installWithProgress: () => Effect.die("unused"),
1092 pruneManagedVersions: Effect.void,
1093 }),
1094 ),
1095 );
1096
1097 const status = yield* runtime.applyConfig({
1098 providerKind: "cloudflare_tunnel",
1099 connectorToken: "token",
1100 });
1101 // The first attempt reports the gap; the install then starts the connector itself.
1102 expect(status).toMatchObject({ status: "failed", failure: "not-installed" });
1103 expect(yield* Deferred.await(spawned)).toBe(pinnedClient.executablePath);
1104 }),
1105 );
1106
1107 it.effect("leaves a PATH relay client alone", () =>
1108 Effect.gen(function* () {
1109 const install = vi.fn(() => Effect.die("unexpected install"));
1110 const spawner = ChildProcessSpawner.make(() =>
1111 Effect.gen(function* () {
1112 const handle = makeHandle({ pid: 8, onKill: () => {} });
1113 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1114 return handle;
1115 }),
1116 );
1117 const runtime = yield* buildCloudManagedEndpointRuntime(
1118 spawner,
1119 Layer.succeed(
1120 RelayClient.RelayClient,
1121 RelayClient.RelayClient.of({
1122 resolve: Effect.succeed({
1123 status: "available",
1124 executablePath: "/opt/homebrew/bin/cloudflared",
1125 source: "path",
1126 version: "2026.10.0",
1127 }),
1128 install: Effect.suspend(install),
1129 installWithProgress: () => Effect.die("unused"),
1130 pruneManagedVersions: Effect.void,
1131 }),
1132 ),
1133 );
1134 expect(
1135 yield* runtime.applyConfig({ providerKind: "cloudflare_tunnel", connectorToken: "token" }),
1136 ).toMatchObject({ status: "running", pid: 8 });
1137 yield* TestClock.adjust(Duration.minutes(30));
1138 expect(install).not.toHaveBeenCalled();
1139 }),
1140 );
1141
1142 it.effect("leaves an override relay client alone", () =>
1143 Effect.gen(function* () {
1144 const install = vi.fn(() => Effect.die("unexpected install"));
1145 const spawner = ChildProcessSpawner.make(() =>
1146 Effect.gen(function* () {
1147 const handle = makeHandle({ pid: 4, onKill: () => {} });
1148 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1149 return handle;
1150 }),
1151 );
1152 const runtime = yield* buildCloudManagedEndpointRuntime(
1153 spawner,
1154 Layer.succeed(
1155 RelayClient.RelayClient,
1156 RelayClient.RelayClient.of({
1157 resolve: Effect.succeed({
1158 status: "available",
1159 executablePath: "/opt/cloudflared",
1160 source: "override",
1161 version: "2025.10.0",
1162 }),
1163 install: Effect.suspend(install),
1164 installWithProgress: () => Effect.die("unused"),
1165 pruneManagedVersions: Effect.void,
1166 }),
1167 ),
1168 );
1169 expect(
1170 yield* runtime.applyConfig({ providerKind: "cloudflare_tunnel", connectorToken: "token" }),
1171 ).toMatchObject({ status: "running", pid: 4 });
1172 yield* Effect.yieldNow;
1173 expect(install).not.toHaveBeenCalled();
1174 }),
1175 );
1176
1177 it.effect("retries a failed background install while the older connector keeps running", () =>
1178 Effect.gen(function* () {
1179 const olderClient = {
1180 status: "available",
1181 executablePath: "/managed/2025.9.0/cloudflared",
1182 source: "managed",
1183 version: "2025.9.0",
1184 } as const;
1185 const pinnedClient = {
1186 status: "available",
1187 executablePath: "/managed/pinned/cloudflared",
1188 source: "managed",
1189 version: RelayClient.CLOUDFLARED_VERSION,
1190 } as const;
1191 let installed = false;
1192 let installAttempts = 0;
1193 const spawned = yield* Queue.unbounded<string>();
1194 const spawner = ChildProcessSpawner.make((command) =>
1195 Effect.gen(function* () {
1196 yield* Queue.offer(
1197 spawned,
1198 ChildProcess.isStandardCommand(command) ? command.command : "",
1199 );
1200 const handle = makeHandle({ pid: 5, onKill: () => {} });
1201 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1202 return handle;
1203 }),
1204 );
1205 const runtime = yield* buildCloudManagedEndpointRuntime(
1206 spawner,
1207 Layer.succeed(
1208 RelayClient.RelayClient,
1209 RelayClient.RelayClient.of({
1210 resolve: Effect.sync(() => (installed ? pinnedClient : olderClient)),
1211 install: Effect.suspend(() => {
1212 installAttempts += 1;
1213 if (installAttempts === 1) {
1214 // A real download fails some time after it starts.
1215 return Effect.sleep(Duration.seconds(30)).pipe(
1216 Effect.andThen(
1217 Effect.fail(
1218 new RelayClient.RelayClientInstallError({
1219 reason: "download_failed",
1220 message: "offline",
1221 }),
1222 ),
1223 ),
1224 );
1225 }
1226 installed = true;
1227 return Effect.succeed(pinnedClient);
1228 }),
1229 installWithProgress: () => Effect.die("unused"),
1230 pruneManagedVersions: Effect.void,
1231 }),
1232 ),
1233 );
1234
1235 yield* runtime.applyConfig({ providerKind: "cloudflare_tunnel", connectorToken: "token" });
1236 expect(yield* Queue.take(spawned)).toBe(olderClient.executablePath);
1237 yield* TestClock.adjust(Duration.seconds(30));
1238 expect(installAttempts).toBe(1);
1239
1240 yield* TestClock.adjust(Duration.minutes(10));
1241 expect(yield* Queue.take(spawned)).toBe(pinnedClient.executablePath);
1242 expect(installAttempts).toBe(2);
1243 }),
1244 );
1245
1246 it.effect("keeps the older connector when the updated relay client fails to start", () =>
1247 Effect.gen(function* () {
1248 const olderClient = {
1249 status: "available",
1250 executablePath: "/managed/2025.9.0/cloudflared",
1251 source: "managed",
1252 version: "2025.9.0",
1253 } as const;
1254 const pinnedClient = {
1255 status: "available",
1256 executablePath: "/managed/pinned/cloudflared",
1257 source: "managed",
1258 version: RelayClient.CLOUDFLARED_VERSION,
1259 } as const;
1260 let installed = false;
1261 let olderKilled = false;
1262 const releaseInstall = yield* Deferred.make<void>();
1263 const pinnedAttempted = yield* Deferred.make<void>();
1264 const spawner = ChildProcessSpawner.make((command) =>
1265 Effect.gen(function* () {
1266 const executable = ChildProcess.isStandardCommand(command) ? command.command : "";
1267 if (executable === pinnedClient.executablePath) {
1268 yield* Deferred.succeed(pinnedAttempted, undefined);
1269 return yield* PlatformError.systemError({
1270 _tag: "PermissionDenied",
1271 module: "ChildProcess",
1272 method: "spawn",
1273 });
1274 }
1275 const handle = makeHandle({
1276 pid: 6,
1277 onKill: () => {
1278 olderKilled = true;
1279 },
1280 });
1281 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1282 return handle;
1283 }),
1284 );
1285 const runtime = yield* buildCloudManagedEndpointRuntime(
1286 spawner,
1287 Layer.succeed(
1288 RelayClient.RelayClient,
1289 RelayClient.RelayClient.of({
1290 resolve: Effect.sync(() => (installed ? pinnedClient : olderClient)),
1291 install: Deferred.await(releaseInstall).pipe(
1292 Effect.andThen(
1293 Effect.sync(() => {
1294 installed = true;
1295 return pinnedClient;
1296 }),
1297 ),
1298 ),
1299 installWithProgress: () => Effect.die("unused"),
1300 pruneManagedVersions: Effect.void,
1301 }),
1302 ),
1303 );
1304 const config = { providerKind: "cloudflare_tunnel", connectorToken: "token" } as const;
1305 expect(yield* runtime.applyConfig(config)).toMatchObject({ status: "running", pid: 6 });
1306
1307 yield* Deferred.succeed(releaseInstall, undefined);
1308 yield* Deferred.await(pinnedAttempted);
1309 // The failed swap leaves the older connector active and running.
1310 expect(yield* runtime.applyConfig(config)).toMatchObject({ status: "running", pid: 6 });
1311 expect(olderKilled).toBe(false);
1312 }),
1313 );
1314
1315 it.effect("requests recovery when the older connector exited during a failed swap", () =>
1316 Effect.gen(function* () {
1317 const olderClient = {
1318 status: "available",
1319 executablePath: "/managed/2025.9.0/cloudflared",
1320 source: "managed",
1321 version: "2025.9.0",
1322 } as const;
1323 const pinnedClient = {
1324 status: "available",
1325 executablePath: "/managed/pinned/cloudflared",
1326 source: "managed",
1327 version: RelayClient.CLOUDFLARED_VERSION,
1328 } as const;
1329 let installed = false;
1330 const releaseInstall = yield* Deferred.make<void>();
1331 const spawner = ChildProcessSpawner.make((command) =>
1332 Effect.gen(function* () {
1333 const executable = ChildProcess.isStandardCommand(command) ? command.command : "";
1334 if (executable === pinnedClient.executablePath) {
1335 return yield* PlatformError.systemError({
1336 _tag: "PermissionDenied",
1337 module: "ChildProcess",
1338 method: "spawn",
1339 });
1340 }
1341 // The older connector has exited by the time the swap fails.
1342 const handle = makeHandle({ pid: 12, onKill: () => {}, isRunning: () => !installed });
1343 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1344 return handle;
1345 }),
1346 );
1347 const runtime = yield* buildCloudManagedEndpointRuntime(
1348 spawner,
1349 Layer.succeed(
1350 RelayClient.RelayClient,
1351 RelayClient.RelayClient.of({
1352 resolve: Effect.sync(() => (installed ? pinnedClient : olderClient)),
1353 install: Deferred.await(releaseInstall).pipe(
1354 Effect.andThen(
1355 Effect.sync(() => {
1356 installed = true;
1357 return pinnedClient;
1358 }),
1359 ),
1360 ),
1361 installWithProgress: () => Effect.die("unused"),
1362 pruneManagedVersions: Effect.void,
1363 }),
1364 ),
1365 );
1366 const config = { providerKind: "cloudflare_tunnel", connectorToken: "token" } as const;
1367 expect(yield* runtime.applyConfig(config)).toMatchObject({ status: "running", pid: 12 });
1368
1369 yield* Deferred.succeed(releaseInstall, undefined);
1370 expect(Option.getOrThrow(yield* Stream.runHead(runtime.recoveryRequests))).toEqual(config);
1371 }),
1372 );
1373
1374 it.effect("restarts a self-updated pinned-folder connector once the pin is reinstalled", () =>
1375 Effect.gen(function* () {
1376 const executablePath = "/managed/pinned/cloudflared";
1377 let version = "2026.9.3";
1378 const releaseInstall = yield* Deferred.make<void>();
1379 const spawned = yield* Queue.unbounded<string>();
1380 const spawner = ChildProcessSpawner.make(() =>
1381 Effect.gen(function* () {
1382 yield* Queue.offer(spawned, version);
1383 const handle = makeHandle({ pid: 7, onKill: () => {} });
1384 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1385 return handle;
1386 }),
1387 );
1388 const current = () =>
1389 ({ status: "available", executablePath, source: "managed", version }) as const;
1390 const runtime = yield* buildCloudManagedEndpointRuntime(
1391 spawner,
1392 Layer.succeed(
1393 RelayClient.RelayClient,
1394 RelayClient.RelayClient.of({
1395 resolve: Effect.sync(current),
1396 install: Deferred.await(releaseInstall).pipe(
1397 Effect.andThen(
1398 Effect.sync(() => {
1399 version = RelayClient.CLOUDFLARED_VERSION;
1400 return current();
1401 }),
1402 ),
1403 ),
1404 installWithProgress: () => Effect.die("unused"),
1405 pruneManagedVersions: Effect.void,
1406 }),
1407 ),
1408 );
1409
1410 yield* runtime.applyConfig({ providerKind: "cloudflare_tunnel", connectorToken: "token" });
1411 expect(yield* Queue.take(spawned)).toBe("2026.9.3");
1412 yield* Deferred.succeed(releaseInstall, undefined);
1413 expect(yield* Queue.take(spawned)).toBe(RelayClient.CLOUDFLARED_VERSION);
1414 }),
1415 );
1416
1417 it.effect("retries a failed install when no relay client is installed", () =>
1418 Effect.gen(function* () {
1419 const pinnedClient = {
1420 status: "available",
1421 executablePath: "/managed/pinned/cloudflared",
1422 source: "managed",
1423 version: RelayClient.CLOUDFLARED_VERSION,
1424 } as const;
1425 let installed = false;
1426 let installAttempts = 0;
1427 const spawned = yield* Deferred.make<string>();
1428 const spawner = ChildProcessSpawner.make((command) =>
1429 Effect.gen(function* () {
1430 yield* Deferred.succeed(
1431 spawned,
1432 ChildProcess.isStandardCommand(command) ? command.command : "",
1433 );
1434 const handle = makeHandle({ pid: 9, onKill: () => {} });
1435 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1436 return handle;
1437 }),
1438 );
1439 const runtime = yield* buildCloudManagedEndpointRuntime(
1440 spawner,
1441 Layer.succeed(
1442 RelayClient.RelayClient,
1443 RelayClient.RelayClient.of({
1444 resolve: Effect.sync(() =>
1445 installed
1446 ? pinnedClient
1447 : { status: "missing" as const, version: RelayClient.CLOUDFLARED_VERSION },
1448 ),
1449 install: Effect.suspend(() => {
1450 installAttempts += 1;
1451 if (installAttempts === 1) {
1452 return Effect.fail(
1453 new RelayClient.RelayClientInstallError({
1454 reason: "download_failed",
1455 message: "offline",
1456 }),
1457 );
1458 }
1459 installed = true;
1460 return Effect.succeed(pinnedClient);
1461 }),
1462 installWithProgress: () => Effect.die("unused"),
1463 pruneManagedVersions: Effect.void,
1464 }),
1465 ),
1466 );
1467
1468 expect(
1469 yield* runtime.applyConfig({ providerKind: "cloudflare_tunnel", connectorToken: "token" }),
1470 ).toMatchObject({ status: "failed", failure: "not-installed" });
1471 yield* Effect.yieldNow;
1472 expect(installAttempts).toBe(1);
1473
1474 yield* TestClock.adjust(Duration.minutes(10));
1475 expect(yield* Deferred.await(spawned)).toBe(pinnedClient.executablePath);
1476 expect(installAttempts).toBe(2);
1477 }),
1478 );
1479
1480 it.effect("restarts on the pin when the first post-install probe failed", () =>
1481 Effect.gen(function* () {
1482 const olderClient = {
1483 status: "available",
1484 executablePath: "/managed/2025.9.0/cloudflared",
1485 source: "managed",
1486 version: "2025.9.0",
1487 } as const;
1488 const pinnedClient = {
1489 status: "available",
1490 executablePath: "/managed/pinned/cloudflared",
1491 source: "managed",
1492 version: RelayClient.CLOUDFLARED_VERSION,
1493 } as const;
1494 let installed = false;
1495 // The first resolve after the install misses the new binary, as when a
1496 // scanner briefly holds it; later resolves see it.
1497 let missedProbes = 0;
1498 const spawned = yield* Queue.unbounded<string>();
1499 const spawner = ChildProcessSpawner.make((command) =>
1500 Effect.gen(function* () {
1501 yield* Queue.offer(
1502 spawned,
1503 ChildProcess.isStandardCommand(command) ? command.command : "",
1504 );
1505 const handle = makeHandle({ pid: 10, onKill: () => {} });
1506 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1507 return handle;
1508 }),
1509 );
1510 const runtime = yield* buildCloudManagedEndpointRuntime(
1511 spawner,
1512 Layer.succeed(
1513 RelayClient.RelayClient,
1514 RelayClient.RelayClient.of({
1515 resolve: Effect.sync(() => {
1516 if (installed && missedProbes === 0) {
1517 missedProbes += 1;
1518 return olderClient;
1519 }
1520 return installed ? pinnedClient : olderClient;
1521 }),
1522 install: Effect.sync(() => {
1523 installed = true;
1524 return pinnedClient;
1525 }),
1526 installWithProgress: () => Effect.die("unused"),
1527 pruneManagedVersions: Effect.void,
1528 }),
1529 ),
1530 );
1531
1532 yield* runtime.applyConfig({ providerKind: "cloudflare_tunnel", connectorToken: "token" });
1533 expect(yield* Queue.take(spawned)).toBe(olderClient.executablePath);
1534 yield* Effect.yieldNow;
1535 // The missed probe kept the older connector running.
1536 expect(Option.isNone(yield* Queue.poll(spawned))).toBe(true);
1537
1538 yield* TestClock.adjust(Duration.minutes(10));
1539 expect(yield* Queue.take(spawned)).toBe(pinnedClient.executablePath);
1540 }),
1541 );
1542
1543 it.effect("starts the connector when the pin landed but its first probe failed", () =>
1544 Effect.gen(function* () {
1545 const pinnedClient = {
1546 status: "available",
1547 executablePath: "/managed/pinned/cloudflared",
1548 source: "managed",
1549 version: RelayClient.CLOUDFLARED_VERSION,
1550 } as const;
1551 const missing = { status: "missing" as const, version: RelayClient.CLOUDFLARED_VERSION };
1552 let installed = false;
1553 // The resolve right after the install misses the new binary.
1554 let missedProbes = 0;
1555 const spawned = yield* Deferred.make<string>();
1556 const spawner = ChildProcessSpawner.make((command) =>
1557 Effect.gen(function* () {
1558 yield* Deferred.succeed(
1559 spawned,
1560 ChildProcess.isStandardCommand(command) ? command.command : "",
1561 );
1562 const handle = makeHandle({ pid: 11, onKill: () => {} });
1563 yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
1564 return handle;
1565 }),
1566 );
1567 const runtime = yield* buildCloudManagedEndpointRuntime(
1568 spawner,
1569 Layer.succeed(
1570 RelayClient.RelayClient,
1571 RelayClient.RelayClient.of({
1572 resolve: Effect.sync(() => {
1573 if (!installed) return missing;
1574 if (missedProbes === 0) {
1575 missedProbes += 1;
1576 return missing;
1577 }
1578 return pinnedClient;
1579 }),
1580 install: Effect.sync(() => {
1581 installed = true;
1582 return pinnedClient;
1583 }),
1584 installWithProgress: () => Effect.die("unused"),
1585 pruneManagedVersions: Effect.void,
1586 }),
1587 ),
1588 );
1589
1590 expect(
1591 yield* runtime.applyConfig({ providerKind: "cloudflare_tunnel", connectorToken: "token" }),
1592 ).toMatchObject({ status: "failed", failure: "not-installed" });
1593 yield* Effect.yieldNow;
1594 expect(Option.isNone(yield* Deferred.poll(spawned))).toBe(true);
1595
1596 yield* TestClock.adjust(Duration.minutes(10));
1597 expect(yield* Deferred.await(spawned)).toBe(pinnedClient.executablePath);
1598 }),
1599 );
1600});