1import * as Provider from "@/Provider";
2import { App as PrismaApp, AppProvider } from "@/Prisma/App";
3import { PrismaClient, type PrismaManagementClient } from "@/Prisma/Client";
4import { describe, expect, it } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import { AlchemyContext } from "@/AlchemyContext";
8import {
9 dispatchTo,
10 makeFakeManagementApi,
11 unhandled,
12} from "./fixtures/FakeManagementApi.ts";
13import { testStackContext } from "./fixtures/StackContext.ts";
14
15const app = (id: string, branchId: string | null = "branch-main") => ({
16 id,
17 type: "app" as const,
18 url: `https://api.prisma.test/v1/services/${id}`,
19 name: "api",
20 region: { id: "us-east-1" as const, name: "US East" },
21 projectId: "project-1",
22 branchId,
23 latestDeploymentId: null,
24 appEndpointDomain: `${id}.prisma.build`,
25 createdAt: "2026-01-01T00:00:00Z",
26});
27
28const branch = (id: string, isDefault = true) => ({
29 id,
30 type: "branch" as const,
31 url: `https://api.prisma.test/v1/branches/${id}`,
32 gitName: "main",
33 isDefault,
34 role: "production" as const,
35 createdAt: "2026-01-01T00:00:00Z",
36 updatedAt: "2026-01-01T00:00:00Z",
37 project: {
38 id: "project-1",
39 url: "https://api.prisma.test/v1/projects/project-1",
40 name: "project",
41 },
42});
43
44const liveProviderContext = Layer.succeed(AlchemyContext, {
45 dotAlchemy: ".alchemy-test",
46 dev: false,
47 adopt: false,
48});
49
50/**
51 * Serve the Management API from the same hermetic client-shaped handlers this
52 * suite declares, for the routes App now reaches through distilled
53 * operations. `dispatchTo` maps each handler's result onto the wire (see the
54 * fixture). The delete path still resolves the hand-rolled client (destroyApp
55 * is D3), so the `PrismaClient` layer stays provided alongside.
56 */
57const clientBackedApi = (client: any) =>
58 makeFakeManagementApi((request) => {
59 // segments[0] is the "v1" prefix.
60 const [head, id, tail] = request.pathname
61 .split("/")
62 .filter((segment) => segment.length > 0)
63 .slice(1);
64 const body = request.bodyJson as any;
65 const query = Object.fromEntries(new URLSearchParams(request.search));
66 const { call, callVoid, list } = dispatchTo(request);
67
68 if (head === "services") {
69 if (id === undefined) {
70 return request.method === "GET"
71 ? call(client.listApps, [query], list)
72 : call(client.createApp, [body]);
73 }
74 if (request.method === "GET") return call(client.getApp, [id]);
75 if (request.method === "PATCH") return call(client.updateApp, [id, body]);
76 if (request.method === "DELETE") return callVoid(client.deleteApp, [id]);
77 }
78 if (
79 head === "projects" &&
80 tail === "branches" &&
81 request.method === "GET"
82 ) {
83 return call(client.listBranches, [id, query], list);
84 }
85 return unhandled(request);
86 });
87
88const provide =
89 (client: PrismaManagementClient) =>
90 <A, E, R>(effect: Effect.Effect<A, E, R>) =>
91 effect.pipe(
92 Effect.provide(AppProvider()),
93 Effect.provide(Layer.succeed(PrismaClient, client)),
94 Effect.provide(liveProviderContext),
95 Effect.provide(testStackContext),
96 Effect.provide(clientBackedApi(client).layer),
97 );
98
99describe(
100 "Prisma App",
101 { tags: ["unit", "provider:prisma", "provider:prisma:app", "local"] },
102 () => {
103 it.effect(
104 "replaces immutable drift and schedules only mutable drift",
105 () => {
106 let defaultBranchId = "branch-main";
107 const client = {
108 listBranches: () => Effect.succeed([branch(defaultBranchId, true)]),
109 } as unknown as PrismaManagementClient;
110 const output = {
111 appId: "app-1",
112 name: "api",
113 projectId: "project-1",
114 regionId: "us-east-1",
115 branchId: "branch-main",
116 latestDeploymentId: null,
117 appEndpointDomain: "app-1.prisma.build",
118 createdAt: "2026-01-01T00:00:00Z",
119 };
120 const props = {
121 project: "project-1",
122 displayName: "api",
123 regionId: "us-east-1" as const,
124 };
125
126 return Effect.gen(function* () {
127 const provider = yield* PrismaApp.Provider;
128 const base = {
129 id: "App",
130 fqn: "App",
131 instanceId: "00000000000000000000000000000000",
132 olds: props,
133 news: props,
134 oldBindings: [],
135 newBindings: [],
136 };
137 const clean = yield* provider.diff!({ ...base, output } as never);
138 const nameDrift = yield* provider.diff!({
139 ...base,
140 output: { ...output, name: "drifted" },
141 } as never);
142 defaultBranchId = "branch-next";
143 const defaultBranchDrift = yield* provider.diff!({
144 ...base,
145 output,
146 } as never);
147 const wrongRegion = yield* provider.diff!({
148 ...base,
149 output: { ...output, regionId: "us-west-2" },
150 } as never).pipe(Effect.result);
151 const wrongProject = yield* provider.diff!({
152 ...base,
153 output: { ...output, projectId: "project-other" },
154 } as never);
155
156 expect(clean).toBeUndefined();
157 expect(nameDrift).toEqual({ action: "update" });
158 expect(defaultBranchDrift).toEqual({ action: "update" });
159 expect(wrongRegion._tag).toBe("Failure");
160 if (wrongRegion._tag === "Failure") {
161 expect(String(wrongRegion.failure)).toContain(
162 "cannot atomically move an App",
163 );
164 }
165 expect(wrongProject).toEqual({ action: "replace" });
166 }).pipe(provide(client));
167 },
168 );
169
170 it.effect("enumerates canonical Apps for unsafe nuke", () => {
171 const client = {
172 listApps: () => Effect.succeed([app("app-1"), app("app-2")]),
173 } as unknown as PrismaManagementClient;
174
175 return Effect.gen(function* () {
176 const provider = yield* PrismaApp.Provider;
177 const apps = yield* provider.list!();
178 expect(apps.map((item) => item.appId)).toEqual(["app-1", "app-2"]);
179 }).pipe(provide(client));
180 });
181
182 it.effect("rejects an ambiguous natural ownership match", () => {
183 const client = {
184 listApps: () => Effect.succeed([app("app-1"), app("app-2")]),
185 listBranches: () => Effect.succeed([branch("branch-main")]),
186 } as unknown as PrismaManagementClient;
187
188 return Effect.gen(function* () {
189 const provider = yield* Provider.findProvider(PrismaApp);
190 const error = yield* provider.read!({
191 id: "App",
192 fqn: "App",
193 instanceId: "00000000000000000000000000000000",
194 olds: { project: "project-1", displayName: "api" },
195 output: undefined,
196 }).pipe(Effect.flip);
197 expect((error as Error).message).toContain("ambiguous ownership match");
198 }).pipe(provide(client));
199 });
200
201 it.effect(
202 "inherits the project region and creates with a resolved branch ID",
203 () => {
204 const calls: Array<[string, unknown?]> = [];
205 const regionalApp = (id: string, branchId: string | null) => ({
206 ...app(id, branchId),
207 region: { id: "eu-west-3" as const, name: "Europe West" },
208 });
209 const client = {
210 listBranches: () => Effect.succeed([branch("branch-wanted")]),
211 createApp: (input: unknown) =>
212 Effect.sync(() => {
213 calls.push(["createApp", input]);
214 return regionalApp("app-1", "branch-wrong");
215 }),
216 updateApp: (id: string, input: unknown) =>
217 Effect.sync(() => {
218 calls.push(["updateApp", { id, input }]);
219 return regionalApp(id, "branch-wanted");
220 }),
221 } as unknown as PrismaManagementClient;
222
223 return Effect.gen(function* () {
224 const provider = yield* PrismaApp.Provider;
225 const output = yield* provider.reconcile({
226 id: "App",
227 fqn: "App",
228 instanceId: "00000000000000000000000000000000",
229 news: {
230 project: "project-1",
231 displayName: "api",
232 branchGitName: "main",
233 },
234 olds: undefined,
235 output: undefined,
236 session: undefined as never,
237 bindings: [],
238 });
239
240 expect(output.branchId).toBe("branch-wanted");
241 expect(output.regionId).toBe("eu-west-3");
242 // JSON drops undefined members, so the wire body carries neither
243 // regionId nor branchGitName.
244 expect(calls[0]).toEqual([
245 "createApp",
246 {
247 projectId: "project-1",
248 displayName: "api",
249 branchId: "branch-wanted",
250 },
251 ]);
252 expect(calls.map(([name]) => name)).toEqual([
253 "createApp",
254 "updateApp",
255 ]);
256 }).pipe(provide(client));
257 },
258 );
259
260 it.effect("repairs externally drifted mutable App state", () => {
261 const calls: Array<[string, unknown?]> = [];
262 const client = {
263 getApp: () => Effect.succeed({ ...app("app-1"), name: "drifted" }),
264 listBranches: () => Effect.succeed([branch("branch-main")]),
265 updateApp: (id: string, input: unknown) =>
266 Effect.sync(() => {
267 calls.push(["updateApp", { id, input }]);
268 return app(id);
269 }),
270 } as unknown as PrismaManagementClient;
271
272 return Effect.gen(function* () {
273 const provider = yield* PrismaApp.Provider;
274 const output = yield* provider.reconcile({
275 id: "App",
276 fqn: "App",
277 instanceId: "00000000000000000000000000000000",
278 news: { project: "project-1", displayName: "api" },
279 olds: { project: "project-1", displayName: "api" },
280 output: {
281 appId: "app-1",
282 name: "api",
283 projectId: "project-1",
284 regionId: "us-east-1",
285 branchId: "branch-main",
286 latestDeploymentId: null,
287 appEndpointDomain: "app-1.prisma.build",
288 createdAt: "2026-01-01T00:00:00Z",
289 },
290 session: undefined as never,
291 bindings: [],
292 });
293
294 expect(output.name).toBe("api");
295 expect(calls.map(([name]) => name)).toEqual(["updateApp"]);
296 }).pipe(provide(client));
297 });
298
299 it.effect("deletes by immutable identity despite mutable drift", () => {
300 let deleted = false;
301 const client = {
302 getApp: () =>
303 Effect.succeed({
304 ...app("app-1", "branch-other"),
305 name: "renamed-out-of-band",
306 }),
307 deleteApp: () =>
308 Effect.sync(() => {
309 deleted = true;
310 }),
311 } as unknown as PrismaManagementClient;
312
313 return Effect.gen(function* () {
314 const provider = yield* PrismaApp.Provider;
315 yield* provider.delete({
316 id: "App",
317 fqn: "App",
318 instanceId: "00000000000000000000000000000000",
319 olds: { project: "project-1", displayName: "api" },
320 output: {
321 appId: "app-1",
322 name: "api",
323 projectId: "project-1",
324 regionId: "us-east-1",
325 branchId: "branch-main",
326 latestDeploymentId: null,
327 appEndpointDomain: "app-1.prisma.build",
328 createdAt: "2026-01-01T00:00:00Z",
329 },
330 session: undefined as never,
331 bindings: [],
332 });
333
334 expect(deleted).toBe(true);
335 }).pipe(provide(client));
336 });
337
338 it.effect(
339 "refuses to patch an App with mismatched immutable identity",
340 () => {
341 let observed: Omit<ReturnType<typeof app>, "projectId" | "region"> & {
342 projectId: string;
343 region: { id: string; name: string };
344 } = { ...app("app-1"), projectId: "project-other" };
345 const client = {
346 getApp: () => Effect.succeed(observed),
347 updateApp: () =>
348 Effect.die("must not patch immutable identity drift"),
349 } as unknown as PrismaManagementClient;
350
351 return Effect.gen(function* () {
352 const provider = yield* PrismaApp.Provider;
353 const reconcile = () =>
354 provider.reconcile({
355 id: "App",
356 fqn: "App",
357 instanceId: "00000000000000000000000000000000",
358 news: {
359 project: "project-1",
360 displayName: "api",
361 regionId: "us-east-1",
362 branchId: "branch-main",
363 },
364 olds: undefined,
365 output: {
366 appId: "app-1",
367 name: "api",
368 projectId: "project-1",
369 regionId: "us-east-1",
370 branchId: "branch-main",
371 latestDeploymentId: null,
372 appEndpointDomain: "app-1.prisma.build",
373 createdAt: "2026-01-01T00:00:00Z",
374 },
375 session: undefined as never,
376 bindings: [],
377 });
378
379 const projectError = yield* reconcile().pipe(Effect.flip);
380 expect((projectError as Error).message).toContain("project-other");
381 expect((projectError as Error).message).toContain(
382 "Refusing to patch",
383 );
384
385 observed = {
386 ...app("app-1"),
387 region: { id: "us-west-2", name: "US West" },
388 };
389 const regionError = yield* reconcile().pipe(Effect.flip);
390 expect((regionError as Error).message).toContain("us-west-2");
391 expect((regionError as Error).message).toContain("Refusing to patch");
392 }).pipe(provide(client));
393 },
394 );
395 },
396);