1import { GraphQLFailure, type GraphQLIssue } from "@distilled.cloud/railway";
2import {
3 ResourceDeletionPending,
4 waitUntilDeleted,
5} from "@/Railway/GraphQL.ts";
6import { describe, expect, it } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Fiber from "effect/Fiber";
9import * as TestClock from "effect/testing/TestClock";
10
11describe(
12 "Railway deletion confirmation",
13 { tags: ["unit", "provider:railway", "local"] },
14 () => {
15 it.effect(
16 "fails with resource identity when bounded polls never confirm absence",
17 () =>
18 Effect.gen(function* () {
19 let reads = 0;
20 const absent = Effect.sync(() => {
21 reads++;
22 return false;
23 });
24 const fiber = yield* waitUntilDeleted(
25 "Railway.Project",
26 "project-1",
27 absent,
28 ).pipe(Effect.flip, Effect.forkChild({ startImmediately: true }));
29
30 yield* TestClock.adjust("8 seconds");
31 const error = yield* Fiber.join(fiber);
32 expect(error).toBeInstanceOf(ResourceDeletionPending);
33 expect(error._tag).toBe("Railway.ResourceDeletionPending");
34 expect(error.resourceType).toBe("Railway.Project");
35 expect(error.resourceId).toBe("project-1");
36 expect(reads).toBe(9);
37 }),
38 );
39
40 it.effect("succeeds immediately when the first read confirms absence", () =>
41 Effect.gen(function* () {
42 let reads = 0;
43 const result = yield* waitUntilDeleted(
44 "Railway.Service",
45 "service-1",
46 Effect.sync(() => {
47 reads++;
48 return true;
49 }),
50 );
51 expect(result).toBeUndefined();
52 expect(reads).toBe(1);
53 }),
54 );
55
56 it.effect("stops polling once a later read confirms absence", () =>
57 Effect.gen(function* () {
58 let reads = 0;
59 const fiber = yield* waitUntilDeleted(
60 "Railway.Group",
61 "group-1",
62 Effect.sync(() => ++reads === 3),
63 4,
64 ).pipe(Effect.forkChild({ startImmediately: true }));
65
66 yield* TestClock.adjust("2 seconds");
67 expect(yield* Fiber.join(fiber)).toBeUndefined();
68 yield* TestClock.adjust("10 seconds");
69 expect(reads).toBe(3);
70 }),
71 );
72
73 it.effect(
74 "preserves the query's typed aggregate failure during polling",
75 () =>
76 Effect.gen(function* () {
77 const denied: GraphQLIssue = {
78 _tag: "RailwayForbidden",
79 message: "Not Authorized",
80 path: ["project"],
81 };
82 const failure = new GraphQLFailure({
83 errors: [denied],
84 data: { project: null },
85 status: 200,
86 });
87 let reads = 0;
88 const absent = Effect.suspend(() =>
89 ++reads === 1 ? Effect.succeed(false) : Effect.fail(failure),
90 );
91 const fiber = yield* waitUntilDeleted(
92 "Railway.Project",
93 "project-1",
94 absent,
95 ).pipe(Effect.flip, Effect.forkChild({ startImmediately: true }));
96
97 yield* TestClock.adjust("1 second");
98 const error = yield* Fiber.join(fiber);
99 expect(error).toBe(failure);
100 expect(
101 error._tag === "GraphQLFailure" &&
102 error.errors.every((issue) => issue._tag === "RailwayForbidden"),
103 ).toBe(true);
104 yield* TestClock.adjust("10 seconds");
105 expect(reads).toBe(2);
106 }),
107 );
108 },
109);