1import { Query } from "@distilled.cloud/core/query";
2import { Railway as RailwayApi } from "@distilled.cloud/railway";
3import * as Railway from "@/Railway";
4import { suitePartition } from "../suiteProject.ts";
5import * as Test from "@/Test/Alchemy";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import * as HttpClient from "effect/http/HttpClient";
11import * as pathe from "pathe";
12import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
13import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
14
15const { test } = Test.make({ providers: Railway.providers() });
16
17const logLevel = Effect.provideService(
18 MinimumLogLevel,
19 process.env.DEBUG ? "Debug" : "Info",
20);
21
22const fixtureDir = pathe.resolve(
23 import.meta.dirname,
24 "../../Cloudflare/Website/vite-spa-fixture",
25);
26const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
27const fixtureEntries = ["index.html", "package.json", "src"];
28
29const readService = Query.fn((id: string) => ({
30 deletedAt: RailwayApi.service({ id }).deletedAt,
31}));
32
33const waitUntilGone = (serviceId: string) =>
34 readService(serviceId).pipe(
35 Effect.map((service) =>
36 service.deletedAt != null ? ("gone" as const) : ("found" as const),
37 ),
38 Effect.catchTag("RailwayNotFound", () => Effect.succeed("gone" as const)),
39 Effect.repeat({
40 schedule: Schedule.spaced("1 second"),
41 until: (status) => status === "gone",
42 times: 10,
43 }),
44 );
45
46test.provider(
47 "Vite SPA: deploy, GET /, destroy, gone",
48 (stack) =>
49 Effect.gen(function* () {
50 yield* stack.destroy();
51
52 const rootDir = yield* cloneFixture(fixtureDir, {
53 prefix: "alchemy-vite-railway-",
54 tempRoot,
55 entries: fixtureEntries,
56 });
57
58 const deployed = yield* stack.deploy(
59 Effect.gen(function* () {
60 const { project, environment } = yield* suitePartition;
61 const site = yield* Railway.Website.Vite("Web", {
62 project,
63 environment,
64 rootDir,
65 memo: {
66 include: ["index.html", "src/**", "package.json"],
67 },
68 });
69 return { site };
70 }),
71 );
72
73 const url = deployed.site.url;
74 expect(url).toBeDefined();
75 expect(url).toMatch(/^https:\/\//);
76 expect(deployed.site.service).toBeDefined();
77 expect(deployed.site.service!.serviceId.length).toBeGreaterThan(0);
78 expect(deployed.site.project).toBeDefined();
79
80 yield* expectUrlContains(`${url!}/`, "Vite SPA fixture", {
81 timeout: "90 seconds",
82 label: "vite spa index",
83 });
84 yield* expectUrlContains(
85 `${url!}/missing-client-route`,
86 "Vite SPA fixture",
87 {
88 timeout: "15 seconds",
89 label: "vite spa fallback",
90 },
91 );
92
93 const client = yield* HttpClient.HttpClient;
94 const health = yield* client.get(`${url!}/health`).pipe(
95 Effect.flatMap((res) =>
96 res.status === 200
97 ? res.text
98 : Effect.fail(new Error(`health returned ${res.status}`)),
99 ),
100 Effect.retry({
101 schedule: Schedule.exponential("500 millis"),
102 times: 6,
103 }),
104 );
105 expect(health).toContain("ok");
106
107 const serviceId = deployed.site.service!.serviceId;
108 yield* stack.destroy();
109 const gone = yield* waitUntilGone(serviceId);
110 expect(gone).toEqual("gone");
111 }).pipe(logLevel),
112 {
113 tags: [
114 "provider:railway",
115 "provider:railway:project",
116 "provider:railway:projectenvironment",
117 "provider:railway:service",
118 "provider:railway:website",
119 "live",
120 ],
121 timeout: 120_000,
122 },
123);