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 * as FileSystem from "effect/FileSystem";
9import * as Path from "effect/Path";
10import { MinimumLogLevel } from "effect/References";
11import * as Schedule from "effect/Schedule";
12import * as pathe from "pathe";
13import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
14import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
15
16const { test } = Test.make({ providers: Railway.providers() });
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22
23const fixtureDir = pathe.resolve(
24 import.meta.dirname,
25 "../../AWS/Website/fixtures/octane-app",
26);
27const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
28const fixtureEntries = [
29 ".gitignore",
30 "package.json",
31 "octane.config.ts",
32 "vite.config.ts",
33 "index.html",
34 "src",
35 "public",
36];
37
38const readService = Query.fn((id: string) => ({
39 deletedAt: RailwayApi.service({ id }).deletedAt,
40}));
41
42const waitUntilGone = (serviceId: string) =>
43 readService(serviceId).pipe(
44 Effect.map((service) =>
45 service.deletedAt != null ? ("gone" as const) : ("found" as const),
46 ),
47 Effect.catchTag("RailwayNotFound", () => Effect.succeed("gone" as const)),
48 Effect.repeat({
49 schedule: Schedule.spaced("1 second"),
50 until: (status) => status === "gone",
51 times: 10,
52 }),
53 );
54
55test.provider(
56 "Octane: deploy, GET /, destroy, gone",
57 (stack) =>
58 Effect.gen(function* () {
59 yield* stack.destroy();
60
61 const rootDir = yield* cloneFixture(fixtureDir, {
62 prefix: "alchemy-octane-railway-",
63 tempRoot,
64 entries: fixtureEntries,
65 });
66
67 const fs = yield* FileSystem.FileSystem;
68 const pathMod = yield* Path.Path;
69 const configPath = pathMod.join(rootDir, "octane.config.ts");
70 const raw = yield* fs.readFileString(configPath);
71 expect(raw).not.toContain("adapter:");
72 expect(raw).not.toContain("@alchemy.run/frontend-frameworks");
73
74 const deployed = yield* stack.deploy(
75 Effect.gen(function* () {
76 const { project, environment } = yield* suitePartition;
77 const site = yield* Railway.Website.Octane("Web", {
78 project,
79 environment,
80 rootDir,
81 memo: {
82 include: [
83 "src/**",
84 "public/**",
85 "package.json",
86 "octane.config.ts",
87 "vite.config.ts",
88 "index.html",
89 ],
90 },
91 });
92 return { site };
93 }),
94 );
95
96 expect(yield* fs.readFileString(configPath)).toBe(raw);
97 const url = deployed.site.url;
98 expect(url).toBeDefined();
99 expect(url).toMatch(/^https:\/\//);
100 expect(deployed.site.service).toBeDefined();
101 expect(deployed.site.project).toBeDefined();
102
103 yield* expectUrlContains(`${url!}/`, "OCTANE_AWS_PAGE_MARKER", {
104 timeout: "90 seconds",
105 label: "home page",
106 });
107 yield* expectUrlContains(
108 `${url!}/api/hello?echo=roundtrip`,
109 "OCTANE_AWS_API_MARKER",
110 {
111 timeout: "30 seconds",
112 label: "api route",
113 },
114 );
115
116 const serviceId = deployed.site.service!.serviceId;
117 yield* stack.destroy();
118 const gone = yield* waitUntilGone(serviceId);
119 expect(gone).toEqual("gone");
120 }).pipe(logLevel),
121 {
122 tags: [
123 "provider:railway",
124 "provider:railway:project",
125 "provider:railway:projectenvironment",
126 "provider:railway:service",
127 "provider:railway:website",
128 "live",
129 ],
130 timeout: 120_000,
131 },
132);