1import { AlchemyContext } from "@/AlchemyContext";
2import * as Bundle from "@/Bundle/Bundle";
3import * as Provider from "@/Provider";
4import { ServiceProvider } from "@/Railway/ServiceProvider";
5import { FunctionProvider } from "@/Railway/Function";
6import * as Layer from "effect/Layer";
7import * as Railway from "@/Railway";
8import { RailwayEnvironment } from "@/Railway/Environment";
9import { Credentials, GqlTransport } from "@distilled.cloud/railway";
10import * as HttpClient from "effect/http/HttpClient";
11import {
12 createRailwayFunctionSupport,
13 createRailwayHostedSupport,
14} from "@/Railway/hosted";
15import { Stack } from "@/Stack";
16import * as Test from "@/Test/Alchemy";
17import { expect } from "alchemy-test";
18import * as Effect from "effect/Effect";
19import * as FileSystem from "effect/FileSystem";
20import * as Path from "effect/Path";
21
22const { test } = Test.make({
23 providers: Layer.mergeAll(ServiceProvider(), FunctionProvider()).pipe(
24 Layer.provide(
25 Layer.mergeAll(
26 Layer.succeed(
27 Credentials,
28 Effect.die("Offline diff must not resolve credentials"),
29 ),
30 Layer.succeed(GqlTransport, {
31 execute: () =>
32 Effect.die("Offline diff must not issue GraphQL requests"),
33 }),
34 Layer.succeed(
35 RailwayEnvironment,
36 Effect.die("Offline diff must not resolve environment"),
37 ),
38 Layer.succeed(
39 HttpClient.HttpClient,
40 HttpClient.make(() =>
41 Effect.die("Offline diff must not make HTTP requests"),
42 ),
43 ),
44 ),
45 ),
46 ),
47});
48
49for (const kind of ["Service", "Function"] as const) {
50 test.provider(
51 `${kind} detects code edits with runtime exports`,
52 () =>
53 Effect.gen(function* () {
54 const fs = yield* FileSystem.FileSystem;
55 const path = yield* Path.Path;
56 const stack = yield* Stack;
57 const { dotAlchemy } = yield* AlchemyContext;
58 yield* fs.makeDirectory(path.resolve(".tmp"), { recursive: true });
59 const directory = yield* fs.makeTempDirectoryScoped({
60 directory: path.resolve(".tmp"),
61 prefix: "railway-code-diff-",
62 });
63 const main = path.join(directory, "main.ts");
64 const source = (value: string) =>
65 `export default { fetch: () => new Response(${JSON.stringify(value)}) };`;
66 yield* fs.writeFileString(main, source("before"));
67 const options = {
68 stackName: stack.name,
69 stage: stack.stage,
70 dotAlchemy,
71 virtualEntryPlugin: yield* Bundle.virtualEntryPlugin,
72 };
73 const hosted =
74 kind === "Service"
75 ? createRailwayHostedSupport(options)
76 : createRailwayFunctionSupport(options);
77 const props = {
78 // Diff receives plain persisted attributes, although its public Props
79 // type still describes the Resource with unresolved Output fields.
80 project: {
81 projectId: "project",
82 name: "project",
83 workspaceId: "workspace",
84 environmentId: "environment",
85 url: "https://railway.com/project/project",
86 } satisfies Railway.Project["Attributes"] as unknown as Railway.Project,
87 environment: { environmentId: "environment" },
88 main,
89 isExternal: true,
90 port: 3000,
91 // Platform supplies runtime exports as Effects during planning.
92 exports: { fetch: Effect.succeed("runtime handler") },
93 };
94 const originalHash = yield* hosted.hash(props);
95 const provider =
96 kind === "Service"
97 ? yield* Provider.findProvider(Railway.Service)
98 : yield* Provider.findProvider(Railway.Function);
99 const diff = (hash: string) =>
100 provider.diff!({
101 id: "Api",
102 fqn: "Api",
103 instanceId: "code-diff",
104 olds: props,
105 news: props,
106 oldBindings: [],
107 newBindings: [],
108 // These are the persisted attributes consulted by both diff methods.
109 output: {
110 projectId: "project",
111 environmentId: "environment",
112 code: { hash },
113 } as Railway.Service["Attributes"] & Railway.Function["Attributes"],
114 });
115 expect(yield* diff(originalHash)).toBeUndefined();
116 yield* fs.writeFileString(main, source("after"));
117 expect(yield* diff(originalHash)).toEqual({ action: "update" });
118 const updatedHash = yield* hosted.hash(props);
119 expect(updatedHash).not.toBe(originalHash);
120 expect(yield* diff(updatedHash)).toBeUndefined();
121 }),
122 {
123 tags: [
124 "unit",
125 "provider:railway",
126 "provider:railway:function",
127 "provider:railway:service",
128 "local",
129 ],
130 timeout: 30_000,
131 },
132 );
133}