1import type { FlyMachineConfig } from "@distilled.cloud/fly-io/machines";
2import { deploymentPolicy, validateDeployment } from "@/Fly/Deployment";
3import {
4 predecessorShutdown,
5 sameChecks,
6 sameServices,
7 toFlyService,
8 toFlyServiceCheck,
9} from "@/Fly/replicas";
10import { expect, it } from "alchemy-test";
11import * as Effect from "effect/Effect";
12
13it.effect(
14 "deployment defaults preserve existing behavior",
15 () =>
16 Effect.gen(function* () {
17 const rolling = yield* deploymentPolicy(undefined, undefined);
18 expect(rolling.bluegreen).toBe(false);
19 expect(rolling.shutdown).toBeUndefined();
20 const bluegreen = yield* deploymentPolicy(
21 { strategy: "bluegreen" },
22 undefined,
23 );
24 expect(bluegreen.healthTimeoutMs).toBe(60_000);
25 expect(bluegreen.shutdown).toEqual({
26 signal: "SIGTERM",
27 timeout: "30000ms",
28 timeoutMs: 30_000,
29 });
30 }),
31 { tags: ["unit", "provider:fly", "provider:fly:service", "local"] },
32);
33
34it.effect(
35 "service intervals honor Fly's one-minute cap without capping named checks",
36 () =>
37 Effect.sync(() => {
38 for (const [requested, effective] of [
39 ["75s", "60s"],
40 ["1m15s", "60s"],
41 ["60s", "60s"],
42 ["59.999s", "59.999s"],
43 ] as const) {
44 const check = { type: "http" as const, port: 80, interval: requested };
45 expect(
46 toFlyService({ protocol: "tcp", internalPort: 80, checks: [check] })
47 .checks?.[0]?.interval,
48 ).toBe(effective);
49 expect(toFlyServiceCheck(check).interval).toBe(requested);
50 }
51 }),
52 { tags: ["unit", "provider:fly", "provider:fly:service", "local"] },
53);
54
55it.effect(
56 "check durations compare exactly at nanosecond precision",
57 () =>
58 Effect.sync(() => {
59 const cases: Array<[string | undefined, string | undefined, boolean]> = [
60 ["1001ms", "1.001s", true],
61 ["1001ms", "1.001000001s", false],
62 ["65s", "1m5s", true],
63 ["1.s", ".5s500ms", true],
64 ["1us", "1µs", true],
65 ["1μs", "1000ns", true],
66 ["+1.001s", "1001ms", true],
67 ["-1.001s", "-1001ms", true],
68 ["-1ns", "1ns", false],
69 ["0", "0s", true],
70 ["-0", "+0s", true],
71 ["0.9ns0.9ns", "0ns", true],
72 ["9007199254740992ns", "9007199254740993ns", false],
73 ["9223372036854775807ns", "9223372036.854775807s", true],
74 ["-9223372036854775808ns", "-9223372036.854775808s", true],
75 ["9223372036854775808ns", "9223372036.854775808s", false],
76 ["-9223372036854775809ns", "-9223372036.854775809s", false],
77 ["9223372036854775807ns1ns", "9223372036.854775808s", false],
78 ["999999999999999999999999h", "999999999999999999999999h0s", false],
79 ["1e3ms", "1s", false],
80 ["1s1e3ms", "2s", false],
81 ["1d", "24h", false],
82 ["1s ", "1s", false],
83 ["1s!", "1s?", false],
84 ["--1s", "1s", false],
85 ["1s-1s", "0s", false],
86 ["1", "1ns", false],
87 ["", "0s", false],
88 [undefined, "0s", false],
89 ];
90 for (const field of ["interval", "grace_period", "timeout"] as const) {
91 for (const [observed, desired, equal] of cases) {
92 const left = { type: "http", port: 80, path: "/", [field]: observed };
93 const right = { type: "http", port: 80, path: "/", [field]: desired };
94 expect(sameChecks({ ready: left }, { ready: right })).toBe(equal);
95 expect(
96 sameServices(
97 [{ internal_port: 80, checks: [left] }],
98 [{ internal_port: 80, checks: [right] }],
99 ),
100 ).toBe(equal);
101 }
102 }
103 }),
104 { tags: ["unit", "provider:fly", "provider:fly:service", "local"] },
105);
106
107it.effect(
108 "exact duration parsing preserves shutdown policy validation",
109 () =>
110 Effect.gen(function* () {
111 const policy = yield* predecessorShutdown({
112 config: {
113 stop_config: { timeout: "1.001s" },
114 env: { ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS: "1001" },
115 },
116 });
117 expect(policy.timeoutMs).toBe(1001);
118 for (const timeout of ["0", "0s", "-1s", "+1s", "300.000000001s"]) {
119 const error = yield* predecessorShutdown({
120 config: { stop_config: { timeout } },
121 }).pipe(Effect.flip);
122 expect(error._tag).toBe("Fly.ShutdownPolicyMismatch");
123 }
124 }),
125 { tags: ["unit", "provider:fly", "provider:fly:service", "local"] },
126);
127
128for (const timeout of [0, -1, 300_001, Infinity, 0.1]) {
129 it.effect(
130 `rejects invalid shutdown duration ${timeout}`,
131 () =>
132 Effect.gen(function* () {
133 const error = yield* deploymentPolicy(undefined, { timeout }).pipe(
134 Effect.flip,
135 );
136 expect(error._tag).toBe("Fly.InvalidDeployment");
137 }),
138 { tags: ["unit", "provider:fly", "provider:fly:service", "local"] },
139 );
140}
141
142it.effect(
143 "S10 S11 rejects unsupported bluegreen configuration before mutation",
144 () =>
145 Effect.gen(function* () {
146 const policy = yield* deploymentPolicy(
147 { strategy: "bluegreen" },
148 undefined,
149 );
150 const checks = { ready: { type: "http", port: 80, path: "/" } };
151 const cases: Array<[FlyMachineConfig, boolean, boolean]> = [
152 [{}, false, false],
153 [{ checks }, true, false],
154 [{ checks }, false, true],
155 [{ checks, auto_destroy: true }, false, false],
156 [{ checks, restart: { policy: "no" } }, false, false],
157 [{ checks, services: [{ ports: [{ port: 80 }] }] }, false, false],
158 ];
159 for (const [config, mounted, skip] of cases) {
160 const error = yield* validateDeployment(
161 policy,
162 config,
163 mounted,
164 skip,
165 ).pipe(Effect.flip);
166 expect(error._tag).toBe("Fly.InvalidDeployment");
167 }
168 const signal = yield* deploymentPolicy(
169 undefined,
170 { signal: "SIGQUIT" },
171 true,
172 ).pipe(Effect.flip);
173 expect(signal._tag).toBe("Fly.InvalidDeployment");
174 }),
175 {
176 tags: [
177 "unit",
178 "provider:fly",
179 "provider:fly:machine",
180 "provider:fly:service",
181 "local",
182 ],
183 },
184);
185
186it.effect(
187 "S06 R05 accepts the finite 300-second boundary without a live sleep",
188 () =>
189 Effect.gen(function* () {
190 const policy = yield* deploymentPolicy(
191 { strategy: "bluegreen", healthTimeout: 300_000 },
192 { timeout: 300_000 },
193 );
194 expect(policy.healthTimeoutMs).toBe(300_000);
195 expect(policy.shutdown?.timeoutMs).toBe(300_000);
196 for (const healthTimeout of [0, -1, 300_001, Infinity, NaN, 0.1]) {
197 const error = yield* deploymentPolicy(
198 { strategy: "bluegreen", healthTimeout },
199 undefined,
200 ).pipe(Effect.flip);
201 expect(error._tag).toBe("Fly.InvalidDeployment");
202 }
203 }),
204 { tags: ["unit", "provider:fly", "provider:fly:service", "local"] },
205);
206
207for (const autostop of ["stop", "suspend"] as const) {
208 it.effect(
209 `S11 accepts ${autostop} instead of silently rejecting idle capacity`,
210 () =>
211 Effect.gen(function* () {
212 const policy = yield* deploymentPolicy(
213 { strategy: "bluegreen" },
214 undefined,
215 );
216 yield* validateDeployment(
217 policy,
218 {
219 services: [
220 {
221 internal_port: 80,
222 ports: [{ port: 80 }],
223 checks: [{ type: "http", port: 80, path: "/" }],
224 autostop,
225 autostart: true,
226 min_machines_running: 1,
227 },
228 ],
229 },
230 false,
231 );
232 }),
233 { tags: ["unit", "provider:fly", "provider:fly:service", "local"] },
234 );
235}