1import { describe, expect, it } from "@effect/vitest";
2import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as TestClock from "effect/testing/TestClock";
6
7import {
8 managedTunnelStartupAction,
9 retryManagedTunnelRegistration,
10} from "./managedTunnelStartup.ts";
11
12describe("managedTunnelStartupAction", () => {
13 const config = {
14 providerKind: "cloudflare_tunnel" as const,
15 connectorToken: "connector-token",
16 tunnelId: "tunnel-1",
17 };
18
19 it("requests tunnel recovery only when the relay proves it is needed", () => {
20 expect(
21 managedTunnelStartupAction({
22 wantsCliLink: true,
23 registration: { status: "recovery_required", config },
24 }),
25 ).toEqual({ action: "request_recovery", config });
26 });
27
28 it("creates a desired CLI link only when no local managed link exists", () => {
29 expect(
30 managedTunnelStartupAction({
31 wantsCliLink: true,
32 registration: { status: "not_linked" },
33 }),
34 ).toEqual({ action: "reconcile_link" });
35 });
36
37 it.each(["ready", "unavailable", "revoked"] as const)(
38 "does not provision after a %s registration result",
39 (status) => {
40 expect(
41 managedTunnelStartupAction({
42 wantsCliLink: true,
43 registration: { status },
44 }),
45 ).toEqual({ action: "none" });
46 },
47 );
48});
49
50describe("retryManagedTunnelRegistration", () => {
51 it.effect("does not fall back or retry when registration is permanently rejected", () =>
52 Effect.gen(function* () {
53 let attempts = 0;
54 let fallbacks = 0;
55 const error = yield* Effect.flip(
56 retryManagedTunnelRegistration(
57 Effect.suspend(() => {
58 attempts += 1;
59 return Effect.fail("not authorized");
60 }),
61 () => false,
62 Effect.sync(() => {
63 fallbacks += 1;
64 }),
65 ),
66 );
67 expect(error).toBe("not authorized");
68 expect(attempts).toBe(1);
69 expect(fallbacks).toBe(0);
70 }),
71 );
72
73 it.effect("stops retrying after the retry window so startup can fall back", () =>
74 Effect.gen(function* () {
75 let attempts = 0;
76 const registration = Effect.suspend(() => {
77 attempts += 1;
78 return Effect.fail("relay unavailable" as const);
79 });
80 const fiber = yield* Effect.forkChild(
81 Effect.flip(retryManagedTunnelRegistration(registration, () => true)),
82 { startImmediately: true },
83 );
84 yield* TestClock.adjust("15 minutes");
85 expect(yield* Fiber.join(fiber)).toBe("relay unavailable");
86 // Capped at 30 seconds between attempts, ten minutes allows a bounded run.
87 expect(attempts).toBeGreaterThan(5);
88 expect(attempts).toBeLessThan(60);
89 }),
90 );
91
92 it.effect("waits for successful registration before it activates the connector", () =>
93 Effect.gen(function* () {
94 const firstAttempt = yield* Deferred.make<void>();
95 let attempts = 0;
96 let activations = 0;
97 let reconciliations = 0;
98 const registration = Effect.suspend(() => {
99 attempts += 1;
100 if (attempts === 1) {
101 return Deferred.succeed(firstAttempt, undefined).pipe(
102 Effect.andThen(Effect.fail("relay unavailable" as const)),
103 );
104 }
105 return Effect.succeed({ status: "ready" as const });
106 });
107 const startup = retryManagedTunnelRegistration(registration, () => true).pipe(
108 Effect.tap((result) =>
109 Effect.sync(() => {
110 const action = managedTunnelStartupAction({ wantsCliLink: true, registration: result });
111 if (action.action === "reconcile_link") {
112 reconciliations += 1;
113 }
114 activations += 1;
115 }),
116 ),
117 );
118
119 const fiber = yield* Effect.forkChild(startup, { startImmediately: true });
120 yield* Deferred.await(firstAttempt);
121 expect(attempts).toBe(1);
122 expect(activations).toBe(0);
123
124 yield* TestClock.adjust("2 seconds");
125 yield* Fiber.join(fiber);
126
127 expect(attempts).toBe(2);
128 expect(activations).toBe(1);
129 expect(reconciliations).toBe(0);
130 }),
131 );
132});