1import * as Provider from "@/Provider";
2import {
3 Compute,
4 ComputeDevProvider,
5 ComputeProvider,
6 syncComputeEnvironment,
7 waitForDeploymentUrl,
8 type ComputeProps,
9} from "@/Prisma/Compute";
10import { Unowned } from "@/AdoptPolicy";
11import { AlchemyContext } from "@/AlchemyContext";
12import {
13 PrismaApiError,
14 PrismaClient,
15 type PrismaManagementClient,
16} from "@/Prisma/Client";
17import * as Output from "@/Output";
18import type { ResourceBinding } from "@/Resource";
19import { Stack } from "@/Stack";
20import { PlatformServices } from "@/Util/PlatformServices";
21import type { Branch as ApiBranch } from "@/Prisma/Types";
22import { describe, expect, it } from "alchemy-test";
23import * as Effect from "effect/Effect";
24import * as FileSystem from "effect/FileSystem";
25import * as Layer from "effect/Layer";
26import * as Option from "effect/Option";
27import * as Path from "effect/Path";
28import * as Redacted from "effect/Redacted";
29import * as Stream from "effect/Stream";
30import * as FetchHttpClient from "effect/http/FetchHttpClient";
31import * as HttpBody from "effect/http/HttpBody";
32import * as HttpClient from "effect/http/HttpClient";
33import * as HttpClientResponse from "effect/http/HttpClientResponse";
34import { gunzipSync } from "node:zlib";
35import { WebSocketServer } from "ws";
36import { fromApiToken } from "@distilled.cloud/prisma";
37import { Credentials } from "@/Prisma/Credentials";
38import {
39 type Captured,
40 data,
41 dispatchTo,
42 failure,
43 FAKE_API_BASE_URL,
44 makeFakeManagementApi,
45 unhandled,
46} from "./fixtures/FakeManagementApi.ts";
47import { testStackContext } from "./fixtures/StackContext.ts";
48
49const testBranch = (
50 id: string,
51 role: "production" | "preview" = "production",
52): ApiBranch => ({
53 id,
54 type: "branch",
55 url: `https://api.prisma.test/v1/branches/${id}`,
56 gitName: id === "branch-main" ? "main" : id,
57 isDefault: role === "production" && id === "branch-main",
58 role,
59 createdAt: "2026-01-01T00:00:00Z",
60 updatedAt: "2026-01-01T00:00:00Z",
61 project: {
62 id: "project-1",
63 url: "https://api.prisma.test/v1/projects/project-1",
64 name: "project",
65 },
66});
67
68const withDefaultBranch = <T extends object>(
69 client: T,
70 role: "production" | "preview" = "production",
71): PrismaManagementClient => {
72 const mock = client as Record<string, (...args: any[]) => any>;
73 return {
74 ...client,
75 getBranch: (id: string) =>
76 mock.getBranch?.(id) ?? Effect.succeed(testBranch(id, role)),
77 listBranches: (projectId: string, query?: { gitName?: string }) =>
78 mock.listBranches?.(projectId, query) ??
79 Effect.succeed([testBranch("branch-main", role)]),
80 listApps: (query: { projectId?: string; limit?: number }) =>
81 mock.listApps?.length >= 2
82 ? mock.listApps(query.projectId, { limit: query.limit })
83 : mock.listApps?.(query),
84 createApp: (input: { projectId: string } & Record<string, unknown>) => {
85 if (mock.createApp?.length < 2) return mock.createApp(input);
86 const { projectId, ...body } = input;
87 return mock.createApp(projectId, body);
88 },
89 createAppDeployment: (appId: string, input: unknown) =>
90 mock.createAppDeployment?.(appId, input),
91 getDeployment: (id: string) => mock.getDeployment?.(id),
92 startDeployment: (id: string) => mock.startDeployment?.(id),
93 stopDeployment: (id: string) => mock.stopDeployment?.(id),
94 deleteDeployment: (id: string) => mock.deleteDeployment?.(id),
95 listAppDeployments: (appId: string, query: unknown) =>
96 mock.listAppDeployments?.(appId, query),
97 promoteApp: (appId: string, target: { deploymentId: string }) =>
98 mock.promoteApp(appId, target),
99 rollbackApp: (appId: string, target: { deploymentId: string }) =>
100 mock.rollbackApp?.(appId, target),
101 } as unknown as PrismaManagementClient;
102};
103
104const fixtureArtifactPath = `${import.meta.dirname}/fixtures/artifact-archive.bin`;
105const fixtureArtifactV1Path = `${import.meta.dirname}/fixtures/artifact-v1.bin`;
106const fixtureArtifactV2Path = `${import.meta.dirname}/fixtures/artifact-v2.bin`;
107
108const readHttpBodyBytes = Effect.fn(function* (body: HttpBody.HttpBody) {
109 if (body._tag === "Uint8Array") return body.body;
110 if (body._tag !== "Stream") return new Uint8Array();
111 const chunks = yield* Stream.runCollect(body.stream).pipe(Effect.orDie);
112 const output = new Uint8Array(
113 chunks.reduce((total, chunk) => total + chunk.byteLength, 0),
114 );
115 let offset = 0;
116 for (const chunk of chunks) {
117 output.set(chunk, offset);
118 offset += chunk.byteLength;
119 }
120 return output;
121});
122
123const readTarString = (buffer: Uint8Array, start: number, length: number) => {
124 const bytes = buffer.slice(start, start + length);
125 const end = bytes.indexOf(0);
126 return new TextDecoder().decode(end >= 0 ? bytes.slice(0, end) : bytes);
127};
128
129const readTarFile = (buffer: Uint8Array, expectedName: string) => {
130 let offset = 0;
131 while (offset + 512 <= buffer.length) {
132 const header = buffer.slice(offset, offset + 512);
133 if (header.every((byte) => byte === 0)) break;
134 const name = readTarString(header, 0, 100);
135 const prefix = readTarString(header, 345, 155);
136 const fullName = prefix ? `${prefix}/${name}` : name;
137 const size = Number.parseInt(
138 readTarString(header, 124, 12).trim() || "0",
139 8,
140 );
141 const bodyStart = offset + 512;
142 if (fullName === expectedName) {
143 return new TextDecoder().decode(
144 buffer.slice(bodyStart, bodyStart + size),
145 );
146 }
147 offset = bodyStart + size + ((512 - (size % 512)) % 512);
148 }
149 throw new Error(`Missing tar entry '${expectedName}'.`);
150};
151
152const httpBodyContentType = (body: HttpBody.HttpBody) =>
153 body._tag === "Uint8Array" || body._tag === "Stream"
154 ? body.contentType
155 : undefined;
156
157const makeHealthLifecycleFixture = (options?: {
158 latestDeploymentId?: string | null;
159 previewStatus?: number;
160 stableStatus?: number;
161 rollbackFailures?: number;
162 getAppFailureCalls?: readonly number[];
163 promoteUpdatesLatest?: boolean;
164 rollbackUpdatesLatest?: boolean;
165}) => {
166 const calls: Array<[string, unknown?]> = [];
167 const deployments = new Map<
168 string,
169 "new" | "running" | "provisioning" | "stopped" | "failed"
170 >([["version-old", "running"]]);
171 let deploymentCounter = 0;
172 let previewStatus = options?.previewStatus ?? 204;
173 let stableStatus = options?.stableStatus ?? 204;
174 let rollbackFailuresRemaining = options?.rollbackFailures ?? 0;
175 let getAppCalls = 0;
176 let latestDeploymentId =
177 options?.latestDeploymentId === undefined
178 ? "version-old"
179 : options.latestDeploymentId;
180
181 const app = () => ({
182 id: "service-1",
183 type: "app" as const,
184 url: "https://api.prisma.test/v1/services/service-1",
185 name: "api",
186 region: { id: "us-east-1", name: "US East" },
187 projectId: "project-1",
188 branchId: "branch-main",
189 latestDeploymentId,
190 appEndpointDomain: "api.prisma.build",
191 createdAt: "2026-01-01T00:00:00Z",
192 });
193 const client = {
194 getApp: (id: string) => {
195 getAppCalls += 1;
196 calls.push(["getApp", id]);
197 if (options?.getAppFailureCalls?.includes(getAppCalls)) {
198 return Effect.fail(
199 new PrismaApiError({
200 method: "GET",
201 path: `/v1/services/${id}`,
202 // Non-transient on purpose: a 5xx would be replayed by the
203 // retry policy.
204 status: 400,
205 message: "app observation unavailable",
206 }),
207 );
208 }
209 return Effect.succeed(app());
210 },
211 createAppDeployment: (appId: string, input: unknown) => {
212 calls.push(["createAppDeployment", { appId, input }]);
213 deploymentCounter += 1;
214 const deploymentId =
215 deploymentCounter === 1
216 ? "version-new"
217 : `version-new-${deploymentCounter}`;
218 deployments.set(deploymentId, "new");
219 return Effect.succeed({
220 id: deploymentId,
221 type: "deployment" as const,
222 url: `https://api.prisma.test/v1/deployments/${deploymentId}`,
223 foundryVersionId: `foundry-${deploymentId}`,
224 uploadUrl: `https://upload.prisma.test/${deploymentId}.tar.gz`,
225 });
226 },
227 getDeployment: (id: string) => {
228 calls.push(["getDeployment", id]);
229 const status = deployments.get(id);
230 return status
231 ? Effect.succeed({
232 id,
233 type: "deployment" as const,
234 serviceId: "service-1",
235 url: `https://api.prisma.test/v1/deployments/${id}`,
236 foundryVersionId: `foundry-${id}`,
237 status,
238 previewDomain: `${id}.preview.prisma.build`,
239 createdAt: "2026-01-01T00:00:00Z",
240 })
241 : Effect.fail(
242 new PrismaApiError({
243 method: "GET",
244 path: `/v1/deployments/${id}`,
245 status: 404,
246 message: "not found",
247 }),
248 );
249 },
250 startDeployment: (id: string) =>
251 Effect.sync(() => {
252 calls.push(["startDeployment", id]);
253 deployments.set(id, "running");
254 return { previewDomain: `${id}.preview.prisma.build` };
255 }),
256 promoteApp: (appId: string, target: { deploymentId: string }) =>
257 Effect.sync(() => {
258 calls.push(["promoteApp", { appId, ...target }]);
259 if (options?.promoteUpdatesLatest !== false) {
260 latestDeploymentId = target.deploymentId;
261 }
262 return { appEndpointDomain: "api.prisma.build", reassignedDomains: 0 };
263 }),
264 rollbackApp: (appId: string, target: { deploymentId: string }) =>
265 Effect.gen(function* () {
266 calls.push(["rollbackApp", { appId, ...target }]);
267 if (rollbackFailuresRemaining > 0) {
268 rollbackFailuresRemaining -= 1;
269 return yield* Effect.fail(
270 new PrismaApiError({
271 method: "POST",
272 path: `/v1/services/${appId}/rollback`,
273 status: 400,
274 message: "rollback unavailable",
275 }),
276 );
277 }
278 if (options?.rollbackUpdatesLatest !== false) {
279 latestDeploymentId = target.deploymentId;
280 }
281 return { appEndpointDomain: "api.prisma.build", reassignedDomains: 0 };
282 }),
283 stopDeployment: (id: string) =>
284 Effect.sync(() => {
285 calls.push(["stopDeployment", id]);
286 deployments.set(id, "stopped");
287 }),
288 deleteDeployment: (id: string) =>
289 Effect.sync(() => {
290 calls.push(["deleteDeployment", id]);
291 deployments.delete(id);
292 }),
293 } as unknown as PrismaManagementClient;
294 const http = HttpClient.make((request) => {
295 if (request.url.startsWith("https://upload.prisma.test/")) {
296 calls.push(["upload", request.url]);
297 return readHttpBodyBytes(request.body as HttpBody.HttpBody).pipe(
298 Effect.as(HttpClientResponse.fromWeb(request, new Response(null))),
299 );
300 }
301 calls.push(["healthRequest", request.url]);
302 return Effect.succeed(
303 HttpClientResponse.fromWeb(
304 request,
305 new Response(null, {
306 status: new URL(request.url).hostname.endsWith(
307 ".preview.prisma.build",
308 )
309 ? previewStatus
310 : stableStatus,
311 }),
312 ),
313 );
314 });
315
316 return {
317 calls,
318 client,
319 http,
320 hasDeployment: (id: string) => deployments.has(id),
321 latestDeploymentId: () => latestDeploymentId,
322 setPreviewStatus: (status: number) => {
323 previewStatus = status;
324 },
325 setStableStatus: (status: number) => {
326 stableStatus = status;
327 },
328 setDeploymentStatus: (
329 id: string,
330 status: "new" | "running" | "provisioning" | "stopped" | "failed",
331 ) => {
332 deployments.set(id, status);
333 },
334 };
335};
336
337const liveProviderContext = Layer.succeed(AlchemyContext, {
338 dotAlchemy: ".alchemy-test",
339 dev: false,
340 adopt: false,
341});
342
343const computeProviderLive = () =>
344 ComputeProvider().pipe(
345 Layer.provide(
346 Layer.mergeAll(liveProviderContext, testStackContext, PlatformServices),
347 ),
348 );
349
350/**
351 * Serve the Management API from the same hermetic client-shaped handlers this
352 * suite declares, for the routes Compute now reaches through distilled
353 * operations. `dispatchTo` maps each handler's result onto the wire (see the
354 * fixture). The log tail still resolves the hand-rolled client, so the
355 * `PrismaClient` layer stays provided alongside.
356 */
357const dispatchManagement = (client: any, request: Captured): Response => {
358 const [head, id, tail] = request.pathname
359 .split("/")
360 .filter((segment) => segment.length > 0)
361 .slice(1);
362 const body = request.bodyJson as any;
363 const query = Object.fromEntries(new URLSearchParams(request.search));
364 const { call, callVoid, list } = dispatchTo(request);
365
366 if (head === "services") {
367 if (id === undefined) {
368 return request.method === "GET"
369 ? call(client.listApps, [query], list)
370 : call(client.createApp, [body]);
371 }
372 if (tail === "deployments") {
373 return request.method === "GET"
374 ? call(client.listAppDeployments, [id, query], list)
375 : call(client.createAppDeployment, [id, body]);
376 }
377 if (tail === "promote") return call(client.promoteApp, [id, body]);
378 if (tail === "rollback") return call(client.rollbackApp, [id, body]);
379 if (tail === undefined) {
380 if (request.method === "GET") return call(client.getApp, [id]);
381 if (request.method === "PATCH") return call(client.updateApp, [id, body]);
382 if (request.method === "DELETE") {
383 return callVoid(client.deleteApp, [id]);
384 }
385 }
386 }
387 if (head === "branches" && id !== undefined && request.method === "GET") {
388 return call(client.getBranch, [id]);
389 }
390 if (head === "projects" && id !== undefined) {
391 if (tail === "branches" && request.method === "GET") {
392 return call(client.listBranches, [id, query], list);
393 }
394 if (tail === undefined && request.method === "DELETE") {
395 return callVoid(client.deleteProject, [id]);
396 }
397 }
398 if (head === "deployments" && id !== undefined) {
399 if (tail === "start") return call(client.startDeployment, [id]);
400 if (tail === "stop") {
401 return callVoid(client.stopDeployment, [id]);
402 }
403 if (request.method === "GET") return call(client.getDeployment, [id]);
404 if (request.method === "DELETE") {
405 return callVoid(client.deleteDeployment, [id]);
406 }
407 }
408 if (head === "environment-variables") {
409 if (id === undefined) {
410 return request.method === "GET"
411 ? call(client.listEnvironmentVariables, [query], list)
412 : call(client.createEnvironmentVariable, [body]);
413 }
414 if (request.method === "GET") {
415 return call(client.getEnvironmentVariable, [id]);
416 }
417 if (request.method === "PATCH") {
418 return call(client.updateEnvironmentVariable, [id, body]);
419 }
420 if (request.method === "DELETE") {
421 return callVoid(client.deleteEnvironmentVariable, [id]);
422 }
423 }
424 return unhandled(request);
425};
426
427/**
428 * Ambient HTTP for a Compute test: Management API requests dispatch to the
429 * client-shaped handlers; anything else (uploads, health probes) falls back
430 * to the HttpClient provided beneath this layer, when there is one.
431 */
432const apiRoutedHttp = (client: any) =>
433 Layer.mergeAll(
434 Layer.effect(
435 HttpClient.HttpClient,
436 Effect.gen(function* () {
437 const fallback = yield* Effect.serviceOption(HttpClient.HttpClient);
438 return HttpClient.make((request) => {
439 if (!request.url.startsWith(FAKE_API_BASE_URL)) {
440 return Option.isSome(fallback)
441 ? fallback.value.execute(request)
442 : Effect.sync(() =>
443 HttpClientResponse.fromWeb(
444 request,
445 unhandled({
446 url: request.url,
447 method: request.method,
448 pathname: new URL(request.url).pathname,
449 search: "",
450 authorization: undefined,
451 bodyJson: undefined,
452 }),
453 ),
454 );
455 }
456 return Effect.sync(() => {
457 const url = new URL(request.url);
458 const body = request.body as HttpBody.HttpBody;
459 const bodyText =
460 body._tag === "Uint8Array"
461 ? new TextDecoder().decode(body.body)
462 : "";
463 return HttpClientResponse.fromWeb(
464 request,
465 dispatchManagement(client, {
466 url: request.url,
467 method: request.method,
468 pathname: url.pathname,
469 search: url.search,
470 authorization: request.headers.authorization,
471 bodyJson: bodyText ? JSON.parse(bodyText) : undefined,
472 }),
473 );
474 });
475 });
476 }),
477 ),
478 fromApiToken({
479 apiToken: "fake-service-token",
480 apiBaseUrl: FAKE_API_BASE_URL,
481 }),
482 );
483
484describe(
485 "Prisma Compute",
486 { tags: ["unit", "provider:prisma", "provider:prisma:compute", "local"] },
487 () => {
488 it.live(
489 "accepts a streaming 200 response without consuming its body",
490 () => {
491 const body = new ReadableStream<Uint8Array>({
492 pull() {
493 // Deliberately never enqueue or close. Reading this body would hang.
494 },
495 });
496 const http = HttpClient.make((request) =>
497 Effect.succeed(
498 HttpClientResponse.fromWeb(
499 request,
500 new Response(body, { status: 200 }),
501 ),
502 ),
503 );
504
505 return waitForDeploymentUrl("https://app.prisma.build", {
506 project: "project-1",
507 appName: "api",
508 urlReadinessTimeoutSeconds: 0.05,
509 }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, http)));
510 },
511 );
512
513 it.live("waits for the configured application health contract", () => {
514 const requests: string[] = [];
515 const http = HttpClient.make((request) => {
516 requests.push(request.url);
517 return Effect.succeed(
518 HttpClientResponse.fromWeb(
519 request,
520 new Response(null, {
521 status: requests.length === 1 ? 503 : 204,
522 }),
523 ),
524 );
525 });
526
527 return Effect.gen(function* () {
528 yield* waitForDeploymentUrl("https://app.prisma.build", {
529 project: "project-1",
530 appName: "api",
531 healthCheck: { path: "/api/health" },
532 pollIntervalMs: 1,
533 // Wall-clock deadline (live clock). The mock succeeds on the second
534 // poll, so the pass path never waits this long — it only needs to
535 // not fire spuriously when a saturated event loop (full-suite run)
536 // delays the second poll beyond a tight budget.
537 urlReadinessTimeoutSeconds: 30,
538 });
539
540 expect(requests).toEqual([
541 "https://app.prisma.build/api/health",
542 "https://app.prisma.build/api/health",
543 ]);
544 }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, http)));
545 });
546
547 it.effect("rejects unsafe application health contracts", () =>
548 Effect.gen(function* () {
549 const pathError = yield* waitForDeploymentUrl(
550 "https://app.prisma.build",
551 {
552 project: "project-1",
553 appName: "api",
554 healthCheck: { path: "//attacker.example/health" },
555 },
556 ).pipe(Effect.flip);
557 const statusError = yield* waitForDeploymentUrl(
558 "https://app.prisma.build",
559 {
560 project: "project-1",
561 appName: "api",
562 healthCheck: { path: "/health", statusCodes: [] },
563 },
564 ).pipe(Effect.flip);
565
566 expect(pathError.message).toContain("healthCheck.path");
567 expect(statusError.message).toContain("healthCheck.statusCodes");
568 }),
569 );
570
571 it.effect("fails closed when an application health probe cannot run", () =>
572 Effect.gen(function* () {
573 const props = {
574 project: "project-1",
575 appName: "api",
576 healthCheck: { path: "/health" },
577 } as const;
578 const missingUrl = yield* waitForDeploymentUrl(undefined, props).pipe(
579 Effect.flip,
580 );
581 const missingRoutingUrl = yield* waitForDeploymentUrl(undefined, {
582 project: "project-1",
583 appName: "api",
584 }).pipe(Effect.flip);
585 const disabled = yield* waitForDeploymentUrl(
586 "https://app.prisma.build",
587 {
588 ...props,
589 verifyUrl: false,
590 },
591 ).pipe(Effect.flip);
592 const missingClient = yield* waitForDeploymentUrl(
593 "https://app.prisma.build",
594 props,
595 ).pipe(Effect.flip);
596
597 expect(missingUrl.message).toContain("did not return");
598 expect(missingRoutingUrl.message).toContain("readiness verification");
599 expect(disabled.message).toContain("verifyUrl: false");
600 expect(missingClient.message).toContain("HTTP client");
601 }),
602 );
603
604 it.live("observes health redirects without following them", () => {
605 const redirects: RequestRedirect[] = [];
606 const fetch = (async (
607 _input: Parameters<typeof globalThis.fetch>[0],
608 init?: Parameters<typeof globalThis.fetch>[1],
609 ) => {
610 redirects.push(init?.redirect ?? "follow");
611 return init?.redirect === "manual"
612 ? new Response(null, {
613 status: 302,
614 headers: { location: "https://attacker.example/" },
615 })
616 : new Response(null, { status: 200 });
617 }) as typeof globalThis.fetch;
618
619 return Effect.gen(function* () {
620 yield* waitForDeploymentUrl("https://app.prisma.build", {
621 project: "project-1",
622 appName: "api",
623 healthCheck: { path: "/health", statusCodes: [302] },
624 pollIntervalMs: 1,
625 urlReadinessTimeoutSeconds: 0.05,
626 });
627 const defaultStatusError = yield* waitForDeploymentUrl(
628 "https://app.prisma.build",
629 {
630 project: "project-1",
631 appName: "api",
632 healthCheck: { path: "/health" },
633 pollIntervalMs: 1,
634 urlReadinessTimeoutSeconds: 0.01,
635 },
636 ).pipe(Effect.flip);
637
638 expect(redirects.length).toBeGreaterThanOrEqual(2);
639 expect(redirects.every((redirect) => redirect === "manual")).toBe(true);
640 expect(defaultStatusError.message).toContain("HTTP 302");
641 }).pipe(
642 Effect.provide(FetchHttpClient.layer),
643 Effect.provideService(FetchHttpClient.Fetch, fetch),
644 );
645 });
646
647 it.live("enforces one deadline for stalled requests and 404 bodies", () => {
648 const stalledRequest = HttpClient.make(() => Effect.never);
649 const stalledBody = HttpClient.make((request) =>
650 Effect.succeed(
651 HttpClientResponse.fromWeb(
652 request,
653 new Response(
654 new ReadableStream<Uint8Array>({
655 pull() {
656 // Deliberately never enqueue or close.
657 },
658 }),
659 { status: 404 },
660 ),
661 ),
662 ),
663 );
664 const props = {
665 project: "project-1",
666 appName: "api",
667 pollIntervalMs: 1,
668 urlReadinessTimeoutSeconds: 0.03,
669 } as const;
670
671 return Effect.gen(function* () {
672 const requestError = yield* waitForDeploymentUrl(
673 "https://request.prisma.build",
674 props,
675 ).pipe(
676 Effect.provide(Layer.succeed(HttpClient.HttpClient, stalledRequest)),
677 Effect.flip,
678 );
679 const bodyError = yield* waitForDeploymentUrl(
680 "https://body.prisma.build",
681 props,
682 ).pipe(
683 Effect.provide(Layer.succeed(HttpClient.HttpClient, stalledBody)),
684 Effect.flip,
685 );
686
687 expect(requestError.message).toContain("Timed out");
688 expect(bodyError.message).toContain("Timed out");
689 });
690 });
691
692 it.live("bounds the inspected prefix of a large Prisma edge 404", () => {
693 let requests = 0;
694 const hugeBody = `${"There is no service on this URL"}${"x".repeat(
695 256 * 1024,
696 )}`;
697 const http = HttpClient.make((request) => {
698 requests += 1;
699 return requests === 1
700 ? Effect.succeed(
701 HttpClientResponse.fromWeb(
702 request,
703 new Response(hugeBody, { status: 404 }),
704 ),
705 )
706 : Effect.never;
707 });
708
709 return Effect.gen(function* () {
710 const error = yield* waitForDeploymentUrl(
711 "https://missing.prisma.build",
712 {
713 project: "project-1",
714 appName: "api",
715 pollIntervalMs: 1,
716 urlReadinessTimeoutSeconds: 0.05,
717 },
718 ).pipe(
719 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
720 Effect.flip,
721 );
722
723 expect(error.message).toContain("There is no service on this URL");
724 expect(requests).toBeGreaterThanOrEqual(1);
725 });
726 });
727
728 it.effect(
729 "rejects invalid URL readiness timings before making a request",
730 () => {
731 const http = HttpClient.make(() =>
732 Effect.die("invalid readiness options must fail first"),
733 );
734
735 return Effect.gen(function* () {
736 const error = yield* waitForDeploymentUrl(
737 "https://app.prisma.build",
738 {
739 project: "project-1",
740 appName: "api",
741 pollIntervalMs: 0,
742 },
743 ).pipe(
744 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
745 Effect.flip,
746 );
747
748 expect(error.message).toContain("pollIntervalMs");
749 });
750 },
751 );
752 it.effect(
753 "rejects destroyOldDeployment when promotion is skipped",
754 () => {
755 const client = {} as PrismaManagementClient;
756
757 return Effect.gen(function* () {
758 const provider = yield* Compute.Provider;
759 const error = yield* provider
760 .reconcile({
761 id: "App",
762 fqn: "App",
763 instanceId: "00000000000000000000000000000000",
764 news: {
765 project: "project-1",
766 appName: "api",
767 skipPromote: true,
768 destroyOldDeployment: true,
769 },
770 olds: undefined,
771 output: undefined,
772 session: undefined as never,
773 bindings: [],
774 })
775 .pipe(Effect.flip);
776
777 expect(error).toBeInstanceOf(Error);
778 expect(error.message).toContain(
779 "destroyOldDeployment cannot be combined with skipPromote",
780 );
781 }).pipe(
782 Effect.provide(computeProviderLive()),
783 Effect.provide(
784 Layer.succeed(PrismaClient, withDefaultBranch(client)),
785 ),
786 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
787 );
788 },
789 { tags: ["provider:prisma:branch"] },
790 );
791
792 it.effect(
793 "rejects a health check when deployment start is disabled",
794 () => {
795 const client = {} as PrismaManagementClient;
796
797 return Effect.gen(function* () {
798 const provider = yield* Compute.Provider;
799 const error = yield* provider
800 .reconcile({
801 id: "App",
802 fqn: "App",
803 instanceId: "00000000000000000000000000000000",
804 news: {
805 project: "project-1",
806 appName: "api",
807 start: false,
808 skipPromote: true,
809 healthCheck: { path: "/health" },
810 },
811 olds: undefined,
812 output: undefined,
813 session: undefined as never,
814 bindings: [],
815 })
816 .pipe(Effect.flip);
817
818 expect(error.message).toContain("healthCheck requires start");
819 }).pipe(
820 Effect.provide(computeProviderLive()),
821 Effect.provide(Layer.succeed(PrismaClient, client)),
822 Effect.provide(apiRoutedHttp(client)),
823 Effect.provide(PlatformServices),
824 );
825 },
826 );
827
828 it.effect(
829 "rejects disabled start when promotion is enabled",
830 () => {
831 const client = {} as PrismaManagementClient;
832
833 return Effect.gen(function* () {
834 const provider = yield* Compute.Provider;
835 const error = yield* provider
836 .reconcile({
837 id: "App",
838 fqn: "App",
839 instanceId: "00000000000000000000000000000000",
840 news: {
841 project: "project-1",
842 appName: "api",
843 start: false,
844 },
845 olds: undefined,
846 output: undefined,
847 session: undefined as never,
848 bindings: [],
849 })
850 .pipe(Effect.flip);
851
852 expect(error).toBeInstanceOf(Error);
853 expect(error.message).toContain(
854 "start: false requires skipPromote: true",
855 );
856 }).pipe(
857 Effect.provide(computeProviderLive()),
858 Effect.provide(
859 Layer.succeed(PrismaClient, withDefaultBranch(client)),
860 ),
861 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
862 );
863 },
864 { tags: ["provider:prisma:branch"] },
865 );
866
867 it.effect(
868 "rejects conflicting branch attachment inputs",
869 () => {
870 const client = {} as PrismaManagementClient;
871
872 return Effect.gen(function* () {
873 const provider = yield* Compute.Provider;
874 const error = yield* provider
875 .reconcile({
876 id: "App",
877 fqn: "App",
878 instanceId: "00000000000000000000000000000000",
879 news: {
880 project: "project-1",
881 appName: "api",
882 branchId: "branch-1",
883 branchGitName: "main",
884 },
885 olds: undefined,
886 output: undefined,
887 session: undefined as never,
888 bindings: [],
889 })
890 .pipe(Effect.flip);
891
892 expect(error).toBeInstanceOf(Error);
893 expect(error.message).toContain(
894 "branchId and branchGitName are mutually exclusive",
895 );
896 }).pipe(
897 Effect.provide(computeProviderLive()),
898 Effect.provide(
899 Layer.succeed(PrismaClient, withDefaultBranch(client)),
900 ),
901 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
902 );
903 },
904 { tags: ["provider:prisma:branch"] },
905 );
906
907 it.effect(
908 "rejects invalid production and dev ports",
909 () => {
910 const client = {} as PrismaManagementClient;
911
912 return Effect.gen(function* () {
913 const provider = yield* Compute.Provider;
914 for (const props of [
915 { port: 0 },
916 { port: 65_536 },
917 { port: 1.5 },
918 { dev: { port: Number.NaN } },
919 ]) {
920 const error = yield* provider
921 .reconcile({
922 id: "App",
923 fqn: "App",
924 instanceId: "00000000000000000000000000000000",
925 news: {
926 project: "project-1",
927 appName: "api",
928 ...props,
929 },
930 olds: undefined,
931 output: undefined,
932 session: undefined as never,
933 bindings: [],
934 })
935 .pipe(Effect.flip);
936 expect(error.message).toContain(
937 "must be an integer between 1 and 65535",
938 );
939 }
940 }).pipe(
941 Effect.provide(computeProviderLive()),
942 Effect.provide(
943 Layer.succeed(PrismaClient, withDefaultBranch(client)),
944 ),
945 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
946 );
947 },
948 { tags: ["provider:prisma:branch"] },
949 );
950
951 it.effect(
952 "rejects unassigned branch attachment for Compute deploys",
953 () => {
954 const client = {} as PrismaManagementClient;
955
956 return Effect.gen(function* () {
957 const provider = yield* Compute.Provider;
958 for (const branchProps of [
959 { branchId: null },
960 { branchGitName: null },
961 ]) {
962 const error = yield* provider
963 .reconcile({
964 id: "App",
965 fqn: "App",
966 instanceId: "00000000000000000000000000000000",
967 news: {
968 project: "project-1",
969 appName: "api",
970 ...branchProps,
971 } as unknown as ComputeProps,
972 olds: undefined,
973 output: undefined,
974 session: undefined as never,
975 bindings: [],
976 })
977 .pipe(Effect.flip);
978
979 expect(error).toBeInstanceOf(Error);
980 expect(error.message).toContain("requires an attached branch");
981 }
982 }).pipe(
983 Effect.provide(computeProviderLive()),
984 Effect.provide(
985 Layer.succeed(PrismaClient, withDefaultBranch(client)),
986 ),
987 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
988 );
989 },
990 { tags: ["provider:prisma:branch"] },
991 );
992
993 it.effect(
994 "rejects skipCodeUpload without a version to fork",
995 () => {
996 const calls: Array<[string, unknown]> = [];
997 const client = {
998 getApp: (id: string) => {
999 calls.push(["getApp", id]);
1000 return Effect.succeed({
1001 id,
1002 type: "app" as const,
1003 url: `https://api.prisma.test/v1/services/${id}`,
1004 name: "api",
1005 region: { id: "us-east-1", name: "US East" },
1006 projectId: "project-1",
1007 branchId: "branch-main",
1008 latestDeploymentId: null,
1009 appEndpointDomain: "api.prisma.build",
1010 createdAt: "2026-01-01T00:00:00Z",
1011 });
1012 },
1013 } as unknown as PrismaManagementClient;
1014
1015 return Effect.gen(function* () {
1016 const provider = yield* Compute.Provider;
1017 const error = yield* provider
1018 .reconcile({
1019 id: "App",
1020 fqn: "App",
1021 instanceId: "00000000000000000000000000000000",
1022 news: {
1023 project: "project-1",
1024 appName: "api",
1025 branchId: "branch-main",
1026 skipCodeUpload: true,
1027 start: false,
1028 skipPromote: true,
1029 },
1030 olds: undefined,
1031 output: {
1032 appId: "service-1",
1033 deploymentId: undefined,
1034 projectId: "project-1",
1035 appName: "api",
1036 regionId: "us-east-1",
1037 deploymentEndpointDomain: undefined,
1038 deploymentUrl: undefined,
1039 appEndpointDomain: "api.prisma.build",
1040 url: "https://api.prisma.build",
1041 promoted: false,
1042 previousDeploymentId: undefined,
1043 previousDeploymentAction: undefined,
1044 artifactHash: undefined,
1045 local: false,
1046 },
1047 session: undefined as never,
1048 bindings: [],
1049 })
1050 .pipe(Effect.flip);
1051
1052 expect(error).toBeInstanceOf(Error);
1053 expect(error.message).toContain(
1054 "skipCodeUpload requires an existing Prisma deployment",
1055 );
1056 expect(calls).toEqual([["getApp", "service-1"]]);
1057 }).pipe(
1058 Effect.provide(computeProviderLive()),
1059 Effect.provide(
1060 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1061 ),
1062 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1063 );
1064 },
1065 { tags: ["provider:prisma:branch"] },
1066 );
1067
1068 it.effect(
1069 "rejects effect-native Compute without a main module",
1070 () => {
1071 const client = {} as PrismaManagementClient;
1072
1073 return Effect.gen(function* () {
1074 const provider = yield* Compute.Provider;
1075 const error = yield* provider
1076 .reconcile({
1077 id: "App",
1078 fqn: "App",
1079 instanceId: "00000000000000000000000000000000",
1080 news: {
1081 project: "project-1",
1082 appName: "api",
1083 exports: { default: "runtime" },
1084 },
1085 olds: undefined,
1086 output: undefined,
1087 session: undefined as never,
1088 bindings: [],
1089 })
1090 .pipe(Effect.flip);
1091
1092 expect(error).toBeInstanceOf(Error);
1093 expect(error.message).toContain(
1094 "Effect-native Prisma Compute apps require `main`",
1095 );
1096 }).pipe(
1097 Effect.provide(computeProviderLive()),
1098 Effect.provide(
1099 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1100 ),
1101 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1102 );
1103 },
1104 { tags: ["provider:prisma:branch"] },
1105 );
1106
1107 it.effect(
1108 "rejects effect-native Compute with an external build",
1109 () => {
1110 const client = {} as PrismaManagementClient;
1111
1112 return Effect.gen(function* () {
1113 const provider = yield* Compute.Provider;
1114 const error = yield* provider
1115 .reconcile({
1116 id: "App",
1117 fqn: "App",
1118 instanceId: "00000000000000000000000000000000",
1119 news: {
1120 project: "project-1",
1121 appName: "api",
1122 main: "app.ts",
1123 build: {
1124 command: "bun run build",
1125 outdir: "dist",
1126 },
1127 },
1128 olds: undefined,
1129 output: undefined,
1130 session: undefined as never,
1131 bindings: [],
1132 })
1133 .pipe(Effect.flip);
1134
1135 expect(error).toBeInstanceOf(Error);
1136 expect(error.message).toContain(
1137 "Effect-native Prisma Compute apps cannot use build",
1138 );
1139 }).pipe(
1140 Effect.provide(computeProviderLive()),
1141 Effect.provide(
1142 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1143 ),
1144 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1145 );
1146 },
1147 { tags: ["provider:prisma:branch"] },
1148 );
1149
1150 it.effect(
1151 "rejects effect-native Compute with an invalid handler name",
1152 () => {
1153 const client = {} as PrismaManagementClient;
1154
1155 return Effect.gen(function* () {
1156 const provider = yield* Compute.Provider;
1157 const error = yield* provider
1158 .reconcile({
1159 id: "App",
1160 fqn: "App",
1161 instanceId: "00000000000000000000000000000000",
1162 news: {
1163 project: "project-1",
1164 appName: "api",
1165 main: "app.ts",
1166 handler: "Api;console.log('nope')",
1167 },
1168 olds: undefined,
1169 output: undefined,
1170 session: undefined as never,
1171 bindings: [],
1172 })
1173 .pipe(Effect.flip);
1174
1175 expect(error).toBeInstanceOf(Error);
1176 expect(error.message).toContain(
1177 "handler must be `default` or a valid JavaScript export identifier",
1178 );
1179 }).pipe(
1180 Effect.provide(computeProviderLive()),
1181 Effect.provide(
1182 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1183 ),
1184 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1185 );
1186 },
1187 { tags: ["provider:prisma:branch"] },
1188 );
1189
1190 it.effect(
1191 "replaces Compute when region changes even if project is unresolved",
1192 () => {
1193 const client = {} as PrismaManagementClient;
1194
1195 return Effect.gen(function* () {
1196 const provider = yield* Compute.Provider;
1197 const error = yield* provider.diff!({
1198 id: "App",
1199 fqn: "App",
1200 instanceId: "00000000000000000000000000000000",
1201 olds: {
1202 project: "project-1",
1203 appName: "api",
1204 regionId: "us-east-1",
1205 },
1206 news: {
1207 project: Output.asOutput("project-1"),
1208 appName: "api",
1209 regionId: "us-west-2",
1210 },
1211 oldBindings: [],
1212 newBindings: [],
1213 output: {
1214 appId: "service-1",
1215 deploymentId: "version-1",
1216 projectId: "project-1",
1217 appName: "api",
1218 regionId: "us-east-1",
1219 deploymentEndpointDomain: "version-1.preview.prisma.build",
1220 deploymentUrl: "https://version-1.preview.prisma.build",
1221 appEndpointDomain: "api.prisma.build",
1222 url: "https://api.prisma.build",
1223 promoted: true,
1224 previousDeploymentId: undefined,
1225 previousDeploymentAction: undefined,
1226 artifactHash: Redacted.make("hash-1"),
1227 local: false,
1228 },
1229 } as never).pipe(Effect.flip);
1230
1231 expect(String(error)).toContain("cannot be changed atomically");
1232 }).pipe(
1233 Effect.provide(computeProviderLive()),
1234 Effect.provide(
1235 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1236 ),
1237 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1238 );
1239 },
1240 { tags: ["provider:prisma:branch"] },
1241 );
1242
1243 it.effect(
1244 "replaces Compute when project changes even if region is unresolved",
1245 () => {
1246 const client = {} as PrismaManagementClient;
1247
1248 return Effect.gen(function* () {
1249 const provider = yield* Compute.Provider;
1250 const diff = yield* provider.diff!({
1251 id: "App",
1252 fqn: "App",
1253 instanceId: "00000000000000000000000000000000",
1254 olds: {
1255 project: "project-1",
1256 appName: "api",
1257 regionId: "us-east-1",
1258 },
1259 news: {
1260 project: "project-2",
1261 appName: "api",
1262 regionId: Output.asOutput("us-east-1"),
1263 },
1264 oldBindings: [],
1265 newBindings: [],
1266 output: {
1267 appId: "service-1",
1268 deploymentId: "version-1",
1269 projectId: "project-1",
1270 appName: "api",
1271 regionId: "us-east-1",
1272 deploymentEndpointDomain: "version-1.preview.prisma.build",
1273 deploymentUrl: "https://version-1.preview.prisma.build",
1274 appEndpointDomain: "api.prisma.build",
1275 url: "https://api.prisma.build",
1276 promoted: true,
1277 previousDeploymentId: undefined,
1278 previousDeploymentAction: undefined,
1279 artifactHash: Redacted.make("hash-1"),
1280 local: false,
1281 },
1282 } as never);
1283
1284 expect(diff).toEqual({ action: "replace" });
1285 }).pipe(
1286 Effect.provide(computeProviderLive()),
1287 Effect.provide(
1288 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1289 ),
1290 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1291 );
1292 },
1293 { tags: ["provider:prisma:branch"] },
1294 );
1295
1296 it.effect(
1297 "updates Compute when props are unchanged so artifacts can rehash",
1298 () => {
1299 const client = {} as PrismaManagementClient;
1300
1301 return Effect.gen(function* () {
1302 const provider = yield* Compute.Provider;
1303 const diff = yield* provider.diff!({
1304 id: "App",
1305 fqn: "App",
1306 instanceId: "00000000000000000000000000000000",
1307 olds: {
1308 project: "project-1",
1309 appName: "api",
1310 regionId: "us-east-1",
1311 path: ".",
1312 entrypoint: "server.ts",
1313 },
1314 news: {
1315 project: "project-1",
1316 appName: "api",
1317 regionId: "us-east-1",
1318 path: ".",
1319 entrypoint: "server.ts",
1320 },
1321 oldBindings: [],
1322 newBindings: [],
1323 output: {
1324 appId: "service-1",
1325 deploymentId: "version-1",
1326 projectId: "project-1",
1327 appName: "api",
1328 regionId: "us-east-1",
1329 deploymentEndpointDomain: "version-1.preview.prisma.build",
1330 deploymentUrl: "https://version-1.preview.prisma.build",
1331 appEndpointDomain: "api.prisma.build",
1332 url: "https://api.prisma.build",
1333 promoted: true,
1334 previousDeploymentId: undefined,
1335 previousDeploymentAction: undefined,
1336 artifactHash: Redacted.make("hash-1"),
1337 local: false,
1338 },
1339 } as never);
1340
1341 expect(diff).toEqual({ action: "update" });
1342 }).pipe(
1343 Effect.provide(computeProviderLive()),
1344 Effect.provide(
1345 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1346 ),
1347 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1348 );
1349 },
1350 { tags: ["provider:prisma:branch"] },
1351 );
1352
1353 it.effect("dev provider applies the same Compute prop validation", () =>
1354 Effect.gen(function* () {
1355 const provider = yield* Compute.Provider;
1356 const error = yield* provider
1357 .reconcile({
1358 id: "App",
1359 fqn: "App",
1360 instanceId: "00000000000000000000000000000000",
1361 news: {
1362 project: "project-1",
1363 appName: "api",
1364 skipPromote: true,
1365 destroyOldDeployment: true,
1366 dev: {
1367 url: "http://localhost:3000",
1368 },
1369 },
1370 olds: undefined,
1371 output: undefined,
1372 session: undefined as never,
1373 bindings: [],
1374 })
1375 .pipe(Effect.flip);
1376
1377 expect(error).toBeInstanceOf(Error);
1378 expect(error.message).toContain(
1379 "destroyOldDeployment cannot be combined with skipPromote",
1380 );
1381 }).pipe(
1382 Effect.provide(ComputeDevProvider()),
1383 Effect.provide(
1384 Layer.succeed(AlchemyContext, {
1385 dotAlchemy: ".alchemy",
1386 dev: true,
1387 adopt: false,
1388 }),
1389 ),
1390 Effect.provide(PlatformServices),
1391 ),
1392 );
1393
1394 it.effect(
1395 "adopts only the matching branch's latest deployment as Unowned",
1396 () => {
1397 const calls: Array<[string, unknown]> = [];
1398 const client = {
1399 listApps: (projectId: string, query: unknown) => {
1400 calls.push(["listApps", { projectId, query }]);
1401 return Effect.succeed([
1402 {
1403 id: "service-1",
1404 type: "app" as const,
1405 url: "https://api.prisma.test/v1/services/service-1",
1406 name: "api",
1407 region: { id: "us-east-1", name: "US East" },
1408 projectId,
1409 branchId: "branch-main",
1410 latestDeploymentId: "version-live",
1411 appEndpointDomain: "api.prisma.build",
1412 createdAt: "2026-01-01T00:00:00Z",
1413 },
1414 {
1415 id: "service-feature",
1416 type: "app" as const,
1417 url: "https://api.prisma.test/v1/services/service-feature",
1418 name: "api",
1419 region: { id: "us-east-1", name: "US East" },
1420 projectId,
1421 branchId: "branch-feature",
1422 latestDeploymentId: "version-feature",
1423 appEndpointDomain: "feature.prisma.build",
1424 createdAt: "2026-01-01T00:00:00Z",
1425 },
1426 ]);
1427 },
1428 getDeployment: (id: string) => {
1429 calls.push(["getDeployment", id]);
1430 return Effect.succeed({
1431 id,
1432 type: "deployment" as const,
1433 serviceId: "service-1",
1434 url: `https://api.prisma.test/v1/deployments/${id}`,
1435 foundryVersionId: "foundry-live",
1436 status: "running",
1437 previewDomain: "version-live.preview.prisma.build",
1438 createdAt: "2026-01-01T00:00:00Z",
1439 });
1440 },
1441 listAppDeployments: () =>
1442 Effect.succeed([
1443 {
1444 id: "version-old",
1445 type: "deployment" as const,
1446 serviceId: "service-1",
1447 url: "https://api.prisma.test/v1/deployments/version-old",
1448 foundryVersionId: "foundry-version-old",
1449 createdAt: "2026-01-01T00:00:00Z",
1450 },
1451 ]),
1452 } as unknown as PrismaManagementClient;
1453
1454 return Effect.gen(function* () {
1455 const provider = yield* Provider.findProvider(Compute);
1456 const output = yield* provider.read!({
1457 id: "App",
1458 fqn: "App",
1459 instanceId: "00000000000000000000000000000000",
1460 olds: {
1461 project: "project-1",
1462 appName: "api",
1463 },
1464 output: undefined,
1465 });
1466
1467 expect(output?.appId).toBe("service-1");
1468 expect(output?.deploymentId).toBe("version-live");
1469 expect(output?.deploymentEndpointDomain).toBe(
1470 "version-live.preview.prisma.build",
1471 );
1472 expect(output?.deploymentUrl).toBe(
1473 "https://version-live.preview.prisma.build",
1474 );
1475 expect(output?.appEndpointDomain).toBe("api.prisma.build");
1476 expect(output?.url).toBe("https://api.prisma.build");
1477 expect(output?.promoted).toBe(true);
1478 expect(Unowned.is(output!)).toBe(true);
1479 expect(calls).toEqual([
1480 ["listApps", { projectId: "project-1", query: { limit: "100" } }],
1481 ["getDeployment", "version-live"],
1482 ]);
1483 }).pipe(
1484 Effect.provide(computeProviderLive()),
1485 Effect.provide(
1486 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1487 ),
1488 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1489 Effect.provide(FetchHttpClient.layer),
1490 Effect.provide(PlatformServices),
1491 );
1492 },
1493 { tags: ["provider:prisma:branch"] },
1494 );
1495
1496 it.effect(
1497 "reads the live deployment through the canonical route",
1498 () => {
1499 const calls: Array<[string, unknown]> = [];
1500 const client = {
1501 listApps: (projectId: string, query: unknown) => {
1502 calls.push(["listApps", { projectId, query }]);
1503 return Effect.succeed([
1504 {
1505 id: "service-1",
1506 type: "app" as const,
1507 url: "https://api.prisma.test/v1/services/service-1",
1508 name: "api",
1509 region: { id: "us-east-1", name: "US East" },
1510 projectId,
1511 branchId: "branch-main",
1512 latestDeploymentId: "version-live",
1513 appEndpointDomain: "api.prisma.build",
1514 createdAt: "2026-01-01T00:00:00Z",
1515 },
1516 ]);
1517 },
1518 getDeployment: (id: string) => {
1519 calls.push(["getDeployment", id]);
1520 return Effect.succeed({
1521 id,
1522 type: "deployment" as const,
1523 serviceId: "service-1",
1524 url: `https://api.prisma.test/v1/deployments/${id}`,
1525 foundryVersionId: "foundry-live",
1526 status: "running",
1527 previewDomain: "version-live.preview.prisma.build",
1528 createdAt: "2026-01-01T00:00:00Z",
1529 });
1530 },
1531 } as unknown as PrismaManagementClient;
1532
1533 return Effect.gen(function* () {
1534 const provider = yield* Provider.findProvider(Compute);
1535 const output = yield* provider.read!({
1536 id: "App",
1537 fqn: "App",
1538 instanceId: "00000000000000000000000000000000",
1539 olds: {
1540 project: "project-1",
1541 appName: "api",
1542 },
1543 output: undefined,
1544 });
1545
1546 expect(output?.deploymentId).toBe("version-live");
1547 expect(output?.promoted).toBe(true);
1548 expect(output?.deploymentUrl).toBe(
1549 "https://version-live.preview.prisma.build",
1550 );
1551 expect(calls).toEqual([
1552 ["listApps", { projectId: "project-1", query: { limit: "100" } }],
1553 ["getDeployment", "version-live"],
1554 ]);
1555 }).pipe(
1556 Effect.provide(computeProviderLive()),
1557 Effect.provide(
1558 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1559 ),
1560 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1561 Effect.provide(FetchHttpClient.layer),
1562 Effect.provide(PlatformServices),
1563 );
1564 },
1565 { tags: ["provider:prisma:branch"] },
1566 );
1567
1568 it.effect(
1569 "marks stored deployment unpromoted when live latest differs",
1570 () => {
1571 const calls: Array<[string, unknown]> = [];
1572 const client = {
1573 getApp: (id: string) => {
1574 calls.push(["getApp", id]);
1575 return Effect.succeed({
1576 id,
1577 type: "app" as const,
1578 url: "https://api.prisma.test/v1/services/service-1",
1579 name: "api",
1580 region: { id: "us-east-1", name: "US East" },
1581 projectId: "project-1",
1582 branchId: "branch-main",
1583 latestDeploymentId: "version-live",
1584 appEndpointDomain: "api.prisma.build",
1585 createdAt: "2026-01-01T00:00:00Z",
1586 });
1587 },
1588 getDeployment: (id: string) => {
1589 calls.push(["getDeployment", id]);
1590 return Effect.succeed({
1591 id,
1592 type: "deployment" as const,
1593 serviceId: "service-1",
1594 url: `https://api.prisma.test/v1/deployments/${id}`,
1595 foundryVersionId: `foundry-${id}`,
1596 status: "running",
1597 previewDomain: `${id}.preview.prisma.build`,
1598 createdAt: "2026-01-01T00:00:00Z",
1599 });
1600 },
1601 listAppDeployments: () =>
1602 Effect.succeed([
1603 {
1604 id: "version-old",
1605 type: "deployment" as const,
1606 serviceId: "service-1",
1607 url: "https://api.prisma.test/v1/deployments/version-old",
1608 foundryVersionId: "foundry-version-old",
1609 createdAt: "2026-01-01T00:00:00Z",
1610 },
1611 ]),
1612 } as unknown as PrismaManagementClient;
1613
1614 return Effect.gen(function* () {
1615 const provider = yield* Provider.findProvider(Compute);
1616 const output = yield* provider.read!({
1617 id: "App",
1618 fqn: "App",
1619 instanceId: "00000000000000000000000000000000",
1620 olds: {
1621 project: "project-1",
1622 appName: "api",
1623 },
1624 output: {
1625 appId: "service-1",
1626 deploymentId: "version-old",
1627 projectId: "project-1",
1628 appName: "api",
1629 regionId: "us-east-1",
1630 deploymentEndpointDomain: "version-old.preview.prisma.build",
1631 deploymentUrl: "https://version-old.preview.prisma.build",
1632 appEndpointDomain: "api.prisma.build",
1633 url: "https://api.prisma.build",
1634 promoted: true,
1635 previousDeploymentId: undefined,
1636 previousDeploymentAction: undefined,
1637 artifactHash: Redacted.make("hash-old"),
1638 local: false,
1639 },
1640 });
1641
1642 expect(output?.deploymentId).toBe("version-old");
1643 expect(output?.promoted).toBe(false);
1644 expect(output?.deploymentEndpointDomain).toBe(
1645 "version-old.preview.prisma.build",
1646 );
1647 expect(output?.url).toBe("https://version-old.preview.prisma.build");
1648 expect(calls).toEqual([
1649 ["getApp", "service-1"],
1650 ["getDeployment", "version-old"],
1651 ]);
1652 }).pipe(
1653 Effect.provide(computeProviderLive()),
1654 Effect.provide(
1655 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1656 ),
1657 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1658 Effect.provide(FetchHttpClient.layer),
1659 Effect.provide(PlatformServices),
1660 );
1661 },
1662 { tags: ["provider:prisma:branch"] },
1663 );
1664
1665 it.effect(
1666 "returns the preview URL for unpromoted Compute deploys",
1667 () => {
1668 const calls: Array<[string, unknown?]> = [];
1669 let status = "new";
1670 const client = {
1671 getApp: (id: string) => {
1672 calls.push(["getApp", id]);
1673 return Effect.succeed({
1674 id,
1675 type: "app" as const,
1676 url: "https://api.prisma.test/v1/services/service-1",
1677 name: "api",
1678 region: { id: "us-east-1", name: "US East" },
1679 projectId: "project-1",
1680 branchId: "branch-main",
1681 latestDeploymentId: "version-live",
1682 appEndpointDomain: "api.prisma.build",
1683 createdAt: "2026-01-01T00:00:00Z",
1684 });
1685 },
1686 listEnvironmentVariables: () => Effect.succeed([]),
1687 createAppDeployment: (appId: string, input: unknown) => {
1688 calls.push(["createAppDeployment", { appId, input }]);
1689 return Effect.succeed({
1690 id: "version-new",
1691 type: "deployment" as const,
1692 url: "https://api.prisma.test/v1/deployments/version-new",
1693 foundryVersionId: "foundry-new",
1694 uploadUrl: null,
1695 });
1696 },
1697 getDeployment: (id: string) => {
1698 calls.push(["getDeployment", id]);
1699 return Effect.succeed({
1700 id,
1701 type: "deployment" as const,
1702 serviceId: "service-1",
1703 url: `https://api.prisma.test/v1/deployments/${id}`,
1704 foundryVersionId: `foundry-${id}`,
1705 status,
1706 previewDomain: "version-new.preview.prisma.build",
1707 createdAt: "2026-01-01T00:00:00Z",
1708 });
1709 },
1710 startDeployment: (id: string) =>
1711 Effect.sync(() => {
1712 calls.push(["startDeployment", id]);
1713 status = "running";
1714 return { previewDomain: "version-new.preview.prisma.build" };
1715 }),
1716 } as unknown as PrismaManagementClient;
1717
1718 return Effect.gen(function* () {
1719 const provider = yield* Compute.Provider;
1720 const output = yield* provider.reconcile({
1721 id: "App",
1722 fqn: "App",
1723 instanceId: "00000000000000000000000000000000",
1724 news: {
1725 project: "project-1",
1726 appName: "api",
1727 branchId: "branch-main",
1728 skipCodeUpload: true,
1729 skipPromote: true,
1730 verifyUrl: false,
1731 },
1732 olds: undefined,
1733 output: {
1734 appId: "service-1",
1735 deploymentId: undefined,
1736 projectId: "project-1",
1737 appName: "api",
1738 regionId: "us-east-1",
1739 deploymentEndpointDomain: undefined,
1740 deploymentUrl: undefined,
1741 appEndpointDomain: "api.prisma.build",
1742 url: "https://api.prisma.build",
1743 promoted: true,
1744 previousDeploymentId: undefined,
1745 previousDeploymentAction: undefined,
1746 artifactHash: undefined,
1747 local: false,
1748 },
1749 session: undefined as never,
1750 bindings: [],
1751 });
1752
1753 expect(output.promoted).toBe(false);
1754 expect(output.deploymentUrl).toBe(
1755 "https://version-new.preview.prisma.build",
1756 );
1757 expect(output.appEndpointDomain).toBe("api.prisma.build");
1758 expect(output.url).toBe("https://version-new.preview.prisma.build");
1759 expect(calls).toEqual([
1760 ["getApp", "service-1"],
1761 [
1762 "createAppDeployment",
1763 {
1764 appId: "service-1",
1765 input: { portMapping: { http: 8080 }, skipCodeUpload: true },
1766 },
1767 ],
1768 ["getDeployment", "version-new"],
1769 ["startDeployment", "version-new"],
1770 ["getDeployment", "version-new"],
1771 ]);
1772 }).pipe(
1773 Effect.provide(computeProviderLive()),
1774 Effect.provide(
1775 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1776 ),
1777 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1778 Effect.provide(FetchHttpClient.layer),
1779 Effect.provide(PlatformServices),
1780 );
1781 },
1782 { tags: ["provider:prisma:branch"] },
1783 );
1784
1785 it.effect(
1786 "syncs a newly created service branch before creating a version",
1787 () => {
1788 const calls: Array<[string, unknown]> = [];
1789 const client = {
1790 listApps: (projectId: string, query: unknown) => {
1791 calls.push(["listApps", { projectId, query }]);
1792 return Effect.succeed([]);
1793 },
1794 createApp: (projectId: string, input: unknown) => {
1795 calls.push(["createApp", { projectId, input }]);
1796 return Effect.succeed({
1797 id: "service-1",
1798 type: "app" as const,
1799 url: "https://api.prisma.test/v1/services/service-1",
1800 name: "api",
1801 region: { id: "us-east-1", name: "US East" },
1802 projectId,
1803 branchId: null,
1804 latestDeploymentId: null,
1805 appEndpointDomain: "api.prisma.build",
1806 createdAt: "2026-01-01T00:00:00Z",
1807 });
1808 },
1809 updateApp: (id: string, input: unknown) => {
1810 calls.push(["updateApp", { id, input }]);
1811 return Effect.succeed({
1812 id,
1813 type: "app" as const,
1814 url: "https://api.prisma.test/v1/services/service-1",
1815 name: "api",
1816 region: { id: "us-east-1", name: "US East" },
1817 projectId: "project-1",
1818 branchId: "branch-main",
1819 latestDeploymentId: null,
1820 appEndpointDomain: "api.prisma.build",
1821 createdAt: "2026-01-01T00:00:00Z",
1822 });
1823 },
1824 createAppDeployment: (appId: string, input: unknown) => {
1825 calls.push(["createAppDeployment", { appId, input }]);
1826 return Effect.succeed({
1827 id: "version-1",
1828 type: "deployment" as const,
1829 url: "https://api.prisma.test/v1/deployments/version-1",
1830 foundryVersionId: "foundry-1",
1831 uploadUrl: "https://upload.prisma.test/version-1.tar.gz",
1832 });
1833 },
1834 getDeployment: (id: string) => {
1835 calls.push(["getDeployment", id]);
1836 return Effect.succeed({
1837 id,
1838 type: "deployment" as const,
1839 serviceId: "service-1",
1840 url: "https://api.prisma.test/v1/deployments/version-1",
1841 foundryVersionId: "foundry-1",
1842 status: "new",
1843 previewDomain: "version-1.preview.prisma.build",
1844 createdAt: "2026-01-01T00:00:00Z",
1845 });
1846 },
1847 } as unknown as PrismaManagementClient;
1848 const http = HttpClient.make((request) =>
1849 Effect.succeed(
1850 HttpClientResponse.fromWeb(request, new Response(null)),
1851 ),
1852 );
1853 return Effect.gen(function* () {
1854 const provider = yield* Compute.Provider;
1855 const output = yield* provider.reconcile({
1856 id: "App",
1857 fqn: "App",
1858 instanceId: "00000000000000000000000000000000",
1859 news: {
1860 project: "project-1",
1861 appName: "api",
1862 branchId: "branch-main",
1863 artifactPath: fixtureArtifactPath,
1864 start: false,
1865 skipPromote: true,
1866 },
1867 olds: undefined,
1868 output: undefined,
1869 session: undefined as never,
1870 bindings: [],
1871 });
1872
1873 expect(output.appId).toBe("service-1");
1874 expect(output.deploymentId).toBe("version-1");
1875 expect(calls).toContainEqual([
1876 "createApp",
1877 {
1878 projectId: "project-1",
1879 input: {
1880 displayName: "api",
1881 regionId: undefined,
1882 branchId: "branch-main",
1883 branchGitName: undefined,
1884 },
1885 },
1886 ]);
1887 expect(calls).toContainEqual([
1888 "updateApp",
1889 {
1890 id: "service-1",
1891 input: {
1892 displayName: "api",
1893 branchId: "branch-main",
1894 branchGitName: undefined,
1895 },
1896 },
1897 ]);
1898 const updateIndex = calls.findIndex(([name]) => name === "updateApp");
1899 const versionIndex = calls.findIndex(
1900 ([name]) => name === "createAppDeployment",
1901 );
1902 expect(updateIndex).toBeGreaterThan(-1);
1903 expect(versionIndex).toBeGreaterThan(updateIndex);
1904 }).pipe(
1905 Effect.provide(computeProviderLive()),
1906 Effect.provide(
1907 Layer.succeed(PrismaClient, withDefaultBranch(client)),
1908 ),
1909 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
1910 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
1911 Effect.provide(PlatformServices),
1912 );
1913 },
1914 { tags: ["provider:prisma:branch"] },
1915 );
1916
1917 it.effect(
1918 "creates a new version when branch attachment changes without artifact changes",
1919 () => {
1920 const calls: Array<[string, unknown]> = [];
1921 let branchId: string | null = "branch-main";
1922 let versionCounter = 0;
1923
1924 const service = () => ({
1925 id: "service-1",
1926 type: "app" as const,
1927 url: "https://api.prisma.test/v1/services/service-1",
1928 name: "api",
1929 region: { id: "us-east-1", name: "US East" },
1930 projectId: "project-1",
1931 branchId,
1932 latestDeploymentId: "version-seed",
1933 appEndpointDomain: "api.prisma.build",
1934 createdAt: "2026-01-01T00:00:00Z",
1935 });
1936
1937 const client = {
1938 getApp: (id: string) => {
1939 calls.push(["getApp", id]);
1940 return Effect.succeed(service());
1941 },
1942 updateApp: (id: string, input: { branchId?: string | null }) => {
1943 calls.push(["updateApp", { id, input }]);
1944 branchId = input.branchId ?? null;
1945 return Effect.succeed(service());
1946 },
1947 createAppDeployment: (appId: string, input: unknown) => {
1948 versionCounter += 1;
1949 const id = `version-${versionCounter}`;
1950 calls.push(["createAppDeployment", { appId, input, id }]);
1951 return Effect.succeed({
1952 id,
1953 type: "deployment" as const,
1954 url: `https://api.prisma.test/v1/deployments/${id}`,
1955 foundryVersionId: `foundry-${versionCounter}`,
1956 uploadUrl: null,
1957 });
1958 },
1959 getDeployment: (id: string) => {
1960 calls.push(["getDeployment", id]);
1961 return Effect.succeed({
1962 id,
1963 type: "deployment" as const,
1964 serviceId: "service-1",
1965 url: `https://api.prisma.test/v1/deployments/${id}`,
1966 foundryVersionId: id.replace("version", "foundry"),
1967 status: "new",
1968 previewDomain: `${id}.preview.prisma.build`,
1969 createdAt: "2026-01-01T00:00:00Z",
1970 });
1971 },
1972 } as unknown as PrismaManagementClient;
1973
1974 const baseProps = {
1975 project: "project-1",
1976 appName: "api",
1977 branchId: "branch-main",
1978 skipCodeUpload: true,
1979 start: false,
1980 skipPromote: true,
1981 };
1982
1983 return Effect.gen(function* () {
1984 const provider = yield* Compute.Provider;
1985 const first = yield* provider.reconcile({
1986 id: "App",
1987 fqn: "App",
1988 instanceId: "00000000000000000000000000000000",
1989 news: baseProps,
1990 olds: undefined,
1991 output: {
1992 appId: "service-1",
1993 deploymentId: undefined,
1994 projectId: "project-1",
1995 appName: "api",
1996 regionId: "us-east-1",
1997 deploymentEndpointDomain: undefined,
1998 deploymentUrl: undefined,
1999 appEndpointDomain: "api.prisma.build",
2000 url: "https://api.prisma.build",
2001 promoted: false,
2002 previousDeploymentId: undefined,
2003 previousDeploymentAction: undefined,
2004 artifactHash: undefined,
2005 local: false,
2006 },
2007 session: undefined as never,
2008 bindings: [],
2009 });
2010
2011 const second = yield* provider.reconcile({
2012 id: "App",
2013 fqn: "App",
2014 instanceId: "00000000000000000000000000000000",
2015 news: { ...baseProps, branchId: "branch-feature" },
2016 olds: baseProps,
2017 output: first,
2018 session: undefined as never,
2019 bindings: [],
2020 });
2021
2022 expect(first.deploymentId).toBe("version-1");
2023 expect(second.deploymentId).toBe("version-2");
2024 expect(second.artifactHash).not.toBe(first.artifactHash);
2025 expect(calls).toContainEqual([
2026 "updateApp",
2027 {
2028 id: "service-1",
2029 input: {
2030 displayName: "api",
2031 branchId: "branch-feature",
2032 branchGitName: undefined,
2033 },
2034 },
2035 ]);
2036 expect(
2037 calls.filter(([name]) => name === "createAppDeployment"),
2038 ).toHaveLength(2);
2039 }).pipe(
2040 Effect.provide(computeProviderLive()),
2041 Effect.provide(
2042 Layer.succeed(PrismaClient, withDefaultBranch(client)),
2043 ),
2044 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
2045 );
2046 },
2047 { tags: ["provider:prisma:branch"] },
2048 );
2049
2050 it.effect(
2051 "does not mutate remote Compute state when artifact resolution fails",
2052 () => {
2053 const calls: Array<[string, unknown?]> = [];
2054 const client = {
2055 listApps: (projectId: string, query: unknown) => {
2056 calls.push(["listApps", { projectId, query }]);
2057 return Effect.succeed([]);
2058 },
2059 createApp: (projectId: string, input: unknown) => {
2060 calls.push(["createApp", { projectId, input }]);
2061 return Effect.die("should not create service");
2062 },
2063 listEnvironmentVariables: (query: unknown) => {
2064 calls.push(["listEnvironmentVariables", query]);
2065 return Effect.succeed([]);
2066 },
2067 createEnvironmentVariable: (input: unknown) => {
2068 calls.push(["createEnvironmentVariable", input]);
2069 return Effect.die("should not create env");
2070 },
2071 } as unknown as PrismaManagementClient;
2072
2073 return Effect.gen(function* () {
2074 const path = yield* Path.Path;
2075 const missingArtifact = path.resolve(
2076 "tmp",
2077 "alchemy-prisma-missing-artifact.tar.gz",
2078 );
2079
2080 const provider = yield* Compute.Provider;
2081 const error = yield* provider
2082 .reconcile({
2083 id: "App",
2084 fqn: "App",
2085 instanceId: "00000000000000000000000000000000",
2086 news: {
2087 project: "project-1",
2088 appName: "api",
2089 artifactPath: missingArtifact,
2090 env: {
2091 TOKEN: "secret",
2092 },
2093 },
2094 olds: undefined,
2095 output: undefined,
2096 session: undefined as never,
2097 bindings: [],
2098 })
2099 .pipe(Effect.flip);
2100
2101 expect(error).toBeDefined();
2102 expect(calls).toEqual([]);
2103 }).pipe(
2104 Effect.provide(computeProviderLive()),
2105 Effect.provide(
2106 Layer.succeed(PrismaClient, withDefaultBranch(client)),
2107 ),
2108 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
2109 Effect.provide(PlatformServices),
2110 );
2111 },
2112 { tags: ["provider:prisma:branch"] },
2113 );
2114
2115 it.effect(
2116 "forwards command build output limits before cloud mutation",
2117 () =>
2118 Effect.gen(function* () {
2119 const fs = yield* FileSystem.FileSystem;
2120 const root = yield* fs.makeTempDirectory({
2121 prefix: "alchemy-prisma-compute-build-limit-",
2122 });
2123 const provider = yield* Compute.Provider;
2124 const error = yield* provider
2125 .reconcile({
2126 id: "App",
2127 fqn: "App",
2128 instanceId: "00000000000000000000000000000000",
2129 news: {
2130 project: "project-1",
2131 appName: "api",
2132 path: root,
2133 build: {
2134 command: "mkdir -p dist; printf '123456789'",
2135 outdir: "dist",
2136 entrypoint: "server.js",
2137 outputLimitBytes: 8,
2138 },
2139 },
2140 olds: undefined,
2141 output: undefined,
2142 session: undefined as never,
2143 bindings: [],
2144 })
2145 .pipe(Effect.flip);
2146
2147 expect(error.message).toContain(
2148 "Build stdout exceeded the 8 byte output safety limit",
2149 );
2150 }).pipe(
2151 Effect.provide(computeProviderLive()),
2152 Effect.provide(
2153 Layer.succeed(
2154 PrismaClient,
2155 withDefaultBranch({} as PrismaManagementClient),
2156 ),
2157 ),
2158 // Fails before any cloud mutation: every Management API route is
2159 // unregistered so a stray call fails loudly.
2160 Effect.provide(makeFakeManagementApi(unhandled).layer),
2161 Effect.provide(PlatformServices),
2162 ),
2163 { tags: ["provider:prisma:branch"] },
2164 );
2165
2166 it.effect(
2167 "fails when Prisma omits an upload URL for app artifacts",
2168 () => {
2169 const calls: Array<[string, unknown?]> = [];
2170 const client = {
2171 getApp: () => {
2172 calls.push(["getApp"]);
2173 return Effect.succeed({
2174 id: "service-1",
2175 type: "app" as const,
2176 url: "https://api.prisma.test/v1/services/service-1",
2177 name: "api",
2178 region: { id: "us-east-1", name: "US East" },
2179 projectId: "project-1",
2180 branchId: "branch-main",
2181 latestDeploymentId: "version-old",
2182 appEndpointDomain: "api.prisma.build",
2183 createdAt: "2026-01-01T00:00:00Z",
2184 });
2185 },
2186 listEnvironmentVariables: () => Effect.succeed([]),
2187 createAppDeployment: () => {
2188 calls.push(["createAppDeployment"]);
2189 return Effect.succeed({
2190 id: "version-1",
2191 type: "deployment" as const,
2192 url: "https://api.prisma.test/v1/deployments/version-1",
2193 foundryVersionId: "foundry-1",
2194 uploadUrl: null,
2195 });
2196 },
2197 getDeployment: (id: string) => {
2198 calls.push(["getDeployment", id]);
2199 return Effect.succeed({
2200 id,
2201 type: "deployment" as const,
2202 serviceId: "service-1",
2203 url: `https://api.prisma.test/v1/deployments/${id}`,
2204 foundryVersionId: "foundry-1",
2205 status: "new",
2206 previewDomain: null,
2207 createdAt: "2026-01-01T00:00:00Z",
2208 });
2209 },
2210 deleteDeployment: (id: string) => {
2211 calls.push(["deleteDeployment", id]);
2212 return Effect.void;
2213 },
2214 } as unknown as PrismaManagementClient;
2215
2216 return Effect.gen(function* () {
2217 const provider = yield* Compute.Provider;
2218 const error = yield* provider
2219 .reconcile({
2220 id: "App",
2221 fqn: "App",
2222 instanceId: "00000000000000000000000000000000",
2223 news: {
2224 project: "project-1",
2225 appName: "api",
2226 artifactPath: fixtureArtifactPath,
2227 branchId: "branch-main",
2228 start: false,
2229 skipPromote: true,
2230 },
2231 olds: undefined,
2232 output: {
2233 appId: "service-1",
2234 deploymentId: undefined,
2235 projectId: "project-1",
2236 appName: "api",
2237 regionId: "us-east-1",
2238 deploymentEndpointDomain: undefined,
2239 deploymentUrl: undefined,
2240 appEndpointDomain: "api.prisma.build",
2241 url: "https://api.prisma.build",
2242 promoted: false,
2243 previousDeploymentId: undefined,
2244 previousDeploymentAction: undefined,
2245 artifactHash: undefined,
2246 local: false,
2247 },
2248 session: undefined as never,
2249 bindings: [],
2250 })
2251 .pipe(Effect.flip);
2252
2253 expect(error).toBeInstanceOf(Error);
2254 expect(error.message).toContain("did not return an upload URL");
2255 expect(calls).toContainEqual(["deleteDeployment", "version-1"]);
2256 }).pipe(
2257 Effect.provide(computeProviderLive()),
2258 Effect.provide(
2259 Layer.succeed(PrismaClient, withDefaultBranch(client)),
2260 ),
2261 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
2262 Effect.provide(PlatformServices),
2263 );
2264 },
2265 { tags: ["provider:prisma:branch"] },
2266 );
2267
2268 it.effect(
2269 "refuses a persisted App with mismatched immutable identity",
2270 () => {
2271 const calls: string[] = [];
2272 const client = {
2273 getApp: () => {
2274 calls.push("getApp");
2275 return Effect.succeed({
2276 id: "service-1",
2277 type: "app" as const,
2278 url: "https://api.prisma.test/v1/services/service-1",
2279 name: "api",
2280 region: { id: "us-east-1", name: "US East" },
2281 projectId: "project-other",
2282 branchId: "branch-main",
2283 latestDeploymentId: null,
2284 appEndpointDomain: "api.prisma.build",
2285 createdAt: "2026-01-01T00:00:00Z",
2286 });
2287 },
2288 updateApp: () =>
2289 Effect.die("must not patch immutable identity drift"),
2290 createAppDeployment: () =>
2291 Effect.die("must not deploy against immutable identity drift"),
2292 } as unknown as PrismaManagementClient;
2293
2294 return Effect.gen(function* () {
2295 const provider = yield* Compute.Provider;
2296 const error = yield* provider
2297 .reconcile({
2298 id: "App",
2299 fqn: "App",
2300 instanceId: "00000000000000000000000000000000",
2301 news: {
2302 project: "project-1",
2303 appName: "api",
2304 artifactPath: fixtureArtifactPath,
2305 branchId: "branch-main",
2306 start: false,
2307 skipPromote: true,
2308 },
2309 olds: undefined,
2310 output: {
2311 appId: "service-1",
2312 deploymentId: undefined,
2313 projectId: "project-1",
2314 appName: "api",
2315 regionId: "us-east-1",
2316 deploymentEndpointDomain: undefined,
2317 deploymentUrl: undefined,
2318 appEndpointDomain: "api.prisma.build",
2319 url: "https://api.prisma.build",
2320 promoted: false,
2321 previousDeploymentId: undefined,
2322 previousDeploymentAction: undefined,
2323 artifactHash: undefined,
2324 local: false,
2325 },
2326 session: undefined as never,
2327 bindings: [],
2328 })
2329 .pipe(Effect.flip);
2330
2331 expect(error.message).toContain("project-other");
2332 expect(error.message).toContain("Refusing to patch");
2333 expect(calls).toEqual(["getApp"]);
2334 }).pipe(
2335 Effect.provide(computeProviderLive()),
2336 Effect.provide(
2337 Layer.succeed(PrismaClient, withDefaultBranch(client)),
2338 ),
2339 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
2340 Effect.provide(FetchHttpClient.layer),
2341 Effect.provide(PlatformServices),
2342 );
2343 },
2344 { tags: ["provider:prisma:branch"] },
2345 );
2346
2347 it.effect(
2348 "deletes a newly created App when a later create step fails",
2349 () => {
2350 const calls: Array<[string, unknown?]> = [];
2351 const client = {
2352 listApps: (projectId: string, query: unknown) => {
2353 calls.push(["listApps", { projectId, query }]);
2354 return Effect.succeed([]);
2355 },
2356 createApp: (projectId: string, input: unknown) => {
2357 calls.push(["createApp", { projectId, input }]);
2358 return Effect.succeed({
2359 id: "service-1",
2360 type: "app" as const,
2361 url: "https://api.prisma.test/v1/services/service-1",
2362 name: "api",
2363 region: { id: "us-east-1", name: "US East" },
2364 projectId,
2365 branchId: "branch-main",
2366 latestDeploymentId: null,
2367 appEndpointDomain: "api.prisma.build",
2368 createdAt: "2026-01-01T00:00:00Z",
2369 });
2370 },
2371 listEnvironmentVariables: () => Effect.succeed([]),
2372 createAppDeployment: () => {
2373 calls.push(["createAppDeployment"]);
2374 return Effect.succeed({
2375 id: "version-1",
2376 type: "deployment" as const,
2377 url: "https://api.prisma.test/v1/deployments/version-1",
2378 foundryVersionId: "foundry-1",
2379 uploadUrl: null,
2380 });
2381 },
2382 getDeployment: (id: string) => {
2383 calls.push(["getDeployment", id]);
2384 return Effect.succeed({
2385 id,
2386 type: "deployment" as const,
2387 serviceId: "service-1",
2388 url: `https://api.prisma.test/v1/deployments/${id}`,
2389 foundryVersionId: "foundry-1",
2390 status: "new",
2391 previewDomain: null,
2392 createdAt: "2026-01-01T00:00:00Z",
2393 });
2394 },
2395 deleteDeployment: (id: string) => {
2396 calls.push(["deleteDeployment", id]);
2397 return Effect.void;
2398 },
2399 listAppDeployments: (appId: string) => {
2400 calls.push(["listAppDeployments", appId]);
2401 return Effect.succeed([]);
2402 },
2403 deleteApp: (id: string) => {
2404 calls.push(["deleteApp", id]);
2405 return Effect.void;
2406 },
2407 } as unknown as PrismaManagementClient;
2408
2409 return Effect.gen(function* () {
2410 const provider = yield* Compute.Provider;
2411 const error = yield* provider
2412 .reconcile({
2413 id: "App",
2414 fqn: "App",
2415 instanceId: "00000000000000000000000000000000",
2416 news: {
2417 project: "project-1",
2418 appName: "api",
2419 artifactPath: fixtureArtifactPath,
2420 branchId: "branch-main",
2421 start: false,
2422 skipPromote: true,
2423 },
2424 olds: undefined,
2425 output: undefined,
2426 session: undefined as never,
2427 bindings: [],
2428 })
2429 .pipe(Effect.flip);
2430
2431 expect(error).toBeInstanceOf(Error);
2432 expect(error.message).toContain("did not return an upload URL");
2433 expect(calls).toContainEqual(["deleteDeployment", "version-1"]);
2434 expect(calls).toContainEqual(["deleteApp", "service-1"]);
2435 }).pipe(
2436 Effect.provide(computeProviderLive()),
2437 Effect.provide(
2438 Layer.succeed(PrismaClient, withDefaultBranch(client)),
2439 ),
2440 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
2441 Effect.provide(PlatformServices),
2442 );
2443 },
2444 { tags: ["provider:prisma:branch"] },
2445 );
2446
2447 it.effect(
2448 "deletes created deployment when artifact upload fails",
2449 () => {
2450 const calls: Array<[string, unknown?]> = [];
2451 const client = {
2452 getApp: () =>
2453 Effect.succeed({
2454 id: "service-1",
2455 type: "app" as const,
2456 url: "https://api.prisma.test/v1/services/service-1",
2457 name: "api",
2458 region: { id: "us-east-1", name: "US East" },
2459 projectId: "project-1",
2460 branchId: "branch-main",
2461 latestDeploymentId: null,
2462 appEndpointDomain: "api.prisma.build",
2463 createdAt: "2026-01-01T00:00:00Z",
2464 }),
2465 listEnvironmentVariables: () => Effect.succeed([]),
2466 createAppDeployment: () => {
2467 calls.push(["createAppDeployment"]);
2468 return Effect.succeed({
2469 id: "version-1",
2470 type: "deployment" as const,
2471 url: "https://api.prisma.test/v1/deployments/version-1",
2472 foundryVersionId: "foundry-1",
2473 uploadUrl: "https://upload.prisma.test/app.tar.gz",
2474 });
2475 },
2476 getDeployment: (id: string) => {
2477 calls.push(["getDeployment", id]);
2478 return Effect.succeed({
2479 id,
2480 type: "deployment" as const,
2481 serviceId: "service-1",
2482 url: `https://api.prisma.test/v1/deployments/${id}`,
2483 foundryVersionId: "foundry-1",
2484 status: "new",
2485 previewDomain: null,
2486 createdAt: "2026-01-01T00:00:00Z",
2487 });
2488 },
2489 deleteDeployment: (id: string) => {
2490 calls.push(["deleteDeployment", id]);
2491 return Effect.void;
2492 },
2493 } as unknown as PrismaManagementClient;
2494 const http = HttpClient.make((request) =>
2495 Effect.succeed(
2496 HttpClientResponse.fromWeb(
2497 request,
2498 new Response("upload failed", { status: 500 }),
2499 ),
2500 ),
2501 );
2502
2503 return Effect.gen(function* () {
2504 const provider = yield* Compute.Provider;
2505 const error = yield* provider
2506 .reconcile({
2507 id: "App",
2508 fqn: "App",
2509 instanceId: "00000000000000000000000000000000",
2510 news: {
2511 project: "project-1",
2512 appName: "api",
2513 artifactPath: fixtureArtifactPath,
2514 branchId: "branch-main",
2515 start: false,
2516 skipPromote: true,
2517 },
2518 olds: undefined,
2519 output: {
2520 appId: "service-1",
2521 deploymentId: undefined,
2522 projectId: "project-1",
2523 appName: "api",
2524 regionId: "us-east-1",
2525 deploymentEndpointDomain: undefined,
2526 deploymentUrl: undefined,
2527 appEndpointDomain: "api.prisma.build",
2528 url: "https://api.prisma.build",
2529 promoted: false,
2530 previousDeploymentId: undefined,
2531 previousDeploymentAction: undefined,
2532 artifactHash: undefined,
2533 local: false,
2534 },
2535 session: undefined as never,
2536 bindings: [],
2537 })
2538 .pipe(Effect.flip);
2539
2540 expect(error).toBeInstanceOf(Error);
2541 expect(error.message).toContain("artifact upload failed");
2542 expect(calls).toContainEqual(["deleteDeployment", "version-1"]);
2543 }).pipe(
2544 Effect.provide(computeProviderLive()),
2545 Effect.provide(
2546 Layer.succeed(PrismaClient, withDefaultBranch(client)),
2547 ),
2548 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
2549 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
2550 Effect.provide(PlatformServices),
2551 );
2552 },
2553 { tags: ["provider:prisma:branch"] },
2554 );
2555
2556 it.effect(
2557 "deletes created deployment when start fails",
2558 () => {
2559 const calls: Array<[string, unknown?]> = [];
2560 const client = {
2561 getApp: (id: string) => {
2562 calls.push(["getApp", id]);
2563 return Effect.succeed({
2564 id,
2565 type: "app" as const,
2566 url: `https://api.prisma.test/v1/services/${id}`,
2567 name: "api",
2568 region: { id: "us-east-1", name: "US East" },
2569 projectId: "project-1",
2570 branchId: "branch-main",
2571 latestDeploymentId: "version-old",
2572 appEndpointDomain: "api.prisma.build",
2573 createdAt: "2026-01-01T00:00:00Z",
2574 });
2575 },
2576 createAppDeployment: (appId: string, input: unknown) => {
2577 calls.push(["createAppDeployment", { appId, input }]);
2578 return Effect.succeed({
2579 id: "version-1",
2580 type: "deployment" as const,
2581 url: "https://api.prisma.test/v1/deployments/version-1",
2582 foundryVersionId: "foundry-1",
2583 uploadUrl: "https://upload.prisma.test/app.tar.gz",
2584 });
2585 },
2586 getDeployment: (id: string) => {
2587 calls.push(["getDeployment", id]);
2588 return Effect.succeed({
2589 id,
2590 type: "deployment" as const,
2591 serviceId: "service-1",
2592 url: `https://api.prisma.test/v1/deployments/${id}`,
2593 foundryVersionId: "foundry-1",
2594 status: "new",
2595 previewDomain: null,
2596 createdAt: "2026-01-01T00:00:00Z",
2597 });
2598 },
2599 startDeployment: (id: string) => {
2600 calls.push(["startDeployment", id]);
2601 return Effect.fail(
2602 new PrismaApiError({
2603 method: "POST",
2604 path: `/v1/deployments/${id}/start`,
2605 status: 400,
2606 message: "start failed",
2607 }),
2608 );
2609 },
2610 deleteDeployment: (id: string) => {
2611 calls.push(["deleteDeployment", id]);
2612 return Effect.void;
2613 },
2614 } as unknown as PrismaManagementClient;
2615 const http = HttpClient.make((request) =>
2616 readHttpBodyBytes(request.body as HttpBody.HttpBody).pipe(
2617 Effect.as(HttpClientResponse.fromWeb(request, new Response(null))),
2618 ),
2619 );
2620
2621 return Effect.gen(function* () {
2622 const provider = yield* Compute.Provider;
2623 const error = yield* provider
2624 .reconcile({
2625 id: "App",
2626 fqn: "App",
2627 instanceId: "00000000000000000000000000000000",
2628 news: {
2629 project: "project-1",
2630 appName: "api",
2631 artifactPath: fixtureArtifactPath,
2632 branchId: "branch-main",
2633 },
2634 olds: undefined,
2635 output: {
2636 appId: "service-1",
2637 deploymentId: undefined,
2638 projectId: "project-1",
2639 appName: "api",
2640 regionId: "us-east-1",
2641 deploymentEndpointDomain: undefined,
2642 deploymentUrl: undefined,
2643 appEndpointDomain: "api.prisma.build",
2644 url: "https://api.prisma.build",
2645 promoted: false,
2646 previousDeploymentId: undefined,
2647 previousDeploymentAction: undefined,
2648 artifactHash: undefined,
2649 local: false,
2650 },
2651 session: undefined as never,
2652 bindings: [],
2653 })
2654 .pipe(Effect.flip);
2655
2656 // Over the wire the injected failure decodes into the typed error.
2657 expect(error._tag).toBe("BadRequest");
2658 expect(error.message).toBe("start failed");
2659 expect(calls).toContainEqual(["deleteDeployment", "version-1"]);
2660 }).pipe(
2661 Effect.provide(computeProviderLive()),
2662 Effect.provide(
2663 Layer.succeed(PrismaClient, withDefaultBranch(client)),
2664 ),
2665 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
2666 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
2667 Effect.provide(PlatformServices),
2668 );
2669 },
2670 { tags: ["provider:prisma:branch"] },
2671 );
2672
2673 it.live(
2674 "blocks promotion and deletes a new deployment when preview health fails",
2675 () => {
2676 const fixture = makeHealthLifecycleFixture({ previewStatus: 503 });
2677
2678 return Effect.gen(function* () {
2679 const provider = yield* Compute.Provider;
2680 const error = yield* provider
2681 .reconcile({
2682 id: "App",
2683 fqn: "App",
2684 instanceId: "00000000000000000000000000000000",
2685 news: {
2686 project: "project-1",
2687 appName: "api",
2688 artifactPath: fixtureArtifactV2Path,
2689 branchId: "branch-main",
2690 healthCheck: { path: "/health" },
2691 pollIntervalMs: 1,
2692 timeoutSeconds: 0.05,
2693 urlReadinessTimeoutSeconds: 0.02,
2694 },
2695 olds: {
2696 project: "project-1",
2697 appName: "api",
2698 artifactPath: fixtureArtifactV1Path,
2699 branchId: "branch-main",
2700 },
2701 output: {
2702 appId: "service-1",
2703 deploymentId: "version-old",
2704 projectId: "project-1",
2705 appName: "api",
2706 regionId: "us-east-1",
2707 deploymentEndpointDomain: "version-old.preview.prisma.build",
2708 deploymentUrl: "https://version-old.preview.prisma.build",
2709 appEndpointDomain: "api.prisma.build",
2710 url: "https://api.prisma.build",
2711 promoted: true,
2712 previousDeploymentId: undefined,
2713 previousDeploymentAction: undefined,
2714 artifactHash: Redacted.make("old-hash"),
2715 local: false,
2716 },
2717 session: undefined as never,
2718 bindings: [],
2719 })
2720 .pipe(Effect.flip);
2721
2722 expect(error.message).toContain(
2723 "https://version-new.preview.prisma.build/health",
2724 );
2725 expect(error.message).toContain("HTTP 503");
2726 expect(
2727 fixture.calls.filter(([operation]) => operation === "promoteApp"),
2728 ).toEqual([]);
2729 expect(fixture.calls).toContainEqual([
2730 "stopDeployment",
2731 "version-new",
2732 ]);
2733 expect(fixture.calls).toContainEqual([
2734 "deleteDeployment",
2735 "version-new",
2736 ]);
2737 expect(fixture.hasDeployment("version-new")).toBe(false);
2738 expect(fixture.hasDeployment("version-old")).toBe(true);
2739 expect(fixture.latestDeploymentId()).toBe("version-old");
2740 }).pipe(
2741 Effect.provide(computeProviderLive()),
2742 Effect.provide(
2743 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
2744 ),
2745 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
2746 Effect.provideService(HttpClient.HttpClient, fixture.http),
2747 Effect.provide(PlatformServices),
2748 );
2749 },
2750 { tags: ["provider:prisma:branch"] },
2751 );
2752
2753 it.live(
2754 "rolls back promotion and deletes the new deployment when stable health fails",
2755 () => {
2756 const fixture = makeHealthLifecycleFixture({
2757 previewStatus: 204,
2758 stableStatus: 503,
2759 });
2760
2761 return Effect.gen(function* () {
2762 const provider = yield* Compute.Provider;
2763 const error = yield* provider
2764 .reconcile({
2765 id: "App",
2766 fqn: "App",
2767 instanceId: "00000000000000000000000000000000",
2768 news: {
2769 project: "project-1",
2770 appName: "api",
2771 artifactPath: fixtureArtifactV2Path,
2772 branchId: "branch-main",
2773 healthCheck: { path: "/health" },
2774 pollIntervalMs: 1,
2775 timeoutSeconds: 0.05,
2776 urlReadinessTimeoutSeconds: 0.02,
2777 },
2778 olds: {
2779 project: "project-1",
2780 appName: "api",
2781 artifactPath: fixtureArtifactV1Path,
2782 branchId: "branch-main",
2783 },
2784 output: {
2785 appId: "service-1",
2786 deploymentId: "version-old",
2787 projectId: "project-1",
2788 appName: "api",
2789 regionId: "us-east-1",
2790 deploymentEndpointDomain: "version-old.preview.prisma.build",
2791 deploymentUrl: "https://version-old.preview.prisma.build",
2792 appEndpointDomain: "api.prisma.build",
2793 url: "https://api.prisma.build",
2794 promoted: true,
2795 previousDeploymentId: undefined,
2796 previousDeploymentAction: undefined,
2797 artifactHash: Redacted.make("old-hash"),
2798 local: false,
2799 },
2800 session: undefined as never,
2801 bindings: [],
2802 })
2803 .pipe(Effect.flip);
2804
2805 expect(error.message).toContain("https://api.prisma.build/health");
2806 expect(error.message).toContain("HTTP 503");
2807 expect(fixture.calls).toContainEqual([
2808 "promoteApp",
2809 { appId: "service-1", deploymentId: "version-new" },
2810 ]);
2811 expect(fixture.calls).toContainEqual([
2812 "rollbackApp",
2813 { appId: "service-1", deploymentId: "version-old" },
2814 ]);
2815 expect(fixture.calls).toContainEqual([
2816 "deleteDeployment",
2817 "version-new",
2818 ]);
2819 const promotionIndex = fixture.calls.findIndex(
2820 ([operation]) => operation === "promoteApp",
2821 );
2822 const rollbackIndex = fixture.calls.findIndex(
2823 ([operation]) => operation === "rollbackApp",
2824 );
2825 const deletionIndex = fixture.calls.findIndex(
2826 ([operation]) => operation === "deleteDeployment",
2827 );
2828 expect(promotionIndex).toBeLessThan(rollbackIndex);
2829 expect(rollbackIndex).toBeLessThan(deletionIndex);
2830 expect(fixture.hasDeployment("version-new")).toBe(false);
2831 expect(fixture.hasDeployment("version-old")).toBe(true);
2832 expect(fixture.latestDeploymentId()).toBe("version-old");
2833 }).pipe(
2834 Effect.provide(computeProviderLive()),
2835 Effect.provide(
2836 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
2837 ),
2838 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
2839 Effect.provideService(HttpClient.HttpClient, fixture.http),
2840 Effect.provide(PlatformServices),
2841 );
2842 },
2843 { tags: ["provider:prisma:branch"] },
2844 );
2845
2846 it.live(
2847 "does not probe stable health after a successful promotion response that does not converge",
2848 () => {
2849 const fixture = makeHealthLifecycleFixture({
2850 previewStatus: 204,
2851 stableStatus: 204,
2852 promoteUpdatesLatest: false,
2853 });
2854
2855 return Effect.gen(function* () {
2856 const provider = yield* Compute.Provider;
2857 const error = yield* provider
2858 .reconcile({
2859 id: "App",
2860 fqn: "App",
2861 instanceId: "00000000000000000000000000000000",
2862 news: {
2863 project: "project-1",
2864 appName: "api",
2865 artifactPath: fixtureArtifactV2Path,
2866 branchId: "branch-main",
2867 healthCheck: { path: "/health" },
2868 pollIntervalMs: 1,
2869 timeoutSeconds: 0.02,
2870 urlReadinessTimeoutSeconds: 0.02,
2871 },
2872 olds: {
2873 project: "project-1",
2874 appName: "api",
2875 artifactPath: fixtureArtifactV1Path,
2876 branchId: "branch-main",
2877 },
2878 output: {
2879 appId: "service-1",
2880 deploymentId: "version-old",
2881 projectId: "project-1",
2882 appName: "api",
2883 regionId: "us-east-1",
2884 deploymentEndpointDomain: "version-old.preview.prisma.build",
2885 deploymentUrl: "https://version-old.preview.prisma.build",
2886 appEndpointDomain: "api.prisma.build",
2887 url: "https://api.prisma.build",
2888 promoted: true,
2889 previousDeploymentId: undefined,
2890 previousDeploymentAction: undefined,
2891 artifactHash: Redacted.make("old-hash"),
2892 local: false,
2893 },
2894 session: undefined as never,
2895 bindings: [],
2896 })
2897 .pipe(Effect.flip);
2898
2899 expect(error).toBeInstanceOf(AggregateError);
2900 expect((error as AggregateError).message).toContain(
2901 "promotion returned success",
2902 );
2903 expect((error as AggregateError).message).toContain(
2904 "did not converge",
2905 );
2906 expect(fixture.latestDeploymentId()).toBe("version-old");
2907 expect(fixture.hasDeployment("version-new")).toBe(true);
2908 expect(
2909 fixture.calls.filter(
2910 ([operation, value]) =>
2911 operation === "healthRequest" &&
2912 value === "https://api.prisma.build/health",
2913 ),
2914 ).toEqual([]);
2915 expect(
2916 fixture.calls.filter(
2917 ([operation]) => operation === "deleteDeployment",
2918 ),
2919 ).toEqual([]);
2920 }).pipe(
2921 Effect.provide(computeProviderLive()),
2922 Effect.provide(
2923 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
2924 ),
2925 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
2926 Effect.provideService(HttpClient.HttpClient, fixture.http),
2927 Effect.provide(PlatformServices),
2928 );
2929 },
2930 { tags: ["provider:prisma:branch"] },
2931 );
2932
2933 it.live(
2934 "does not delete a promoted deployment after a successful rollback response that does not converge",
2935 () => {
2936 const fixture = makeHealthLifecycleFixture({
2937 previewStatus: 204,
2938 stableStatus: 503,
2939 rollbackUpdatesLatest: false,
2940 });
2941
2942 return Effect.gen(function* () {
2943 const provider = yield* Compute.Provider;
2944 const error = yield* provider
2945 .reconcile({
2946 id: "App",
2947 fqn: "App",
2948 instanceId: "00000000000000000000000000000000",
2949 news: {
2950 project: "project-1",
2951 appName: "api",
2952 artifactPath: fixtureArtifactV2Path,
2953 branchId: "branch-main",
2954 healthCheck: { path: "/health" },
2955 pollIntervalMs: 1,
2956 timeoutSeconds: 0.02,
2957 urlReadinessTimeoutSeconds: 0.02,
2958 },
2959 olds: {
2960 project: "project-1",
2961 appName: "api",
2962 artifactPath: fixtureArtifactV1Path,
2963 branchId: "branch-main",
2964 },
2965 output: {
2966 appId: "service-1",
2967 deploymentId: "version-old",
2968 projectId: "project-1",
2969 appName: "api",
2970 regionId: "us-east-1",
2971 deploymentEndpointDomain: "version-old.preview.prisma.build",
2972 deploymentUrl: "https://version-old.preview.prisma.build",
2973 appEndpointDomain: "api.prisma.build",
2974 url: "https://api.prisma.build",
2975 promoted: true,
2976 previousDeploymentId: undefined,
2977 previousDeploymentAction: undefined,
2978 artifactHash: Redacted.make("old-hash"),
2979 local: false,
2980 },
2981 session: undefined as never,
2982 bindings: [],
2983 })
2984 .pipe(Effect.flip);
2985
2986 expect(error).toBeInstanceOf(AggregateError);
2987 expect((error as AggregateError).message).toContain(
2988 "rollback to deployment 'version-old' did not converge",
2989 );
2990 expect(fixture.latestDeploymentId()).toBe("version-new");
2991 expect(fixture.hasDeployment("version-new")).toBe(true);
2992 expect(fixture.hasDeployment("version-old")).toBe(true);
2993 expect(
2994 fixture.calls.filter(
2995 ([operation]) => operation === "deleteDeployment",
2996 ),
2997 ).toEqual([]);
2998 }).pipe(
2999 Effect.provide(computeProviderLive()),
3000 Effect.provide(
3001 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
3002 ),
3003 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
3004 Effect.provideService(HttpClient.HttpClient, fixture.http),
3005 Effect.provide(PlatformServices),
3006 );
3007 },
3008 { tags: ["provider:prisma:branch"] },
3009 );
3010
3011 it.live(
3012 "replaces a hash-matching terminal failed deployment before cleaning it",
3013 () => {
3014 const fixture = makeHealthLifecycleFixture({
3015 latestDeploymentId: null,
3016 previewStatus: 204,
3017 stableStatus: 204,
3018 });
3019 const news = {
3020 project: "project-1",
3021 appName: "api",
3022 artifactPath: fixtureArtifactV1Path,
3023 branchId: "branch-main",
3024 healthCheck: { path: "/health" },
3025 pollIntervalMs: 1,
3026 timeoutSeconds: 0.02,
3027 urlReadinessTimeoutSeconds: 0.02,
3028 } as const;
3029
3030 return Effect.gen(function* () {
3031 const provider = yield* Compute.Provider;
3032 const first = yield* provider.reconcile({
3033 id: "App",
3034 fqn: "App",
3035 instanceId: "00000000000000000000000000000000",
3036 news,
3037 olds: undefined,
3038 output: {
3039 appId: "service-1",
3040 deploymentId: undefined,
3041 projectId: "project-1",
3042 appName: "api",
3043 regionId: "us-east-1",
3044 deploymentEndpointDomain: undefined,
3045 deploymentUrl: undefined,
3046 appEndpointDomain: "api.prisma.build",
3047 url: undefined,
3048 promoted: false,
3049 previousDeploymentId: undefined,
3050 previousDeploymentAction: undefined,
3051 artifactHash: undefined,
3052 local: false,
3053 },
3054 session: undefined as never,
3055 bindings: [],
3056 });
3057
3058 fixture.setDeploymentStatus("version-new", "failed");
3059 fixture.calls.splice(0);
3060 const recovered = yield* provider.reconcile({
3061 id: "App",
3062 fqn: "App",
3063 instanceId: "00000000000000000000000000000000",
3064 news,
3065 olds: news,
3066 output: first,
3067 session: undefined as never,
3068 bindings: [],
3069 });
3070
3071 const replacementCreationIndex = fixture.calls.findIndex(
3072 ([operation]) => operation === "createAppDeployment",
3073 );
3074 const stableHealthIndex = fixture.calls.findIndex(
3075 ([operation, value]) =>
3076 operation === "healthRequest" &&
3077 value === "https://api.prisma.build/health",
3078 );
3079 const failedDeletionIndex = fixture.calls.findIndex(
3080 ([operation, value]) =>
3081 operation === "deleteDeployment" && value === "version-new",
3082 );
3083
3084 expect(recovered.deploymentId).toBe("version-new-2");
3085 expect(recovered.promoted).toBe(true);
3086 expect(recovered.readinessStatus).toBe("ready");
3087 expect(recovered.previousDeploymentId).toBe("version-new");
3088 expect(recovered.previousDeploymentAction).toBe("destroyed");
3089 expect(replacementCreationIndex).toBeGreaterThanOrEqual(0);
3090 expect(stableHealthIndex).toBeGreaterThan(replacementCreationIndex);
3091 expect(failedDeletionIndex).toBeGreaterThan(stableHealthIndex);
3092 expect(fixture.hasDeployment("version-new")).toBe(false);
3093 expect(fixture.hasDeployment("version-new-2")).toBe(true);
3094 expect(
3095 fixture.calls.filter(
3096 ([operation, value]) =>
3097 operation === "startDeployment" && value === "version-new",
3098 ),
3099 ).toEqual([]);
3100 }).pipe(
3101 Effect.provide(computeProviderLive()),
3102 Effect.provide(
3103 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
3104 ),
3105 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
3106 Effect.provideService(HttpClient.HttpClient, fixture.http),
3107 Effect.provide(PlatformServices),
3108 );
3109 },
3110 { tags: ["provider:prisma:branch"] },
3111 );
3112
3113 it.live(
3114 "destroys a changed-hash terminal failed deployment only after replacement convergence",
3115 () => {
3116 const fixture = makeHealthLifecycleFixture({
3117 previewStatus: 204,
3118 stableStatus: 204,
3119 });
3120 fixture.setDeploymentStatus("version-old", "failed");
3121
3122 return Effect.gen(function* () {
3123 const provider = yield* Compute.Provider;
3124 const output = yield* provider.reconcile({
3125 id: "App",
3126 fqn: "App",
3127 instanceId: "00000000000000000000000000000000",
3128 news: {
3129 project: "project-1",
3130 appName: "api",
3131 artifactPath: fixtureArtifactV2Path,
3132 branchId: "branch-main",
3133 healthCheck: { path: "/health" },
3134 pollIntervalMs: 1,
3135 timeoutSeconds: 0.02,
3136 urlReadinessTimeoutSeconds: 0.02,
3137 },
3138 olds: {
3139 project: "project-1",
3140 appName: "api",
3141 artifactPath: fixtureArtifactV1Path,
3142 branchId: "branch-main",
3143 },
3144 output: {
3145 appId: "service-1",
3146 deploymentId: "version-old",
3147 projectId: "project-1",
3148 appName: "api",
3149 regionId: "us-east-1",
3150 deploymentEndpointDomain: "version-old.preview.prisma.build",
3151 deploymentUrl: "https://version-old.preview.prisma.build",
3152 appEndpointDomain: "api.prisma.build",
3153 url: "https://api.prisma.build",
3154 promoted: true,
3155 previousDeploymentId: undefined,
3156 previousDeploymentAction: undefined,
3157 artifactHash: Redacted.make("old-hash"),
3158 local: false,
3159 },
3160 session: undefined as never,
3161 bindings: [],
3162 });
3163
3164 const stableHealthIndex = fixture.calls.findIndex(
3165 ([operation, value]) =>
3166 operation === "healthRequest" &&
3167 value === "https://api.prisma.build/health",
3168 );
3169 const failedDeletionIndex = fixture.calls.findIndex(
3170 ([operation, value]) =>
3171 operation === "deleteDeployment" && value === "version-old",
3172 );
3173
3174 expect(output.deploymentId).toBe("version-new");
3175 expect(output.previousDeploymentId).toBe("version-old");
3176 expect(output.previousDeploymentAction).toBe("destroyed");
3177 expect(stableHealthIndex).toBeGreaterThanOrEqual(0);
3178 expect(failedDeletionIndex).toBeGreaterThan(stableHealthIndex);
3179 expect(fixture.hasDeployment("version-old")).toBe(false);
3180 expect(fixture.hasDeployment("version-new")).toBe(true);
3181 expect(
3182 fixture.calls.filter(
3183 ([operation, value]) =>
3184 operation === "stopDeployment" && value === "version-old",
3185 ),
3186 ).toEqual([]);
3187 }).pipe(
3188 Effect.provide(computeProviderLive()),
3189 Effect.provide(
3190 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
3191 ),
3192 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
3193 Effect.provideService(HttpClient.HttpClient, fixture.http),
3194 Effect.provide(PlatformServices),
3195 );
3196 },
3197 { tags: ["provider:prisma:branch"] },
3198 );
3199
3200 it.live(
3201 "never rolls back to a changed-hash terminal failed deployment and blocks duplicate retry creation",
3202 () => {
3203 const fixture = makeHealthLifecycleFixture({
3204 previewStatus: 204,
3205 stableStatus: 503,
3206 });
3207 fixture.setDeploymentStatus("version-old", "failed");
3208 const news = {
3209 project: "project-1",
3210 appName: "api",
3211 artifactPath: fixtureArtifactV2Path,
3212 branchId: "branch-main",
3213 healthCheck: { path: "/health" },
3214 pollIntervalMs: 1,
3215 timeoutSeconds: 0.02,
3216 urlReadinessTimeoutSeconds: 0.02,
3217 } as const;
3218 const olds = {
3219 project: "project-1",
3220 appName: "api",
3221 artifactPath: fixtureArtifactV1Path,
3222 branchId: "branch-main",
3223 } as const;
3224 const output = {
3225 appId: "service-1",
3226 deploymentId: "version-old",
3227 projectId: "project-1",
3228 appName: "api",
3229 regionId: "us-east-1",
3230 deploymentEndpointDomain: "version-old.preview.prisma.build",
3231 deploymentUrl: "https://version-old.preview.prisma.build",
3232 appEndpointDomain: "api.prisma.build",
3233 url: "https://api.prisma.build",
3234 promoted: true,
3235 previousDeploymentId: undefined,
3236 previousDeploymentAction: undefined,
3237 artifactHash: Redacted.make("old-hash"),
3238 local: false,
3239 } as const;
3240
3241 return Effect.gen(function* () {
3242 const provider = yield* Compute.Provider;
3243 const firstError = yield* provider
3244 .reconcile({
3245 id: "App",
3246 fqn: "App",
3247 instanceId: "00000000000000000000000000000000",
3248 news,
3249 olds,
3250 output,
3251 session: undefined as never,
3252 bindings: [],
3253 })
3254 .pipe(Effect.flip);
3255
3256 expect(firstError).toBeInstanceOf(AggregateError);
3257 expect((firstError as AggregateError).message).toContain(
3258 "no safe rollback target exists",
3259 );
3260 expect(
3261 fixture.calls.filter(([operation]) => operation === "rollbackApp"),
3262 ).toEqual([]);
3263 expect(fixture.latestDeploymentId()).toBe("version-new");
3264 expect(fixture.hasDeployment("version-old")).toBe(true);
3265 expect(fixture.hasDeployment("version-new")).toBe(true);
3266
3267 fixture.setStableStatus(204);
3268 const callsBeforeRetry = fixture.calls.length;
3269 const retryError = yield* provider
3270 .reconcile({
3271 id: "App",
3272 fqn: "App",
3273 instanceId: "00000000000000000000000000000000",
3274 news,
3275 olds,
3276 output,
3277 session: undefined as never,
3278 bindings: [],
3279 })
3280 .pipe(Effect.flip);
3281 const retryCalls = fixture.calls.slice(callsBeforeRetry);
3282
3283 expect(retryError).toBeInstanceOf(AggregateError);
3284 expect((retryError as AggregateError).message).toContain(
3285 "cannot prove that the live deployment is the interrupted replacement",
3286 );
3287 expect(
3288 retryCalls.filter(
3289 ([operation]) => operation === "createAppDeployment",
3290 ),
3291 ).toEqual([]);
3292 expect(
3293 retryCalls.filter(([operation]) => operation === "rollbackApp"),
3294 ).toEqual([]);
3295 expect(
3296 retryCalls.filter(
3297 ([operation]) => operation === "deleteDeployment",
3298 ),
3299 ).toEqual([]);
3300 expect(fixture.latestDeploymentId()).toBe("version-new");
3301 expect(fixture.hasDeployment("version-old")).toBe(true);
3302 expect(fixture.hasDeployment("version-new")).toBe(true);
3303 }).pipe(
3304 Effect.provide(computeProviderLive()),
3305 Effect.provide(
3306 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
3307 ),
3308 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
3309 Effect.provideService(HttpClient.HttpClient, fixture.http),
3310 Effect.provide(PlatformServices),
3311 );
3312 },
3313 { tags: ["provider:prisma:branch"] },
3314 );
3315
3316 it.live(
3317 "does not create another generation after failed terminal replacement loses a safe rollback target",
3318 () => {
3319 const fixture = makeHealthLifecycleFixture({
3320 latestDeploymentId: null,
3321 previewStatus: 204,
3322 stableStatus: 204,
3323 });
3324 const news = {
3325 project: "project-1",
3326 appName: "api",
3327 artifactPath: fixtureArtifactV1Path,
3328 branchId: "branch-main",
3329 healthCheck: { path: "/health" },
3330 pollIntervalMs: 1,
3331 timeoutSeconds: 0.02,
3332 urlReadinessTimeoutSeconds: 0.02,
3333 } as const;
3334
3335 return Effect.gen(function* () {
3336 const provider = yield* Compute.Provider;
3337 const first = yield* provider.reconcile({
3338 id: "App",
3339 fqn: "App",
3340 instanceId: "00000000000000000000000000000000",
3341 news,
3342 olds: undefined,
3343 output: {
3344 appId: "service-1",
3345 deploymentId: undefined,
3346 projectId: "project-1",
3347 appName: "api",
3348 regionId: "us-east-1",
3349 deploymentEndpointDomain: undefined,
3350 deploymentUrl: undefined,
3351 appEndpointDomain: "api.prisma.build",
3352 url: undefined,
3353 promoted: false,
3354 previousDeploymentId: undefined,
3355 previousDeploymentAction: undefined,
3356 artifactHash: undefined,
3357 local: false,
3358 },
3359 session: undefined as never,
3360 bindings: [],
3361 });
3362
3363 fixture.setDeploymentStatus("version-new", "failed");
3364 fixture.setStableStatus(503);
3365 fixture.calls.splice(0);
3366 const replacementError = yield* provider
3367 .reconcile({
3368 id: "App",
3369 fqn: "App",
3370 instanceId: "00000000000000000000000000000000",
3371 news,
3372 olds: news,
3373 output: first,
3374 session: undefined as never,
3375 bindings: [],
3376 })
3377 .pipe(Effect.flip);
3378
3379 expect(replacementError).toBeInstanceOf(AggregateError);
3380 expect((replacementError as AggregateError).message).toContain(
3381 "no safe rollback target exists",
3382 );
3383 expect(fixture.latestDeploymentId()).toBe("version-new-2");
3384 expect(fixture.hasDeployment("version-new")).toBe(true);
3385 expect(fixture.hasDeployment("version-new-2")).toBe(true);
3386 expect(
3387 fixture.calls.filter(
3388 ([operation]) => operation === "deleteDeployment",
3389 ),
3390 ).toEqual([]);
3391
3392 fixture.setStableStatus(204);
3393 const callsBeforeRetry = fixture.calls.length;
3394 const retryError = yield* provider
3395 .reconcile({
3396 id: "App",
3397 fqn: "App",
3398 instanceId: "00000000000000000000000000000000",
3399 news,
3400 olds: news,
3401 output: first,
3402 session: undefined as never,
3403 bindings: [],
3404 })
3405 .pipe(Effect.flip);
3406 const retryCalls = fixture.calls.slice(callsBeforeRetry);
3407
3408 expect(retryError).toBeInstanceOf(AggregateError);
3409 expect((retryError as AggregateError).message).toContain(
3410 "cannot prove that the live deployment is the interrupted replacement",
3411 );
3412 expect(
3413 retryCalls.filter(
3414 ([operation]) => operation === "createAppDeployment",
3415 ),
3416 ).toEqual([]);
3417 expect(
3418 retryCalls.filter(
3419 ([operation]) => operation === "deleteDeployment",
3420 ),
3421 ).toEqual([]);
3422 expect(fixture.latestDeploymentId()).toBe("version-new-2");
3423 expect(fixture.hasDeployment("version-new")).toBe(true);
3424 expect(fixture.hasDeployment("version-new-2")).toBe(true);
3425 }).pipe(
3426 Effect.provide(computeProviderLive()),
3427 Effect.provide(
3428 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
3429 ),
3430 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
3431 Effect.provideService(HttpClient.HttpClient, fixture.http),
3432 Effect.provide(PlatformServices),
3433 );
3434 },
3435 { tags: ["provider:prisma:branch"] },
3436 );
3437
3438 it.live(
3439 "fails closed on rollback failure and recovers from persisted state before retrying",
3440 () => {
3441 const fixture = makeHealthLifecycleFixture({
3442 previewStatus: 204,
3443 stableStatus: 503,
3444 rollbackFailures: 2,
3445 });
3446 const news = {
3447 project: "project-1",
3448 appName: "api",
3449 artifactPath: fixtureArtifactV2Path,
3450 branchId: "branch-main",
3451 healthCheck: { path: "/health" },
3452 pollIntervalMs: 1,
3453 timeoutSeconds: 0.05,
3454 urlReadinessTimeoutSeconds: 0.02,
3455 } as const;
3456 const olds = {
3457 project: "project-1",
3458 appName: "api",
3459 artifactPath: fixtureArtifactV1Path,
3460 branchId: "branch-main",
3461 } as const;
3462 const output = {
3463 appId: "service-1",
3464 deploymentId: "version-old",
3465 projectId: "project-1",
3466 appName: "api",
3467 regionId: "us-east-1",
3468 deploymentEndpointDomain: "version-old.preview.prisma.build",
3469 deploymentUrl: "https://version-old.preview.prisma.build",
3470 appEndpointDomain: "api.prisma.build",
3471 url: "https://api.prisma.build",
3472 promoted: true,
3473 previousDeploymentId: undefined,
3474 previousDeploymentAction: undefined,
3475 artifactHash: Redacted.make("old-hash"),
3476 local: false,
3477 } as const;
3478 const reconcile = Effect.gen(function* () {
3479 const provider = yield* Compute.Provider;
3480 return yield* provider.reconcile({
3481 id: "App",
3482 fqn: "App",
3483 instanceId: "00000000000000000000000000000000",
3484 news,
3485 olds,
3486 output,
3487 session: undefined as never,
3488 bindings: [],
3489 });
3490 });
3491
3492 return Effect.gen(function* () {
3493 const firstError = yield* reconcile.pipe(Effect.flip);
3494
3495 expect(firstError).toBeInstanceOf(AggregateError);
3496 expect((firstError as AggregateError).message).toContain(
3497 "promoted deployment 'version-new'",
3498 );
3499 expect((firstError as AggregateError).message).toContain(
3500 "rollback to deployment 'version-old' did not converge",
3501 );
3502 expect((firstError as AggregateError).message).toContain(
3503 "next reconcile will retry recovery",
3504 );
3505 expect(fixture.latestDeploymentId()).toBe("version-new");
3506 expect(fixture.hasDeployment("version-new")).toBe(true);
3507 expect(fixture.hasDeployment("version-old")).toBe(true);
3508 expect(
3509 fixture.calls.filter(
3510 ([operation]) => operation === "deleteDeployment",
3511 ),
3512 ).toEqual([]);
3513
3514 fixture.setStableStatus(204);
3515 const callsBeforeBlockedRetry = fixture.calls.length;
3516 const retryError = yield* reconcile.pipe(Effect.flip);
3517 const blockedRetryCalls = fixture.calls.slice(
3518 callsBeforeBlockedRetry,
3519 );
3520
3521 expect(retryError).toBeInstanceOf(AggregateError);
3522 expect((retryError as AggregateError).message).toContain(
3523 "no environment variables or new deployment were changed",
3524 );
3525 expect(
3526 (retryError as AggregateError).errors.some(
3527 (error) =>
3528 error instanceof Error &&
3529 error.message === "rollback unavailable",
3530 ),
3531 ).toBe(true);
3532 expect(
3533 blockedRetryCalls.filter(
3534 ([operation]) => operation === "createAppDeployment",
3535 ),
3536 ).toEqual([]);
3537 expect(
3538 fixture.calls.filter(
3539 ([operation]) => operation === "createAppDeployment",
3540 ),
3541 ).toHaveLength(1);
3542 expect(fixture.latestDeploymentId()).toBe("version-new");
3543 expect(fixture.hasDeployment("version-new")).toBe(true);
3544
3545 const callsBeforeRecovery = fixture.calls.length;
3546 const recovered = yield* reconcile;
3547 const recoveryCalls = fixture.calls.slice(callsBeforeRecovery);
3548 const rollbackIndex = recoveryCalls.findIndex(
3549 ([operation]) => operation === "rollbackApp",
3550 );
3551 const failedDeletionIndex = recoveryCalls.findIndex(
3552 ([operation, value]) =>
3553 operation === "deleteDeployment" && value === "version-new",
3554 );
3555 const replacementCreationIndex = recoveryCalls.findIndex(
3556 ([operation]) => operation === "createAppDeployment",
3557 );
3558
3559 expect(recovered.deploymentId).toBe("version-new-2");
3560 expect(recovered.promoted).toBe(true);
3561 expect(recovered.readinessStatus).toBe("ready");
3562 expect(rollbackIndex).toBeGreaterThanOrEqual(0);
3563 expect(rollbackIndex).toBeLessThan(failedDeletionIndex);
3564 expect(failedDeletionIndex).toBeLessThan(replacementCreationIndex);
3565 expect(fixture.hasDeployment("version-new")).toBe(false);
3566 expect(fixture.hasDeployment("version-new-2")).toBe(true);
3567 expect(fixture.latestDeploymentId()).toBe("version-new-2");
3568 expect(
3569 fixture.calls.filter(
3570 ([operation]) => operation === "createAppDeployment",
3571 ),
3572 ).toHaveLength(2);
3573 }).pipe(
3574 Effect.provide(computeProviderLive()),
3575 Effect.provide(
3576 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
3577 ),
3578 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
3579 Effect.provideService(HttpClient.HttpClient, fixture.http),
3580 Effect.provide(PlatformServices),
3581 );
3582 },
3583 { tags: ["provider:prisma:branch"] },
3584 );
3585
3586 it.effect(
3587 "honors custom accepted health statuses before and after promotion",
3588 () => {
3589 const fixture = makeHealthLifecycleFixture({
3590 latestDeploymentId: null,
3591 previewStatus: 302,
3592 stableStatus: 302,
3593 });
3594
3595 return Effect.gen(function* () {
3596 const provider = yield* Compute.Provider;
3597 const output = yield* provider.reconcile({
3598 id: "App",
3599 fqn: "App",
3600 instanceId: "00000000000000000000000000000000",
3601 news: {
3602 project: "project-1",
3603 appName: "api",
3604 artifactPath: fixtureArtifactV2Path,
3605 branchId: "branch-main",
3606 healthCheck: { path: "/ready", statusCodes: [302] },
3607 pollIntervalMs: 1,
3608 timeoutSeconds: 0.05,
3609 urlReadinessTimeoutSeconds: 0.02,
3610 },
3611 olds: undefined,
3612 output: {
3613 appId: "service-1",
3614 deploymentId: undefined,
3615 projectId: "project-1",
3616 appName: "api",
3617 regionId: "us-east-1",
3618 deploymentEndpointDomain: undefined,
3619 deploymentUrl: undefined,
3620 appEndpointDomain: "api.prisma.build",
3621 url: undefined,
3622 promoted: false,
3623 previousDeploymentId: undefined,
3624 previousDeploymentAction: undefined,
3625 artifactHash: undefined,
3626 local: false,
3627 },
3628 session: undefined as never,
3629 bindings: [],
3630 });
3631
3632 expect(output.deploymentId).toBe("version-new");
3633 expect(output.promoted).toBe(true);
3634 expect(output.readinessStatus).toBe("ready");
3635 expect(
3636 fixture.calls.filter(
3637 ([operation]) => operation === "healthRequest",
3638 ),
3639 ).toEqual([
3640 ["healthRequest", "https://version-new.preview.prisma.build/ready"],
3641 ["healthRequest", "https://api.prisma.build/ready"],
3642 ]);
3643 expect(
3644 fixture.calls.filter(([operation]) => operation === "rollbackApp"),
3645 ).toEqual([]);
3646 expect(
3647 fixture.calls.filter(
3648 ([operation]) => operation === "deleteDeployment",
3649 ),
3650 ).toEqual([]);
3651 }).pipe(
3652 Effect.provide(computeProviderLive()),
3653 Effect.provide(
3654 Layer.succeed(PrismaClient, withDefaultBranch(fixture.client)),
3655 ),
3656 Effect.provide(apiRoutedHttp(withDefaultBranch(fixture.client))),
3657 Effect.provideService(HttpClient.HttpClient, fixture.http),
3658 Effect.provide(PlatformServices),
3659 );
3660 },
3661 { tags: ["provider:prisma:branch"] },
3662 );
3663
3664 it.effect(
3665 "uploads a pre-created artifact from artifactPath",
3666 () => {
3667 let uploaded:
3668 | { url: string; contentType: string | undefined; bytes: Uint8Array }
3669 | undefined;
3670 const client = {
3671 getApp: () =>
3672 Effect.succeed({
3673 id: "service-1",
3674 type: "app" as const,
3675 url: "https://api.prisma.test/v1/services/service-1",
3676 name: "api",
3677 region: { id: "us-east-1", name: "US East" },
3678 projectId: "project-1",
3679 branchId: "branch-main",
3680 latestDeploymentId: "version-old",
3681 appEndpointDomain: "api.prisma.build",
3682 createdAt: "2026-01-01T00:00:00Z",
3683 }),
3684 listEnvironmentVariables: () => Effect.succeed([]),
3685 createAppDeployment: () =>
3686 Effect.succeed({
3687 id: "version-1",
3688 type: "deployment" as const,
3689 url: "https://api.prisma.test/v1/deployments/version-1",
3690 foundryVersionId: "foundry-1",
3691 uploadUrl: "https://upload.prisma.test/app.tar.gz",
3692 }),
3693 getDeployment: (id: string) =>
3694 Effect.succeed({
3695 id,
3696 type: "deployment" as const,
3697 serviceId: "service-1",
3698 url: "https://api.prisma.test/v1/deployments/version-1",
3699 foundryVersionId: "foundry-1",
3700 status: "new",
3701 previewDomain: "version-1.preview.prisma.build",
3702 createdAt: "2026-01-01T00:00:00Z",
3703 }),
3704 } as unknown as PrismaManagementClient;
3705 const http = HttpClient.make((request) =>
3706 Effect.gen(function* () {
3707 const body = request.body as HttpBody.HttpBody;
3708 uploaded = {
3709 url: request.url,
3710 contentType: httpBodyContentType(body),
3711 bytes: yield* readHttpBodyBytes(body),
3712 };
3713 return HttpClientResponse.fromWeb(request, new Response(null));
3714 }),
3715 );
3716
3717 return Effect.gen(function* () {
3718 const fs = yield* FileSystem.FileSystem;
3719 const path = yield* Path.Path;
3720 const root = yield* fs.makeTempDirectory({
3721 prefix: "alchemy-prisma-artifact-",
3722 });
3723 const artifactPath = path.join(root, "app.tar.gz");
3724 yield* fs.writeFileString(artifactPath, "prebuilt-archive");
3725
3726 const provider = yield* Compute.Provider;
3727 const output = yield* provider.reconcile({
3728 id: "App",
3729 fqn: "App",
3730 instanceId: "00000000000000000000000000000000",
3731 news: {
3732 project: "project-1",
3733 appName: "api",
3734 artifactPath,
3735 branchId: "branch-main",
3736 start: false,
3737 skipPromote: true,
3738 },
3739 olds: undefined,
3740 output: {
3741 appId: "service-1",
3742 deploymentId: undefined,
3743 projectId: "project-1",
3744 appName: "api",
3745 regionId: "us-east-1",
3746 deploymentEndpointDomain: undefined,
3747 deploymentUrl: undefined,
3748 appEndpointDomain: "api.prisma.build",
3749 url: "https://api.prisma.build",
3750 promoted: false,
3751 previousDeploymentId: undefined,
3752 previousDeploymentAction: undefined,
3753 artifactHash: undefined,
3754 local: false,
3755 },
3756 session: undefined as never,
3757 bindings: [],
3758 });
3759
3760 expect(output.deploymentId).toBe("version-1");
3761 expect(uploaded?.url).toBe("https://upload.prisma.test/app.tar.gz");
3762 expect(uploaded?.contentType).toBe("application/gzip");
3763 expect(new TextDecoder().decode(uploaded?.bytes)).toBe(
3764 "prebuilt-archive",
3765 );
3766 }).pipe(
3767 Effect.provide(computeProviderLive()),
3768 Effect.provide(
3769 Layer.succeed(PrismaClient, withDefaultBranch(client)),
3770 ),
3771 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
3772 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
3773 Effect.provide(PlatformServices),
3774 );
3775 },
3776 { tags: ["provider:prisma:branch"] },
3777 );
3778
3779 it.effect(
3780 "bundles effect-native Compute apps into an upload artifact",
3781 () => {
3782 const calls: Array<[string, unknown]> = [];
3783 let uploaded:
3784 | { url: string; contentType: string | undefined; bytes: Uint8Array }
3785 | undefined;
3786 const client = {
3787 listApps: (projectId: string, query: unknown) => {
3788 calls.push(["listApps", { projectId, query }]);
3789 return Effect.succeed([]);
3790 },
3791 createApp: (projectId: string, input: unknown) => {
3792 calls.push(["createApp", { projectId, input }]);
3793 return Effect.succeed({
3794 id: "service-1",
3795 type: "app" as const,
3796 url: "https://api.prisma.test/v1/services/service-1",
3797 name: "api",
3798 region: { id: "us-east-1", name: "US East" },
3799 projectId,
3800 branchId: "branch-main",
3801 latestDeploymentId: "version-old",
3802 appEndpointDomain: "api.prisma.build",
3803 createdAt: "2026-01-01T00:00:00Z",
3804 });
3805 },
3806 listEnvironmentVariables: () => Effect.succeed([]),
3807 createAppDeployment: (appId: string, input: unknown) => {
3808 calls.push(["createAppDeployment", { appId, input }]);
3809 return Effect.succeed({
3810 id: "version-1",
3811 type: "deployment" as const,
3812 url: "https://api.prisma.test/v1/deployments/version-1",
3813 foundryVersionId: "foundry-1",
3814 uploadUrl: "https://upload.prisma.test/effect.tar.gz",
3815 });
3816 },
3817 getDeployment: (id: string) => {
3818 calls.push(["getDeployment", id]);
3819 return Effect.succeed({
3820 id,
3821 type: "deployment" as const,
3822 serviceId: "service-1",
3823 url: "https://api.prisma.test/v1/deployments/version-1",
3824 foundryVersionId: "foundry-1",
3825 status: "new",
3826 previewDomain: "version-1.preview.prisma.build",
3827 createdAt: "2026-01-01T00:00:00Z",
3828 });
3829 },
3830 } as unknown as PrismaManagementClient;
3831 const http = HttpClient.make((request) =>
3832 Effect.gen(function* () {
3833 const body = request.body as HttpBody.HttpBody;
3834 uploaded = {
3835 url: request.url,
3836 contentType: httpBodyContentType(body),
3837 bytes: yield* readHttpBodyBytes(body),
3838 };
3839 return HttpClientResponse.fromWeb(request, new Response(null));
3840 }),
3841 );
3842
3843 return Effect.gen(function* () {
3844 const fs = yield* FileSystem.FileSystem;
3845 const path = yield* Path.Path;
3846 const root = yield* fs.makeTempDirectory({
3847 prefix: "alchemy-prisma-compute-effect-",
3848 });
3849 const main = path.join(root, "app.ts");
3850 yield* fs.writeFileString(
3851 main,
3852 [
3853 'import * as Prisma from "alchemy/Prisma";',
3854 'import * as Effect from "effect/Effect";',
3855 'import * as HttpServerResponse from "effect/http/HttpServerResponse";',
3856 "",
3857 "export default Prisma.Compute(",
3858 ' "App",',
3859 " {",
3860 ' project: "project-1",',
3861 ' appName: "api",',
3862 " main: import.meta.filename,",
3863 " port: 4555,",
3864 " },",
3865 " Effect.gen(function* () {",
3866 " return {",
3867 ' fetch: HttpServerResponse.text("effect-native-ok"),',
3868 " };",
3869 " }),",
3870 ");",
3871 "",
3872 ].join("\n"),
3873 );
3874
3875 const provider = yield* Compute.Provider;
3876 const output = yield* provider.reconcile({
3877 id: "App",
3878 fqn: "App",
3879 instanceId: "00000000000000000000000000000000",
3880 news: {
3881 project: "project-1",
3882 appName: "api",
3883 branchId: "branch-main",
3884 main,
3885 port: 4555,
3886 start: false,
3887 skipPromote: true,
3888 },
3889 olds: undefined,
3890 output: undefined,
3891 session: undefined as never,
3892 bindings: [],
3893 });
3894
3895 expect(output.deploymentId).toBe("version-1");
3896 expect(uploaded?.url).toBe(
3897 "https://upload.prisma.test/effect.tar.gz",
3898 );
3899 expect(uploaded?.contentType).toBe("application/gzip");
3900 const tar = yield* Effect.sync(() => gunzipSync(uploaded!.bytes));
3901 const manifest = readTarFile(tar, "compute.manifest.json");
3902 const bundle = readTarFile(tar, "bundle/index.js");
3903 expect(JSON.parse(manifest)).toMatchObject({
3904 entrypoint: "bundle/index.js",
3905 });
3906 // The generated entry is a shim over alchemy/Runtime/Bootstrap/Prisma;
3907 // its label and the shared "bootstrap starting" message are separate
3908 // literals joined at runtime (see Runtime/Bootstrap/Process.ts).
3909 expect(bundle).toContain("Prisma Compute");
3910 expect(bundle).toContain("bootstrap starting");
3911 expect(bundle).toMatch(/hostname\s*:\s*["'`]0\.0\.0\.0["'`]/);
3912 // The deploy-time stack identity is baked into the artifact (the shim
3913 // passes it to the bootstrap once; the module fans it out to `Stack`
3914 // and `Stage`).
3915 expect(bundle).toContain("prisma-runtime-stack-sentinel");
3916 expect(bundle).toContain("prisma-runtime-stage-sentinel");
3917 expect(bundle).toContain("ALCHEMY_PHASE");
3918 expect(bundle).toContain("runtime");
3919 expect(bundle).toContain("effect-native-ok");
3920 expect(calls).toContainEqual([
3921 "createAppDeployment",
3922 {
3923 appId: "service-1",
3924 input: {
3925 portMapping: { http: 4555 },
3926 skipCodeUpload: undefined,
3927 },
3928 },
3929 ]);
3930 }).pipe(
3931 Effect.provideService(Stack, {
3932 name: "prisma-runtime-stack-sentinel",
3933 stage: "prisma-runtime-stage-sentinel",
3934 bindings: {},
3935 resources: {},
3936 actions: {},
3937 }),
3938 Effect.provide(computeProviderLive()),
3939 Effect.provide(
3940 Layer.succeed(PrismaClient, withDefaultBranch(client)),
3941 ),
3942 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
3943 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
3944 Effect.provide(PlatformServices),
3945 );
3946 },
3947 { tags: ["provider:prisma:branch"] },
3948 );
3949
3950 it.effect(
3951 "bundles effect-native Compute apps from a named export",
3952 () => {
3953 let uploaded: Uint8Array | undefined;
3954 const client = {
3955 listApps: () => Effect.succeed([]),
3956 createApp: ({ projectId }: { projectId: string }) =>
3957 Effect.succeed({
3958 id: "service-1",
3959 type: "app" as const,
3960 url: "https://api.prisma.test/v1/services/service-1",
3961 name: "api",
3962 region: { id: "us-east-1", name: "US East" },
3963 projectId,
3964 branchId: "branch-main",
3965 latestDeploymentId: "version-old",
3966 appEndpointDomain: "api.prisma.build",
3967 createdAt: "2026-01-01T00:00:00Z",
3968 }),
3969 listEnvironmentVariables: () => Effect.succeed([]),
3970 createAppDeployment: () =>
3971 Effect.succeed({
3972 id: "version-1",
3973 type: "deployment" as const,
3974 url: "https://api.prisma.test/v1/deployments/version-1",
3975 foundryVersionId: "foundry-1",
3976 uploadUrl: "https://upload.prisma.test/effect.tar.gz",
3977 }),
3978 getDeployment: (id: string) =>
3979 Effect.succeed({
3980 id,
3981 type: "deployment" as const,
3982 serviceId: "service-1",
3983 url: "https://api.prisma.test/v1/deployments/version-1",
3984 foundryVersionId: "foundry-1",
3985 status: "new",
3986 previewDomain: "version-1.preview.prisma.build",
3987 createdAt: "2026-01-01T00:00:00Z",
3988 }),
3989 } as unknown as PrismaManagementClient;
3990 const http = HttpClient.make((request) =>
3991 Effect.gen(function* () {
3992 const body = request.body as HttpBody.HttpBody;
3993 uploaded = yield* readHttpBodyBytes(body);
3994 return HttpClientResponse.fromWeb(request, new Response(null));
3995 }),
3996 );
3997
3998 return Effect.gen(function* () {
3999 const fs = yield* FileSystem.FileSystem;
4000 const path = yield* Path.Path;
4001 const root = yield* fs.makeTempDirectory({
4002 prefix: "alchemy-prisma-compute-named-effect-",
4003 });
4004 const main = path.join(root, "app.ts");
4005 yield* fs.writeFileString(
4006 main,
4007 [
4008 'import * as Prisma from "alchemy/Prisma";',
4009 'import * as Effect from "effect/Effect";',
4010 'import * as HttpServerResponse from "effect/http/HttpServerResponse";',
4011 "",
4012 "export const Api = Prisma.Compute(",
4013 ' "App",',
4014 " {",
4015 ' project: "project-1",',
4016 ' appName: "api",',
4017 " main: import.meta.filename,",
4018 ' handler: "Api",',
4019 " },",
4020 " Effect.gen(function* () {",
4021 " return {",
4022 ' fetch: HttpServerResponse.text("named-handler-ok"),',
4023 " };",
4024 " }),",
4025 ");",
4026 "",
4027 ].join("\n"),
4028 );
4029
4030 const provider = yield* Compute.Provider;
4031 const output = yield* provider.reconcile({
4032 id: "App",
4033 fqn: "App",
4034 instanceId: "00000000000000000000000000000000",
4035 news: {
4036 project: "project-1",
4037 appName: "api",
4038 branchId: "branch-main",
4039 main,
4040 handler: "Api",
4041 start: false,
4042 skipPromote: true,
4043 },
4044 olds: undefined,
4045 output: undefined,
4046 session: undefined as never,
4047 bindings: [],
4048 });
4049
4050 expect(output.deploymentId).toBe("version-1");
4051 const tarText = new TextDecoder().decode(
4052 yield* Effect.sync(() => gunzipSync(uploaded!)),
4053 );
4054 expect(tarText).toContain("compute.manifest.json");
4055 expect(tarText).toContain("named-handler-ok");
4056 }).pipe(
4057 Effect.provide(computeProviderLive()),
4058 Effect.provide(
4059 Layer.succeed(PrismaClient, withDefaultBranch(client)),
4060 ),
4061 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
4062 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
4063 Effect.provide(PlatformServices),
4064 );
4065 },
4066 { tags: ["provider:prisma:branch"] },
4067 );
4068
4069 it.effect("syncs env vars through the environment variable API", () => {
4070 const calls: Array<[string, unknown]> = [];
4071 const projectToken = {
4072 id: "env-token",
4073 type: "environment-variable" as const,
4074 url: "https://api.prisma.test/v1/environment-variables/env-token",
4075 projectId: "project-1",
4076 branchId: null,
4077 class: "production" as const,
4078 key: "TOKEN",
4079 valueKid: "kid-1",
4080 isManagedBySystem: false,
4081 createdAt: "2026-01-01T00:00:00Z",
4082 updatedAt: "2026-01-01T00:00:00Z",
4083 };
4084 const projectRemove = {
4085 id: "env-remove",
4086 type: "environment-variable" as const,
4087 url: "https://api.prisma.test/v1/environment-variables/env-remove",
4088 projectId: "project-1",
4089 branchId: null,
4090 class: "production" as const,
4091 key: "REMOVE_ME",
4092 valueKid: "kid-1",
4093 isManagedBySystem: false,
4094 createdAt: "2026-01-01T00:00:00Z",
4095 updatedAt: "2026-01-01T00:00:00Z",
4096 };
4097 const byKey = new Map([
4098 [
4099 "TOKEN",
4100 [
4101 { ...projectToken, id: "env-token-branch", branchId: "branch-1" },
4102 { ...projectToken, id: "env-token-branch-2", branchId: "branch-2" },
4103 projectToken,
4104 ],
4105 ],
4106 [
4107 "REMOVE_ME",
4108 [
4109 { ...projectRemove, id: "env-remove-branch", branchId: "branch-1" },
4110 projectRemove,
4111 ],
4112 ],
4113 ]);
4114
4115 const client = {
4116 listEnvironmentVariables: (query: { key: string }) => {
4117 calls.push(["list", query]);
4118 return Effect.succeed(byKey.get(query.key) ?? []);
4119 },
4120 createEnvironmentVariable: (input: unknown) => {
4121 calls.push(["create", input]);
4122 return Effect.succeed({
4123 id: "env-created",
4124 type: "environment-variable" as const,
4125 url: "https://api.prisma.test/v1/environment-variables/env-created",
4126 projectId: "project-1",
4127 branchId: "branch-main",
4128 class: "production" as const,
4129 key: "API_URL",
4130 valueKid: "kid-2",
4131 isManagedBySystem: false,
4132 createdAt: "2026-01-01T00:00:00Z",
4133 updatedAt: "2026-01-01T00:00:00Z",
4134 });
4135 },
4136 updateEnvironmentVariable: (id: string, input: unknown) => {
4137 calls.push(["update", { id, input }]);
4138 return Effect.succeed(projectToken);
4139 },
4140 deleteEnvironmentVariable: (id: string) => {
4141 calls.push(["delete", id]);
4142 return Effect.void;
4143 },
4144 } as unknown as PrismaManagementClient;
4145
4146 return Effect.gen(function* () {
4147 const result = yield* syncComputeEnvironment(
4148 "project-1",
4149 "production",
4150 {
4151 API_URL: "https://example.test",
4152 TOKEN: Redacted.make("secret"),
4153 REMOVE_ME: null,
4154 SKIP_ME: undefined,
4155 },
4156 undefined,
4157 {
4158 TOKEN: "env-token",
4159 REMOVE_ME: "env-remove",
4160 },
4161 );
4162
4163 expect(result).toEqual({
4164 synced: ["API_URL", "TOKEN"],
4165 deleted: ["REMOVE_ME"],
4166 ownedIds: {
4167 API_URL: "env-created",
4168 TOKEN: "env-token",
4169 },
4170 });
4171 expect(calls).toEqual([
4172 [
4173 "list",
4174 {
4175 projectId: "project-1",
4176 class: "production",
4177 key: "API_URL",
4178 limit: "100",
4179 },
4180 ],
4181 [
4182 "list",
4183 {
4184 projectId: "project-1",
4185 class: "production",
4186 key: "TOKEN",
4187 limit: "100",
4188 },
4189 ],
4190 [
4191 "list",
4192 {
4193 projectId: "project-1",
4194 class: "production",
4195 key: "REMOVE_ME",
4196 limit: "100",
4197 },
4198 ],
4199 [
4200 "create",
4201 {
4202 projectId: "project-1",
4203 class: "production",
4204 key: "API_URL",
4205 value: "https://example.test",
4206 },
4207 ],
4208 ["update", { id: "env-token", input: { value: "secret" } }],
4209 ["delete", "env-remove"],
4210 ]);
4211 }).pipe(Effect.provide(apiRoutedHttp(client)));
4212 });
4213
4214 it.effect(
4215 "rolls back variables created by a partially failed env sync",
4216 () => {
4217 const calls: Array<[string, unknown]> = [];
4218 const createError = new Error("second create failed");
4219 const client = {
4220 listEnvironmentVariables: (query: unknown) => {
4221 calls.push(["list", query]);
4222 return Effect.succeed([]);
4223 },
4224 createEnvironmentVariable: (input: { key: string }) => {
4225 calls.push(["create", input]);
4226 return input.key === "A"
4227 ? Effect.succeed({
4228 id: "env-a",
4229 type: "environment-variable" as const,
4230 url: "https://api.prisma.test/v1/environment-variables/env-a",
4231 projectId: "project-1",
4232 branchId: null,
4233 class: "production" as const,
4234 key: "A",
4235 valueKid: "kid-a",
4236 isManagedBySystem: false,
4237 createdAt: "2026-01-01T00:00:00Z",
4238 updatedAt: "2026-01-01T00:00:00Z",
4239 })
4240 : Effect.fail(createError);
4241 },
4242 deleteEnvironmentVariable: (id: string) => {
4243 calls.push(["delete", id]);
4244 return Effect.void;
4245 },
4246 } as unknown as PrismaManagementClient;
4247
4248 return Effect.gen(function* () {
4249 const error = yield* syncComputeEnvironment(
4250 "project-1",
4251 "production",
4252 {
4253 A: "one",
4254 B: "two",
4255 },
4256 ).pipe(Effect.flip);
4257
4258 expect((error as Error).message).toContain("second create failed");
4259 expect(calls.map(([name]) => name)).toEqual([
4260 "list",
4261 "list",
4262 "create",
4263 "create",
4264 "delete",
4265 ]);
4266 expect(calls).toContainEqual(["delete", "env-a"]);
4267 }).pipe(Effect.provide(apiRoutedHttp(client)));
4268 },
4269 );
4270
4271 it.effect(
4272 "surfaces env rollback failures with manual cleanup routes",
4273 () => {
4274 const createError = new Error("second create failed");
4275 const cleanupError = new Error("rollback delete failed");
4276 const client = {
4277 listEnvironmentVariables: () => Effect.succeed([]),
4278 createEnvironmentVariable: (input: { key: string }) =>
4279 input.key === "A"
4280 ? Effect.succeed({
4281 id: "env-a",
4282 type: "environment-variable" as const,
4283 url: "https://api.prisma.test/v1/environment-variables/env-a",
4284 projectId: "project-1",
4285 branchId: null,
4286 class: "production" as const,
4287 key: "A",
4288 valueKid: "kid-a",
4289 isManagedBySystem: false,
4290 createdAt: "2026-01-01T00:00:00Z",
4291 updatedAt: "2026-01-01T00:00:00Z",
4292 })
4293 : Effect.fail(createError),
4294 deleteEnvironmentVariable: () => Effect.fail(cleanupError),
4295 } as unknown as PrismaManagementClient;
4296
4297 return Effect.gen(function* () {
4298 const error = yield* syncComputeEnvironment(
4299 "project-1",
4300 "production",
4301 {
4302 A: "one",
4303 B: "two",
4304 },
4305 ).pipe(Effect.flip);
4306
4307 expect(error).toBeInstanceOf(AggregateError);
4308 expect(
4309 ((error as AggregateError).errors[0] as Error).message,
4310 ).toContain("second create failed");
4311 expect(
4312 ((error as AggregateError).errors[1] as Error).message,
4313 ).toContain("rollback delete failed");
4314 expect((error as AggregateError).message).toContain(
4315 "DELETE /v1/environment-variables/env-a",
4316 );
4317 }).pipe(Effect.provide(apiRoutedHttp(client)));
4318 },
4319 );
4320
4321 it.effect("preflights foreign env ownership before any write", () => {
4322 const calls: string[] = [];
4323 const foreign = {
4324 id: "env-foreign",
4325 type: "environment-variable" as const,
4326 url: "https://api.prisma.test/v1/environment-variables/env-foreign",
4327 projectId: "project-1",
4328 branchId: null,
4329 class: "production" as const,
4330 key: "B",
4331 valueKid: "kid-foreign",
4332 isManagedBySystem: false,
4333 createdAt: "2026-01-01T00:00:00Z",
4334 updatedAt: "2026-01-01T00:00:00Z",
4335 };
4336 const client = {
4337 listEnvironmentVariables: (query: { key: string }) => {
4338 calls.push(`list:${query.key}`);
4339 return Effect.succeed(query.key === "B" ? [foreign] : []);
4340 },
4341 createEnvironmentVariable: () => {
4342 calls.push("create");
4343 return Effect.die("ownership preflight must happen first");
4344 },
4345 } as unknown as PrismaManagementClient;
4346
4347 return Effect.gen(function* () {
4348 const error = yield* syncComputeEnvironment("project-1", "production", {
4349 A: "one",
4350 B: "two",
4351 }).pipe(Effect.flip);
4352
4353 expect((error as Error).message).toContain("is not owned");
4354 expect(calls).toEqual(["list:A", "list:B"]);
4355 }).pipe(Effect.provide(apiRoutedHttp(client)));
4356 });
4357
4358 it.effect(
4359 "preserves old owned env variables when new-scope validation fails",
4360 () => {
4361 const calls: Array<[string, unknown?]> = [];
4362 const client = {
4363 getApp: () =>
4364 Effect.succeed({
4365 id: "service-1",
4366 type: "app" as const,
4367 url: "https://api.prisma.test/v1/services/service-1",
4368 name: "api",
4369 region: { id: "us-east-1", name: "US East" },
4370 projectId: "project-1",
4371 branchId: "branch-main",
4372 latestDeploymentId: "version-old",
4373 appEndpointDomain: "api.prisma.build",
4374 createdAt: "2026-01-01T00:00:00Z",
4375 }),
4376 getBranch: () => Effect.succeed(testBranch("branch-main")),
4377 getDeployment: (id: string) =>
4378 Effect.succeed({
4379 id,
4380 type: "deployment" as const,
4381 serviceId: "service-1",
4382 url: `https://api.prisma.test/v1/deployments/${id}`,
4383 foundryVersionId: `foundry-${id}`,
4384 status: "running",
4385 previewDomain: `${id}.preview.prisma.build`,
4386 createdAt: "2026-01-01T00:00:00Z",
4387 }),
4388 listEnvironmentVariables: (query: { key: string }) => {
4389 calls.push(["listEnvironmentVariables", query]);
4390 return Effect.succeed([
4391 {
4392 id: query.key === "OLD" ? "env-old" : "env-foreign",
4393 type: "environment-variable" as const,
4394 url: `https://api.prisma.test/v1/environment-variables/${query.key}`,
4395 projectId: "project-1",
4396 branchId: null,
4397 class: "production" as const,
4398 key: query.key,
4399 valueKid: "kid-1",
4400 isManagedBySystem: false,
4401 createdAt: "2026-01-01T00:00:00Z",
4402 updatedAt: "2026-01-01T00:00:00Z",
4403 },
4404 ]);
4405 },
4406 deleteEnvironmentVariable: (id: string) => {
4407 calls.push(["deleteEnvironmentVariable", id]);
4408 return Effect.void;
4409 },
4410 } as unknown as PrismaManagementClient;
4411
4412 return Effect.gen(function* () {
4413 const provider = yield* Compute.Provider;
4414 const error = yield* provider
4415 .reconcile({
4416 id: "App",
4417 fqn: "App",
4418 instanceId: "00000000000000000000000000000000",
4419 news: {
4420 project: "project-1",
4421 appName: "api",
4422 artifactPath: fixtureArtifactPath,
4423 branchId: "branch-main",
4424 env: { FOREIGN: "new" },
4425 start: false,
4426 skipPromote: true,
4427 },
4428 olds: {
4429 project: "project-1",
4430 appName: "api",
4431 artifactPath: fixtureArtifactV1Path,
4432 branchId: "branch-main",
4433 env: { OLD: "old" },
4434 },
4435 output: {
4436 appId: "service-1",
4437 deploymentId: "version-old",
4438 projectId: "project-1",
4439 appName: "api",
4440 regionId: "us-east-1",
4441 deploymentEndpointDomain: "version-old.preview.prisma.build",
4442 deploymentUrl: "https://version-old.preview.prisma.build",
4443 appEndpointDomain: "api.prisma.build",
4444 url: "https://api.prisma.build",
4445 promoted: true,
4446 previousDeploymentId: undefined,
4447 previousDeploymentAction: undefined,
4448 environmentVariableIds: { OLD: "env-old" },
4449 environmentClass: "production",
4450 environmentBranchId: null,
4451 artifactHash: Redacted.make("old-hash"),
4452 local: false,
4453 },
4454 session: undefined as never,
4455 bindings: [],
4456 })
4457 .pipe(Effect.flip);
4458
4459 expect(error.message).toContain("is not owned");
4460 expect(calls).toEqual([
4461 [
4462 "listEnvironmentVariables",
4463 {
4464 projectId: "project-1",
4465 class: "production",
4466 key: "FOREIGN",
4467 limit: "100",
4468 },
4469 ],
4470 ]);
4471 }).pipe(
4472 Effect.provide(computeProviderLive()),
4473 Effect.provide(
4474 Layer.succeed(PrismaClient, withDefaultBranch(client)),
4475 ),
4476 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
4477 Effect.provide(FetchHttpClient.layer),
4478 Effect.provide(PlatformServices),
4479 );
4480 },
4481 { tags: ["provider:prisma:branch"] },
4482 );
4483
4484 it.effect("refuses to sync system-managed Compute env vars", () => {
4485 const calls: Array<[string, unknown]> = [];
4486 const systemVariable = {
4487 id: "env-system",
4488 type: "environment-variable" as const,
4489 url: "https://api.prisma.test/v1/environment-variables/env-system",
4490 projectId: "project-1",
4491 branchId: null,
4492 class: "production" as const,
4493 key: "PRISMA_INTERNAL_URL",
4494 valueKid: "kid-system",
4495 isManagedBySystem: true,
4496 createdAt: "2026-01-01T00:00:00Z",
4497 updatedAt: "2026-01-01T00:00:00Z",
4498 };
4499 const client = {
4500 listEnvironmentVariables: (query: unknown) => {
4501 calls.push(["list", query]);
4502 return Effect.succeed([systemVariable]);
4503 },
4504 updateEnvironmentVariable: (id: string, input: unknown) => {
4505 calls.push(["update", { id, input }]);
4506 return Effect.succeed(systemVariable);
4507 },
4508 } as unknown as PrismaManagementClient;
4509
4510 return Effect.gen(function* () {
4511 const error = yield* syncComputeEnvironment("project-1", "production", {
4512 PRISMA_INTERNAL_URL: "secret",
4513 }).pipe(Effect.flip);
4514
4515 expect(error).toBeInstanceOf(Error);
4516 expect((error as Error).message).toContain(
4517 "is managed by Prisma and cannot be managed by Alchemy",
4518 );
4519 expect(calls).toEqual([
4520 [
4521 "list",
4522 {
4523 projectId: "project-1",
4524 class: "production",
4525 key: "PRISMA_INTERNAL_URL",
4526 limit: "100",
4527 },
4528 ],
4529 ]);
4530 }).pipe(Effect.provide(apiRoutedHttp(client)));
4531 });
4532
4533 it.effect(
4534 "validates Compute env vars before remote writes",
4535 () => {
4536 const calls: Array<[string, unknown]> = [];
4537 const client = {
4538 getApp: (id: string) =>
4539 Effect.sync(() => {
4540 calls.push(["getApp", id]);
4541 return {
4542 id,
4543 type: "app" as const,
4544 url: "https://api.prisma.test/v1/services/service-1",
4545 name: "api",
4546 region: { id: "us-east-1", name: "US East" },
4547 projectId: "project-1",
4548 branchId: "branch-main",
4549 latestDeploymentId: "version-old",
4550 appEndpointDomain: "api.prisma.build",
4551 createdAt: "2026-01-01T00:00:00Z",
4552 };
4553 }),
4554 } as unknown as PrismaManagementClient;
4555
4556 return Effect.gen(function* () {
4557 const provider = yield* Compute.Provider;
4558 const error = yield* provider
4559 .reconcile({
4560 id: "App",
4561 fqn: "App",
4562 instanceId: "00000000000000000000000000000000",
4563 news: {
4564 project: "project-1",
4565 appName: "api",
4566 artifactPath: fixtureArtifactV1Path,
4567 env: {
4568 "bad-key": "secret",
4569 },
4570 },
4571 olds: undefined,
4572 output: undefined,
4573 session: undefined as never,
4574 bindings: [],
4575 })
4576 .pipe(Effect.flip);
4577
4578 expect(error).toBeInstanceOf(Error);
4579 expect(error.message).toContain("must match POSIX env-var key shape");
4580 expect(calls).toEqual([]);
4581 }).pipe(
4582 Effect.provide(computeProviderLive()),
4583 Effect.provide(
4584 Layer.succeed(PrismaClient, withDefaultBranch(client)),
4585 ),
4586 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
4587 Effect.provide(PlatformServices),
4588 );
4589 },
4590 { tags: ["provider:prisma:branch"] },
4591 );
4592
4593 it.effect(
4594 "merges binding env into Compute deployment env",
4595 () => {
4596 const calls: Array<[string, unknown]> = [];
4597 const client = {
4598 getApp: (id: string) => {
4599 calls.push(["getApp", id]);
4600 return Effect.succeed({
4601 id,
4602 type: "app" as const,
4603 url: "https://api.prisma.test/v1/services/service-1",
4604 name: "api",
4605 region: { id: "us-east-1", name: "US East" },
4606 projectId: "project-1",
4607 branchId: "branch-main",
4608 latestDeploymentId: "version-old",
4609 appEndpointDomain: "api.prisma.build",
4610 createdAt: "2026-01-01T00:00:00Z",
4611 });
4612 },
4613 listEnvironmentVariables: (query: unknown) => {
4614 calls.push(["listEnvironmentVariables", query]);
4615 return Effect.succeed([]);
4616 },
4617 createEnvironmentVariable: (input: unknown) => {
4618 calls.push(["createEnvironmentVariable", input]);
4619 return Effect.succeed({
4620 id: "env-created",
4621 type: "environment-variable" as const,
4622 url: "https://api.prisma.test/v1/environment-variables/env-created",
4623 projectId: "project-1",
4624 branchId: "branch-main",
4625 class: "production" as const,
4626 key: "DATABASE_URL",
4627 valueKid: "kid-created",
4628 isManagedBySystem: false,
4629 createdAt: "2026-01-01T00:00:00Z",
4630 updatedAt: "2026-01-01T00:00:00Z",
4631 });
4632 },
4633 createAppDeployment: (appId: string, input: unknown) => {
4634 calls.push(["createAppDeployment", { appId, input }]);
4635 return Effect.succeed({
4636 id: "version-1",
4637 type: "deployment" as const,
4638 url: "https://api.prisma.test/v1/deployments/version-1",
4639 foundryVersionId: "foundry-1",
4640 uploadUrl: null,
4641 });
4642 },
4643 getDeployment: (id: string) => {
4644 calls.push(["getDeployment", id]);
4645 return Effect.succeed({
4646 id,
4647 type: "deployment" as const,
4648 serviceId: "service-1",
4649 url: "https://api.prisma.test/v1/deployments/version-1",
4650 foundryVersionId: "foundry-1",
4651 status: "new",
4652 previewDomain: "version-1.preview.prisma.build",
4653 createdAt: "2026-01-01T00:00:00Z",
4654 });
4655 },
4656 } as unknown as PrismaManagementClient;
4657
4658 return Effect.gen(function* () {
4659 const provider = yield* Compute.Provider;
4660 const output = yield* provider.reconcile({
4661 id: "App",
4662 fqn: "App",
4663 instanceId: "00000000000000000000000000000000",
4664 news: {
4665 project: "project-1",
4666 appName: "api",
4667 branchId: "branch-main",
4668 skipCodeUpload: true,
4669 start: false,
4670 skipPromote: true,
4671 envClass: "preview",
4672 },
4673 olds: undefined,
4674 output: {
4675 appId: "service-1",
4676 deploymentId: undefined,
4677 projectId: "project-1",
4678 appName: "api",
4679 regionId: "us-east-1",
4680 deploymentEndpointDomain: undefined,
4681 deploymentUrl: undefined,
4682 appEndpointDomain: "api.prisma.build",
4683 url: "https://api.prisma.build",
4684 promoted: false,
4685 previousDeploymentId: undefined,
4686 previousDeploymentAction: undefined,
4687 artifactHash: undefined,
4688 local: false,
4689 },
4690 session: undefined as never,
4691 bindings: [
4692 {
4693 sid: "Connection",
4694 data: {
4695 env: {
4696 DATABASE_URL: Redacted.make("postgres://bound"),
4697 SHARED_FLAG: "from-binding",
4698 },
4699 },
4700 },
4701 ],
4702 });
4703
4704 expect(output.deploymentId).toBe("version-1");
4705 expect(output.environmentKeys).toEqual([
4706 "DATABASE_URL",
4707 "SHARED_FLAG",
4708 ]);
4709 expect(output.environmentClass).toBe("preview");
4710 expect(calls).toContainEqual([
4711 "createEnvironmentVariable",
4712 {
4713 projectId: "project-1",
4714 branchId: "branch-main",
4715 class: "preview",
4716 key: "DATABASE_URL",
4717 value: "postgres://bound",
4718 },
4719 ]);
4720 expect(calls).toContainEqual([
4721 "createEnvironmentVariable",
4722 {
4723 projectId: "project-1",
4724 branchId: "branch-main",
4725 class: "preview",
4726 key: "SHARED_FLAG",
4727 value: "from-binding",
4728 },
4729 ]);
4730 }).pipe(
4731 Effect.provide(computeProviderLive()),
4732 Effect.provide(
4733 Layer.succeed(PrismaClient, withDefaultBranch(client, "preview")),
4734 ),
4735 Effect.provide(apiRoutedHttp(withDefaultBranch(client, "preview"))),
4736 Effect.provide(FetchHttpClient.layer),
4737 Effect.provide(PlatformServices),
4738 );
4739 },
4740 { tags: ["provider:prisma:branch"] },
4741 );
4742
4743 it.effect(
4744 "lets explicit Compute env override bindings and ignores deleted bindings",
4745 () => {
4746 const calls: Array<[string, unknown]> = [];
4747 const client = {
4748 getApp: (id: string) => {
4749 calls.push(["getApp", id]);
4750 return Effect.succeed({
4751 id,
4752 type: "app" as const,
4753 url: "https://api.prisma.test/v1/services/service-1",
4754 name: "api",
4755 region: { id: "us-east-1", name: "US East" },
4756 projectId: "project-1",
4757 branchId: "branch-main",
4758 latestDeploymentId: "version-old",
4759 appEndpointDomain: "api.prisma.build",
4760 createdAt: "2026-01-01T00:00:00Z",
4761 });
4762 },
4763 listEnvironmentVariables: (query: unknown) => {
4764 calls.push(["listEnvironmentVariables", query]);
4765 return Effect.succeed([]);
4766 },
4767 createEnvironmentVariable: (input: {
4768 key: string;
4769 projectId: string;
4770 class: "production" | "preview";
4771 }) => {
4772 calls.push(["createEnvironmentVariable", input]);
4773 return Effect.succeed({
4774 id: `env-${input.key.toLowerCase()}`,
4775 type: "environment-variable" as const,
4776 url: `https://api.prisma.test/v1/environment-variables/env-${input.key.toLowerCase()}`,
4777 projectId: input.projectId,
4778 branchId: null,
4779 class: input.class,
4780 key: input.key,
4781 valueKid: `kid-${input.key.toLowerCase()}`,
4782 isManagedBySystem: false,
4783 createdAt: "2026-01-01T00:00:00Z",
4784 updatedAt: "2026-01-01T00:00:00Z",
4785 });
4786 },
4787 createAppDeployment: (appId: string, input: unknown) => {
4788 calls.push(["createAppDeployment", { appId, input }]);
4789 return Effect.succeed({
4790 id: "version-1",
4791 type: "deployment" as const,
4792 url: "https://api.prisma.test/v1/deployments/version-1",
4793 foundryVersionId: "foundry-1",
4794 uploadUrl: "https://upload.prisma.test/version-1.tar.gz",
4795 });
4796 },
4797 getDeployment: (id: string) => {
4798 calls.push(["getDeployment", id]);
4799 return Effect.succeed({
4800 id,
4801 type: "deployment" as const,
4802 serviceId: "service-1",
4803 url: "https://api.prisma.test/v1/deployments/version-1",
4804 foundryVersionId: "foundry-1",
4805 status: "new",
4806 previewDomain: "version-1.preview.prisma.build",
4807 createdAt: "2026-01-01T00:00:00Z",
4808 });
4809 },
4810 } as unknown as PrismaManagementClient;
4811 return Effect.gen(function* () {
4812 const provider = yield* Compute.Provider;
4813 const deletedBinding = {
4814 sid: "RemovedConnection",
4815 action: "delete",
4816 data: {
4817 env: {
4818 DELETED_BINDING: "must-not-sync",
4819 },
4820 },
4821 } as ResourceBinding<Compute["Binding"]> & { action: "delete" };
4822 const output = yield* provider.reconcile({
4823 id: "App",
4824 fqn: "App",
4825 instanceId: "00000000000000000000000000000000",
4826 news: {
4827 project: "project-1",
4828 appName: "api",
4829 branchId: "branch-main",
4830 skipCodeUpload: true,
4831 start: false,
4832 skipPromote: true,
4833 env: {
4834 DATABASE_URL: Redacted.make("postgres://explicit"),
4835 BOUND_ONLY: null,
4836 },
4837 },
4838 olds: undefined,
4839 output: {
4840 appId: "service-1",
4841 deploymentId: undefined,
4842 projectId: "project-1",
4843 appName: "api",
4844 regionId: "us-east-1",
4845 deploymentEndpointDomain: undefined,
4846 deploymentUrl: undefined,
4847 appEndpointDomain: "api.prisma.build",
4848 url: "https://api.prisma.build",
4849 promoted: false,
4850 previousDeploymentId: undefined,
4851 previousDeploymentAction: undefined,
4852 artifactHash: undefined,
4853 local: false,
4854 },
4855 session: undefined as never,
4856 bindings: [
4857 {
4858 sid: "Connection",
4859 data: {
4860 env: {
4861 DATABASE_URL: Redacted.make("postgres://bound"),
4862 BOUND_ONLY: "from-binding",
4863 ACTIVE_BINDING: "from-active-binding",
4864 },
4865 },
4866 },
4867 deletedBinding,
4868 ],
4869 });
4870
4871 expect(output.environmentKeys).toEqual([
4872 "ACTIVE_BINDING",
4873 "DATABASE_URL",
4874 ]);
4875 expect(calls).toContainEqual([
4876 "createEnvironmentVariable",
4877 {
4878 projectId: "project-1",
4879 class: "production",
4880 key: "DATABASE_URL",
4881 value: "postgres://explicit",
4882 },
4883 ]);
4884 expect(calls).toContainEqual([
4885 "createEnvironmentVariable",
4886 {
4887 projectId: "project-1",
4888 class: "production",
4889 key: "ACTIVE_BINDING",
4890 value: "from-active-binding",
4891 },
4892 ]);
4893 expect(calls).not.toContainEqual([
4894 "createEnvironmentVariable",
4895 {
4896 projectId: "project-1",
4897 class: "production",
4898 key: "DELETED_BINDING",
4899 value: "must-not-sync",
4900 },
4901 ]);
4902 expect(calls).not.toContainEqual([
4903 "createEnvironmentVariable",
4904 {
4905 projectId: "project-1",
4906 class: "production",
4907 key: "BOUND_ONLY",
4908 value: "from-binding",
4909 },
4910 ]);
4911 }).pipe(
4912 Effect.provide(computeProviderLive()),
4913 Effect.provide(
4914 Layer.succeed(PrismaClient, withDefaultBranch(client)),
4915 ),
4916 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
4917 Effect.provide(FetchHttpClient.layer),
4918 Effect.provide(PlatformServices),
4919 );
4920 },
4921 { tags: ["provider:prisma:branch"] },
4922 );
4923
4924 it.effect(
4925 "removes env vars from previously managed bindings",
4926 () => {
4927 const calls: Array<[string, unknown]> = [];
4928 const byKey = new Map([
4929 [
4930 "OLD_BOUND_FLAG",
4931 {
4932 id: "env-old-bound-flag",
4933 type: "environment-variable" as const,
4934 url: "https://api.prisma.test/v1/environment-variables/env-old-bound-flag",
4935 projectId: "project-1",
4936 branchId: null,
4937 class: "production" as const,
4938 key: "OLD_BOUND_FLAG",
4939 valueKid: "kid-old",
4940 isManagedBySystem: false,
4941 createdAt: "2026-01-01T00:00:00Z",
4942 updatedAt: "2026-01-01T00:00:00Z",
4943 },
4944 ],
4945 ]);
4946 const client = {
4947 getApp: (id: string) => {
4948 calls.push(["getApp", id]);
4949 return Effect.succeed({
4950 id,
4951 type: "app" as const,
4952 url: "https://api.prisma.test/v1/services/service-1",
4953 name: "api",
4954 region: { id: "us-east-1", name: "US East" },
4955 projectId: "project-1",
4956 branchId: "branch-main",
4957 latestDeploymentId: "version-old",
4958 appEndpointDomain: "api.prisma.build",
4959 createdAt: "2026-01-01T00:00:00Z",
4960 });
4961 },
4962 listEnvironmentVariables: (query: { key: string }) => {
4963 calls.push(["listEnvironmentVariables", query]);
4964 return Effect.succeed(
4965 byKey.get(query.key) ? [byKey.get(query.key)] : [],
4966 );
4967 },
4968 createEnvironmentVariable: (input: unknown) => {
4969 calls.push(["createEnvironmentVariable", input]);
4970 return Effect.succeed({
4971 id: "env-created",
4972 type: "environment-variable" as const,
4973 url: "https://api.prisma.test/v1/environment-variables/env-created",
4974 projectId: "project-1",
4975 branchId: null,
4976 class: "production" as const,
4977 key: "DATABASE_URL",
4978 valueKid: "kid-created",
4979 isManagedBySystem: false,
4980 createdAt: "2026-01-01T00:00:00Z",
4981 updatedAt: "2026-01-01T00:00:00Z",
4982 });
4983 },
4984 deleteEnvironmentVariable: (id: string) => {
4985 calls.push(["deleteEnvironmentVariable", id]);
4986 return Effect.void;
4987 },
4988 createAppDeployment: (appId: string, input: unknown) => {
4989 calls.push(["createAppDeployment", { appId, input }]);
4990 return Effect.succeed({
4991 id: "version-1",
4992 type: "deployment" as const,
4993 url: "https://api.prisma.test/v1/deployments/version-1",
4994 foundryVersionId: "foundry-1",
4995 uploadUrl: null,
4996 });
4997 },
4998 getDeployment: (id: string) => {
4999 calls.push(["getDeployment", id]);
5000 return Effect.succeed({
5001 id,
5002 type: "deployment" as const,
5003 serviceId: "service-1",
5004 url: "https://api.prisma.test/v1/deployments/version-1",
5005 foundryVersionId: "foundry-1",
5006 status: "new",
5007 previewDomain: "version-1.preview.prisma.build",
5008 createdAt: "2026-01-01T00:00:00Z",
5009 });
5010 },
5011 } as unknown as PrismaManagementClient;
5012
5013 return Effect.gen(function* () {
5014 const provider = yield* Compute.Provider;
5015 const output = yield* provider.reconcile({
5016 id: "App",
5017 fqn: "App",
5018 instanceId: "00000000000000000000000000000000",
5019 news: {
5020 project: "project-1",
5021 appName: "api",
5022 branchId: "branch-main",
5023 skipCodeUpload: true,
5024 start: false,
5025 skipPromote: true,
5026 },
5027 olds: undefined,
5028 output: {
5029 appId: "service-1",
5030 deploymentId: undefined,
5031 projectId: "project-1",
5032 appName: "api",
5033 regionId: "us-east-1",
5034 deploymentEndpointDomain: undefined,
5035 deploymentUrl: undefined,
5036 appEndpointDomain: "api.prisma.build",
5037 url: "https://api.prisma.build",
5038 promoted: false,
5039 previousDeploymentId: undefined,
5040 previousDeploymentAction: undefined,
5041 environmentKeys: ["DATABASE_URL", "OLD_BOUND_FLAG"],
5042 environmentVariableIds: {
5043 OLD_BOUND_FLAG: "env-old-bound-flag",
5044 },
5045 artifactHash: undefined,
5046 local: false,
5047 },
5048 session: undefined as never,
5049 bindings: [
5050 {
5051 sid: "Connection",
5052 data: {
5053 env: {
5054 DATABASE_URL: Redacted.make("postgres://still-bound"),
5055 },
5056 },
5057 },
5058 ],
5059 });
5060
5061 expect(output.environmentKeys).toEqual(["DATABASE_URL"]);
5062 expect(calls).toContainEqual([
5063 "deleteEnvironmentVariable",
5064 "env-old-bound-flag",
5065 ]);
5066 }).pipe(
5067 Effect.provide(computeProviderLive()),
5068 Effect.provide(
5069 Layer.succeed(PrismaClient, withDefaultBranch(client)),
5070 ),
5071 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
5072 Effect.provide(FetchHttpClient.layer),
5073 Effect.provide(PlatformServices),
5074 );
5075 },
5076 { tags: ["provider:prisma:branch"] },
5077 );
5078
5079 it.effect(
5080 "deletes persisted env keys even when old props contain null tombstones",
5081 () => {
5082 const calls: Array<[string, unknown?]> = [];
5083 const staleVariable = {
5084 id: "env-stale-flag",
5085 type: "environment-variable" as const,
5086 url: "https://api.prisma.test/v1/environment-variables/env-stale-flag",
5087 projectId: "project-1",
5088 branchId: null,
5089 class: "production" as const,
5090 key: "STALE_FLAG",
5091 valueKid: "kid-stale",
5092 isManagedBySystem: false,
5093 createdAt: "2026-01-01T00:00:00Z",
5094 updatedAt: "2026-01-01T00:00:00Z",
5095 };
5096 const client = {
5097 listEnvironmentVariables: (query: { key: string }) => {
5098 calls.push(["listEnvironmentVariables", query]);
5099 return Effect.succeed(
5100 query.key === "STALE_FLAG" ? [staleVariable] : [],
5101 );
5102 },
5103 deleteEnvironmentVariable: (id: string) => {
5104 calls.push(["deleteEnvironmentVariable", id]);
5105 return Effect.void;
5106 },
5107 listAppDeployments: (appId: string, query: unknown) => {
5108 calls.push(["listAppDeployments", { appId, query }]);
5109 return Effect.succeed([]);
5110 },
5111 deleteApp: (id: string) => {
5112 calls.push(["deleteApp", id]);
5113 return Effect.void;
5114 },
5115 } as unknown as PrismaManagementClient;
5116
5117 return Effect.gen(function* () {
5118 const provider = yield* Compute.Provider;
5119 yield* provider.delete({
5120 id: "App",
5121 fqn: "App",
5122 instanceId: "00000000000000000000000000000000",
5123 olds: {
5124 project: "project-1",
5125 appName: "api",
5126 env: {
5127 STALE_FLAG: null,
5128 },
5129 },
5130 output: {
5131 appId: "service-1",
5132 deploymentId: undefined,
5133 projectId: "project-1",
5134 appName: "api",
5135 regionId: "us-east-1",
5136 deploymentEndpointDomain: undefined,
5137 deploymentUrl: undefined,
5138 appEndpointDomain: "api.prisma.build",
5139 url: "https://api.prisma.build",
5140 promoted: false,
5141 previousDeploymentId: undefined,
5142 previousDeploymentAction: undefined,
5143 environmentKeys: ["STALE_FLAG"],
5144 environmentVariableIds: {
5145 STALE_FLAG: "env-stale-flag",
5146 },
5147 environmentClass: "production",
5148 artifactHash: undefined,
5149 local: false,
5150 },
5151 session: undefined as never,
5152 bindings: [],
5153 });
5154
5155 expect(calls).toEqual([
5156 [
5157 "listEnvironmentVariables",
5158 {
5159 projectId: "project-1",
5160 class: "production",
5161 key: "STALE_FLAG",
5162 limit: "100",
5163 },
5164 ],
5165 ["deleteEnvironmentVariable", "env-stale-flag"],
5166 ["deleteApp", "service-1"],
5167 ]);
5168 }).pipe(
5169 Effect.provide(computeProviderLive()),
5170 Effect.provide(
5171 Layer.succeed(PrismaClient, withDefaultBranch(client)),
5172 ),
5173 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
5174 );
5175 },
5176 { tags: ["provider:prisma:branch"] },
5177 );
5178
5179 it.effect(
5180 "does not expose redacted env values in Compute outputs",
5181 () => {
5182 const calls: Array<[string, unknown]> = [];
5183 const client = {
5184 getApp: (id: string) => {
5185 calls.push(["getApp", id]);
5186 return Effect.succeed({
5187 id,
5188 type: "app" as const,
5189 url: "https://api.prisma.test/v1/services/service-1",
5190 name: "api",
5191 region: { id: "us-east-1", name: "US East" },
5192 projectId: "project-1",
5193 branchId: "branch-main",
5194 latestDeploymentId: "version-old",
5195 appEndpointDomain: "api.prisma.build",
5196 createdAt: "2026-01-01T00:00:00Z",
5197 });
5198 },
5199 listEnvironmentVariables: (query: unknown) => {
5200 calls.push(["listEnvironmentVariables", query]);
5201 return Effect.succeed([]);
5202 },
5203 createEnvironmentVariable: (input: unknown) => {
5204 calls.push(["createEnvironmentVariable", input]);
5205 return Effect.succeed({
5206 id: "env-secret",
5207 type: "environment-variable" as const,
5208 url: "https://api.prisma.test/v1/environment-variables/env-secret",
5209 projectId: "project-1",
5210 branchId: null,
5211 class: "production" as const,
5212 key: "SECRET",
5213 valueKid: "kid-secret",
5214 isManagedBySystem: false,
5215 createdAt: "2026-01-01T00:00:00Z",
5216 updatedAt: "2026-01-01T00:00:00Z",
5217 });
5218 },
5219 createAppDeployment: (appId: string, input: unknown) => {
5220 calls.push(["createAppDeployment", { appId, input }]);
5221 return Effect.succeed({
5222 id: "version-new",
5223 type: "deployment" as const,
5224 url: "https://api.prisma.test/v1/deployments/version-new",
5225 foundryVersionId: "foundry-new",
5226 uploadUrl: null,
5227 });
5228 },
5229 getDeployment: (id: string) => {
5230 calls.push(["getDeployment", id]);
5231 return Effect.succeed({
5232 id,
5233 type: "deployment" as const,
5234 serviceId: "service-1",
5235 url: `https://api.prisma.test/v1/deployments/${id}`,
5236 foundryVersionId: "foundry-new",
5237 status: "new",
5238 previewDomain: "version-new.preview.prisma.build",
5239 createdAt: "2026-01-01T00:00:00Z",
5240 });
5241 },
5242 } as unknown as PrismaManagementClient;
5243
5244 return Effect.gen(function* () {
5245 const provider = yield* Compute.Provider;
5246 const output = yield* provider.reconcile({
5247 id: "App",
5248 fqn: "App",
5249 instanceId: "00000000000000000000000000000000",
5250 news: {
5251 project: "project-1",
5252 appName: "api",
5253 branchId: "branch-main",
5254 skipCodeUpload: true,
5255 start: false,
5256 skipPromote: true,
5257 env: {
5258 SECRET: Redacted.make("super-secret"),
5259 },
5260 },
5261 olds: undefined,
5262 output: {
5263 appId: "service-1",
5264 deploymentId: "version-old",
5265 projectId: "project-1",
5266 appName: "api",
5267 regionId: "us-east-1",
5268 deploymentEndpointDomain: "version-old.preview.prisma.build",
5269 deploymentUrl: "https://version-old.preview.prisma.build",
5270 appEndpointDomain: "api.prisma.build",
5271 url: "https://api.prisma.build",
5272 promoted: true,
5273 previousDeploymentId: undefined,
5274 previousDeploymentAction: undefined,
5275 artifactHash: Redacted.make("old-hash"),
5276 local: false,
5277 },
5278 session: undefined as never,
5279 bindings: [],
5280 });
5281
5282 expect(output.deploymentId).toBe("version-new");
5283 expect(Redacted.value(output.artifactHash!)).toMatch(
5284 /^[a-f0-9]{64}$/,
5285 );
5286 expect(JSON.stringify(output)).not.toContain("super-secret");
5287 expect(calls).toContainEqual([
5288 "createEnvironmentVariable",
5289 {
5290 projectId: "project-1",
5291 class: "production",
5292 key: "SECRET",
5293 value: "super-secret",
5294 },
5295 ]);
5296 }).pipe(
5297 Effect.provide(computeProviderLive()),
5298 Effect.provide(
5299 Layer.succeed(PrismaClient, withDefaultBranch(client)),
5300 ),
5301 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
5302 Effect.provide(FetchHttpClient.layer),
5303 Effect.provide(PlatformServices),
5304 );
5305 },
5306 { tags: ["provider:prisma:branch"] },
5307 );
5308
5309 it.effect(
5310 "runs a build command and uploads the built archive",
5311 () => {
5312 const calls: Array<[string, unknown]> = [];
5313 let latestDeploymentId: string | null = null;
5314 let uploaded:
5315 | { url: string; contentType: string | undefined; bytes: Uint8Array }
5316 | undefined;
5317 const client = {
5318 listApps: (projectId: string, query: unknown) => {
5319 calls.push(["listApps", { projectId, query }]);
5320 return Effect.succeed([]);
5321 },
5322 createApp: (projectId: string, input: unknown) => {
5323 calls.push(["createApp", { projectId, input }]);
5324 return Effect.succeed({
5325 id: "service-1",
5326 type: "app" as const,
5327 url: "https://api.prisma.test/v1/services/service-1",
5328 name: "api",
5329 region: { id: "us-east-1", name: "US East" },
5330 projectId,
5331 branchId: "branch-main",
5332 latestDeploymentId,
5333 appEndpointDomain: "api.prisma.build",
5334 createdAt: "2026-01-01T00:00:00Z",
5335 });
5336 },
5337 updateApp: (id: string, input: unknown) => {
5338 calls.push(["updateApp", { id, input }]);
5339 return Effect.succeed({
5340 id,
5341 type: "app" as const,
5342 url: "https://api.prisma.test/v1/services/service-1",
5343 name: "api",
5344 region: { id: "us-east-1", name: "US East" },
5345 projectId: "project-1",
5346 branchId: "branch-main",
5347 latestDeploymentId,
5348 appEndpointDomain: "api.prisma.build",
5349 createdAt: "2026-01-01T00:00:00Z",
5350 });
5351 },
5352 getApp: (id: string) =>
5353 Effect.succeed({
5354 id,
5355 type: "app" as const,
5356 url: "https://api.prisma.test/v1/services/service-1",
5357 name: "api",
5358 region: { id: "us-east-1", name: "US East" },
5359 projectId: "project-1",
5360 branchId: "branch-main",
5361 latestDeploymentId,
5362 appEndpointDomain: "api.prisma.build",
5363 createdAt: "2026-01-01T00:00:00Z",
5364 }),
5365 listEnvironmentVariables: () => Effect.succeed([]),
5366 createAppDeployment: (appId: string, input: unknown) => {
5367 calls.push(["createAppDeployment", { appId, input }]);
5368 return Effect.succeed({
5369 id: "version-1",
5370 type: "deployment" as const,
5371 url: "https://api.prisma.test/v1/deployments/version-1",
5372 foundryVersionId: "foundry-1",
5373 uploadUrl: "https://upload.prisma.test/artifact.tar.gz",
5374 });
5375 },
5376 getDeployment: (id: string) => {
5377 calls.push(["getDeployment", id]);
5378 return Effect.succeed({
5379 id,
5380 type: "deployment" as const,
5381 serviceId: "service-1",
5382 url: "https://api.prisma.test/v1/deployments/version-1",
5383 foundryVersionId: "foundry-1",
5384 status: "running",
5385 previewDomain: "version-1.preview.prisma.build",
5386 createdAt: "2026-01-01T00:00:00Z",
5387 });
5388 },
5389 promoteApp: (
5390 appId: string,
5391 { deploymentId }: { deploymentId: string },
5392 ) => {
5393 calls.push(["promoteApp", { appId, deploymentId }]);
5394 latestDeploymentId = deploymentId;
5395 return Effect.succeed({
5396 appEndpointDomain: "api.prisma.build",
5397 reassignedDomains: 0,
5398 });
5399 },
5400 } as unknown as PrismaManagementClient;
5401 const http = HttpClient.make((request) =>
5402 Effect.gen(function* () {
5403 const body = request.body as HttpBody.HttpBody;
5404 uploaded = {
5405 url: request.url,
5406 contentType: httpBodyContentType(body),
5407 bytes: yield* readHttpBodyBytes(body),
5408 };
5409 return HttpClientResponse.fromWeb(request, new Response(null));
5410 }),
5411 );
5412
5413 return Effect.gen(function* () {
5414 const fs = yield* FileSystem.FileSystem;
5415 const path = yield* Path.Path;
5416 const root = yield* fs.makeTempDirectory({
5417 prefix: "alchemy-prisma-compute-build-",
5418 });
5419 yield* fs.writeFileString(
5420 path.join(root, "build.sh"),
5421 [
5422 "mkdir -p dist",
5423 'printf \'console.log("%s");\' "$BUILD_GREETING" > dist/server.js',
5424 "",
5425 ].join("\n"),
5426 );
5427
5428 const provider = yield* Compute.Provider;
5429 const output = yield* provider.reconcile({
5430 id: "App",
5431 fqn: "App",
5432 instanceId: "00000000000000000000000000000000",
5433 news: {
5434 project: "project-1",
5435 appName: "api",
5436 path: root,
5437 port: 4567,
5438 verifyUrl: false,
5439 build: {
5440 command: "sh build.sh",
5441 cwd: root,
5442 outdir: "dist",
5443 entrypoint: "server.js",
5444 env: { BUILD_GREETING: "hello-build" },
5445 },
5446 },
5447 olds: undefined,
5448 output: undefined,
5449 session: undefined as never,
5450 bindings: [],
5451 });
5452
5453 expect(output.deploymentId).toBe("version-1");
5454 expect(uploaded?.url).toBe(
5455 "https://upload.prisma.test/artifact.tar.gz",
5456 );
5457 expect(uploaded?.contentType).toBe("application/gzip");
5458 const tarText = new TextDecoder().decode(
5459 yield* Effect.sync(() => gunzipSync(uploaded!.bytes)),
5460 );
5461 expect(tarText).toContain("compute.manifest.json");
5462 expect(tarText).toContain("bundle/server.js");
5463 expect(tarText).toContain("hello-build");
5464 expect(calls).toContainEqual([
5465 "createAppDeployment",
5466 {
5467 appId: "service-1",
5468 input: {
5469 portMapping: { http: 4567 },
5470 skipCodeUpload: undefined,
5471 },
5472 },
5473 ]);
5474 }).pipe(
5475 Effect.provide(computeProviderLive()),
5476 Effect.provide(
5477 Layer.succeed(PrismaClient, withDefaultBranch(client)),
5478 ),
5479 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
5480 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
5481 Effect.provide(PlatformServices),
5482 );
5483 },
5484 { tags: ["provider:prisma:branch"] },
5485 );
5486
5487 it.effect(
5488 "auto-builds a Bun app before uploading",
5489 () => {
5490 const calls: Array<[string, unknown]> = [];
5491 let uploaded:
5492 | { url: string; contentType: string | undefined; bytes: Uint8Array }
5493 | undefined;
5494 const client = {
5495 listApps: (projectId: string, query: unknown) => {
5496 calls.push(["listApps", { projectId, query }]);
5497 return Effect.succeed([]);
5498 },
5499 createApp: (projectId: string, input: unknown) => {
5500 calls.push(["createApp", { projectId, input }]);
5501 return Effect.succeed({
5502 id: "service-1",
5503 type: "app" as const,
5504 url: "https://api.prisma.test/v1/services/service-1",
5505 name: "api",
5506 region: { id: "us-east-1", name: "US East" },
5507 projectId,
5508 branchId: "branch-main",
5509 latestDeploymentId: null,
5510 appEndpointDomain: "api.prisma.build",
5511 createdAt: "2026-01-01T00:00:00Z",
5512 });
5513 },
5514 listEnvironmentVariables: () => Effect.succeed([]),
5515 createAppDeployment: (appId: string, input: unknown) => {
5516 calls.push(["createAppDeployment", { appId, input }]);
5517 return Effect.succeed({
5518 id: "version-1",
5519 type: "deployment" as const,
5520 url: "https://api.prisma.test/v1/deployments/version-1",
5521 foundryVersionId: "foundry-1",
5522 uploadUrl: "https://upload.prisma.test/auto.tar.gz",
5523 });
5524 },
5525 getDeployment: (id: string) => {
5526 calls.push(["getDeployment", id]);
5527 return Effect.succeed({
5528 id,
5529 type: "deployment" as const,
5530 serviceId: "service-1",
5531 url: "https://api.prisma.test/v1/deployments/version-1",
5532 foundryVersionId: "foundry-1",
5533 status: "new",
5534 previewDomain: "version-1.preview.prisma.build",
5535 createdAt: "2026-01-01T00:00:00Z",
5536 });
5537 },
5538 } as unknown as PrismaManagementClient;
5539 const http = HttpClient.make((request) =>
5540 Effect.gen(function* () {
5541 const body = request.body as HttpBody.HttpBody;
5542 uploaded = {
5543 url: request.url,
5544 contentType: httpBodyContentType(body),
5545 bytes: yield* readHttpBodyBytes(body),
5546 };
5547 return HttpClientResponse.fromWeb(request, new Response(null));
5548 }),
5549 );
5550
5551 return Effect.gen(function* () {
5552 const fs = yield* FileSystem.FileSystem;
5553 const path = yield* Path.Path;
5554 const root = yield* fs.makeTempDirectory({
5555 prefix: "alchemy-prisma-compute-auto-",
5556 });
5557 yield* fs.makeDirectory(path.join(root, "src"));
5558 yield* fs.writeFileString(
5559 path.join(root, "package.json"),
5560 JSON.stringify({ main: "src/server.ts" }),
5561 );
5562 yield* fs.writeFileString(
5563 path.join(root, "src", "server.ts"),
5564 "console.log('auto app');",
5565 );
5566
5567 const provider = yield* Compute.Provider;
5568 const output = yield* provider.reconcile({
5569 id: "App",
5570 fqn: "App",
5571 instanceId: "00000000000000000000000000000000",
5572 news: {
5573 project: "project-1",
5574 appName: "api",
5575 path: root,
5576 build: "auto",
5577 start: false,
5578 skipPromote: true,
5579 },
5580 olds: undefined,
5581 output: undefined,
5582 session: undefined as never,
5583 bindings: [],
5584 });
5585
5586 expect(output.deploymentId).toBe("version-1");
5587 expect(uploaded?.url).toBe("https://upload.prisma.test/auto.tar.gz");
5588 expect(uploaded?.contentType).toBe("application/gzip");
5589 const tarText = new TextDecoder().decode(
5590 yield* Effect.sync(() => gunzipSync(uploaded!.bytes)),
5591 );
5592 expect(tarText).toContain("bundle/server.js");
5593 expect(tarText).toContain("auto app");
5594 expect(calls).toContainEqual([
5595 "createAppDeployment",
5596 {
5597 appId: "service-1",
5598 input: {
5599 portMapping: { http: 8080 },
5600 skipCodeUpload: undefined,
5601 },
5602 },
5603 ]);
5604 }).pipe(
5605 Effect.provide(computeProviderLive()),
5606 Effect.provide(
5607 Layer.succeed(PrismaClient, withDefaultBranch(client)),
5608 ),
5609 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
5610 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
5611 Effect.provide(PlatformServices),
5612 );
5613 },
5614 { tags: ["provider:prisma:branch"] },
5615 );
5616
5617 it.effect(
5618 "uses framework auto-build default ports in Compute",
5619 () => {
5620 const calls: Array<[string, unknown]> = [];
5621 const client = {
5622 listApps: (projectId: string, query: unknown) => {
5623 calls.push(["listApps", { projectId, query }]);
5624 return Effect.succeed([]);
5625 },
5626 createApp: (projectId: string, input: unknown) => {
5627 calls.push(["createApp", { projectId, input }]);
5628 return Effect.succeed({
5629 id: "service-1",
5630 type: "app" as const,
5631 url: "https://api.prisma.test/v1/services/service-1",
5632 name: "web",
5633 region: { id: "us-east-1", name: "US East" },
5634 projectId,
5635 branchId: "branch-main",
5636 latestDeploymentId: null,
5637 appEndpointDomain: "web.prisma.build",
5638 createdAt: "2026-01-01T00:00:00Z",
5639 });
5640 },
5641 listEnvironmentVariables: () => Effect.succeed([]),
5642 createAppDeployment: (appId: string, input: unknown) => {
5643 calls.push(["createAppDeployment", { appId, input }]);
5644 return Effect.succeed({
5645 id: "version-1",
5646 type: "deployment" as const,
5647 url: "https://api.prisma.test/v1/deployments/version-1",
5648 foundryVersionId: "foundry-1",
5649 uploadUrl: "https://upload.prisma.test/next.tar.gz",
5650 });
5651 },
5652 getDeployment: (id: string) => {
5653 calls.push(["getDeployment", id]);
5654 return Effect.succeed({
5655 id,
5656 type: "deployment" as const,
5657 serviceId: "service-1",
5658 url: "https://api.prisma.test/v1/deployments/version-1",
5659 foundryVersionId: "foundry-1",
5660 status: "new",
5661 previewDomain: "version-1.preview.prisma.build",
5662 createdAt: "2026-01-01T00:00:00Z",
5663 });
5664 },
5665 } as unknown as PrismaManagementClient;
5666 const http = HttpClient.make((request) =>
5667 Effect.succeed(
5668 HttpClientResponse.fromWeb(request, new Response(null)),
5669 ),
5670 );
5671
5672 return Effect.gen(function* () {
5673 const fs = yield* FileSystem.FileSystem;
5674 const path = yield* Path.Path;
5675 const root = yield* fs.makeTempDirectory({
5676 prefix: "alchemy-prisma-compute-auto-next-",
5677 });
5678 const binDir = path.join(root, "node_modules", ".bin");
5679 const nextBin = path.join(binDir, "next");
5680 yield* fs.makeDirectory(binDir, { recursive: true });
5681 yield* fs.writeFileString(
5682 path.join(root, "package.json"),
5683 JSON.stringify({ dependencies: { next: "0.0.0-test" } }),
5684 );
5685 yield* fs.writeFileString(
5686 nextBin,
5687 [
5688 "#!/bin/sh",
5689 "mkdir -p .next/standalone",
5690 "printf 'next server' > .next/standalone/server.js",
5691 "",
5692 ].join("\n"),
5693 );
5694 yield* fs.chmod(nextBin, 0o755);
5695
5696 const provider = yield* Compute.Provider;
5697 const output = yield* provider.reconcile({
5698 id: "App",
5699 fqn: "App",
5700 instanceId: "00000000000000000000000000000000",
5701 news: {
5702 project: "project-1",
5703 appName: "web",
5704 path: root,
5705 build: { type: "auto", framework: "nextjs" },
5706 start: false,
5707 skipPromote: true,
5708 },
5709 olds: undefined,
5710 output: undefined,
5711 session: undefined as never,
5712 bindings: [],
5713 });
5714
5715 expect(output.deploymentId).toBe("version-1");
5716 expect(calls).toContainEqual([
5717 "createAppDeployment",
5718 {
5719 appId: "service-1",
5720 input: {
5721 portMapping: { http: 3000 },
5722 skipCodeUpload: undefined,
5723 },
5724 },
5725 ]);
5726 }).pipe(
5727 Effect.provide(computeProviderLive()),
5728 Effect.provide(
5729 Layer.succeed(PrismaClient, withDefaultBranch(client)),
5730 ),
5731 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
5732 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
5733 Effect.provide(PlatformServices),
5734 );
5735 },
5736 { tags: ["provider:prisma:branch"] },
5737 );
5738
5739 it.effect(
5740 "refuses to claim a foreign service after a create conflict",
5741 () => {
5742 const calls: Array<[string, unknown]> = [];
5743 let serviceListCount = 0;
5744 const service = {
5745 id: "service-1",
5746 type: "app" as const,
5747 url: "https://api.prisma.test/v1/services/service-1",
5748 name: "api",
5749 region: { id: "us-east-1", name: "US East" },
5750 projectId: "project-1",
5751 branchId: "branch-main",
5752 latestDeploymentId: null,
5753 appEndpointDomain: "api.prisma.build",
5754 createdAt: "2026-01-01T00:00:00Z",
5755 };
5756
5757 const client = {
5758 listApps: (projectId: string, query: unknown) =>
5759 Effect.sync(() => {
5760 serviceListCount += 1;
5761 calls.push(["listApps", { projectId, query }]);
5762 return serviceListCount === 1 ? [] : [service];
5763 }),
5764 createApp: (projectId: string, input: unknown) =>
5765 Effect.gen(function* () {
5766 calls.push(["createApp", { projectId, input }]);
5767 return yield* Effect.fail(
5768 new PrismaApiError({
5769 method: "POST",
5770 path: `/v1/services`,
5771 status: 409,
5772 message: "already exists",
5773 }),
5774 );
5775 }),
5776 listBranches: (projectId: string, query: unknown) => {
5777 calls.push(["listBranches", { projectId, query }]);
5778 return Effect.succeed([
5779 {
5780 id: "branch-main",
5781 type: "branch" as const,
5782 url: "https://api.prisma.test/v1/branches/branch-main",
5783 gitName: "main",
5784 isDefault: true,
5785 createdAt: "2026-01-01T00:00:00Z",
5786 updatedAt: "2026-01-01T00:00:00Z",
5787 project: {
5788 id: "project-1",
5789 url: "https://api.prisma.test/v1/projects/project-1",
5790 name: "project",
5791 },
5792 },
5793 ]);
5794 },
5795 updateApp: (id: string, input: unknown) => {
5796 calls.push(["updateApp", { id, input }]);
5797 return Effect.succeed({ ...service, branchId: "branch-main" });
5798 },
5799 listEnvironmentVariables: (query: unknown) => {
5800 calls.push(["listEnvironmentVariables", query]);
5801 return Effect.succeed([]);
5802 },
5803 createAppDeployment: (appId: string, input: unknown) => {
5804 calls.push(["createAppDeployment", { appId, input }]);
5805 return Effect.succeed({
5806 id: "version-1",
5807 type: "deployment" as const,
5808 url: "https://api.prisma.test/v1/deployments/version-1",
5809 foundryVersionId: "foundry-1",
5810 uploadUrl: "https://upload.prisma.test/version-1.tar.gz",
5811 });
5812 },
5813 getDeployment: (id: string) => {
5814 calls.push(["getDeployment", id]);
5815 return Effect.succeed({
5816 id,
5817 type: "deployment" as const,
5818 serviceId: "service-1",
5819 url: "https://api.prisma.test/v1/deployments/version-1",
5820 foundryVersionId: "foundry-1",
5821 status: "new",
5822 previewDomain: null,
5823 createdAt: "2026-01-01T00:00:00Z",
5824 });
5825 },
5826 } as unknown as PrismaManagementClient;
5827 const http = HttpClient.make((request) =>
5828 Effect.succeed(
5829 HttpClientResponse.fromWeb(request, new Response(null)),
5830 ),
5831 );
5832
5833 return Effect.gen(function* () {
5834 const provider = yield* Compute.Provider;
5835 const error = yield* provider
5836 .reconcile({
5837 id: "App",
5838 fqn: "App",
5839 instanceId: "00000000000000000000000000000000",
5840 news: {
5841 project: "project-1",
5842 appName: "api",
5843 artifactPath: fixtureArtifactPath,
5844 start: false,
5845 skipPromote: true,
5846 },
5847 olds: undefined,
5848 output: undefined,
5849 session: undefined as never,
5850 bindings: [],
5851 })
5852 .pipe(Effect.flip);
5853
5854 expect(error.message).toContain("is not owned");
5855 expect(calls.some(([name]) => name === "createAppDeployment")).toBe(
5856 false,
5857 );
5858 }).pipe(
5859 Effect.provide(computeProviderLive()),
5860 Effect.provide(
5861 Layer.succeed(PrismaClient, withDefaultBranch(client)),
5862 ),
5863 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
5864 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
5865 Effect.provide(PlatformServices),
5866 );
5867 },
5868 { tags: ["provider:prisma:branch"] },
5869 );
5870
5871 it.effect(
5872 "reconciles deploy updates and destroys old deployments",
5873 () => {
5874 const calls: Array<[string, unknown]> = [];
5875 const versions = new Map<string, "new" | "running" | "stopped">();
5876 let latestDeploymentId: string | null = null;
5877 let versionCounter = 0;
5878
5879 const service = () => ({
5880 id: "service-1",
5881 type: "app" as const,
5882 url: "https://api.prisma.test/v1/services/service-1",
5883 name: "api",
5884 region: { id: "us-east-1", name: "US East" },
5885 projectId: "project-1",
5886 branchId: "branch-main",
5887 latestDeploymentId,
5888 appEndpointDomain: "api.prisma.build",
5889 createdAt: "2026-01-01T00:00:00Z",
5890 });
5891
5892 const version = (id: string) => ({
5893 id,
5894 type: "deployment" as const,
5895 serviceId: "service-1",
5896 url: `https://api.prisma.test/v1/deployments/${id}`,
5897 foundryVersionId: `foundry-${id}`,
5898 status: versions.get(id) ?? "new",
5899 previewDomain: `${id}.preview.prisma.build`,
5900 createdAt: "2026-01-01T00:00:00Z",
5901 });
5902
5903 const client = {
5904 listApps: (projectId: string, query: unknown) => {
5905 calls.push(["listApps", { projectId, query }]);
5906 return Effect.succeed([]);
5907 },
5908 getApp: (id: string) => {
5909 calls.push(["getApp", id]);
5910 return Effect.succeed(service());
5911 },
5912 listBranches: (projectId: string, query: unknown) => {
5913 calls.push(["listBranches", { projectId, query }]);
5914 return Effect.succeed([
5915 {
5916 id: "branch-main",
5917 type: "branch" as const,
5918 url: "https://api.prisma.test/v1/branches/branch-main",
5919 gitName: "main",
5920 isDefault: true,
5921 createdAt: "2026-01-01T00:00:00Z",
5922 updatedAt: "2026-01-01T00:00:00Z",
5923 project: {
5924 id: "project-1",
5925 url: "https://api.prisma.test/v1/projects/project-1",
5926 name: "project",
5927 },
5928 },
5929 ]);
5930 },
5931 createApp: (projectId: string, input: unknown) => {
5932 calls.push(["createApp", { projectId, input }]);
5933 return Effect.succeed(service());
5934 },
5935 updateApp: (id: string, input: unknown) => {
5936 calls.push(["updateApp", { id, input }]);
5937 return Effect.succeed(service());
5938 },
5939 listEnvironmentVariables: (query: unknown) => {
5940 calls.push(["listEnvironmentVariables", query]);
5941 return Effect.succeed([]);
5942 },
5943 createAppDeployment: (appId: string, input: unknown) =>
5944 Effect.sync(() => {
5945 const id = `version-${++versionCounter}`;
5946 calls.push(["createAppDeployment", { appId, input }]);
5947 versions.set(id, "new");
5948 return {
5949 id,
5950 type: "deployment" as const,
5951 url: `https://api.prisma.test/v1/deployments/${id}`,
5952 foundryVersionId: `foundry-${id}`,
5953 uploadUrl: `https://upload.prisma.test/${id}.tar.gz`,
5954 };
5955 }),
5956 getDeployment: (id: string) => {
5957 calls.push(["getDeployment", id]);
5958 return versions.has(id)
5959 ? Effect.succeed(version(id))
5960 : Effect.fail(
5961 new PrismaApiError({
5962 method: "GET",
5963 path: `/v1/deployments/${id}`,
5964 status: 404,
5965 message: "not found",
5966 }),
5967 );
5968 },
5969 startDeployment: (id: string) =>
5970 Effect.sync(() => {
5971 calls.push(["startDeployment", id]);
5972 versions.set(id, "running");
5973 return { previewDomain: `${id}.preview.prisma.build` };
5974 }),
5975 promoteApp: (
5976 appId: string,
5977 { deploymentId }: { deploymentId: string },
5978 ) =>
5979 Effect.sync(() => {
5980 calls.push(["promoteApp", { appId, deploymentId }]);
5981 latestDeploymentId = deploymentId;
5982 return {
5983 appEndpointDomain: "api.prisma.build",
5984 reassignedDomains: 0,
5985 };
5986 }),
5987 stopDeployment: (id: string) =>
5988 Effect.gen(function* () {
5989 calls.push(["stopDeployment", id]);
5990 if (id === "version-1") {
5991 versions.delete(id);
5992 return yield* Effect.fail(
5993 new PrismaApiError({
5994 method: "POST",
5995 path: `/v1/deployments/${id}/stop`,
5996 status: 404,
5997 message: "not found",
5998 }),
5999 );
6000 }
6001 versions.set(id, "stopped");
6002 }),
6003 deleteDeployment: (id: string) =>
6004 Effect.sync(() => {
6005 calls.push(["deleteDeployment", id]);
6006 versions.delete(id);
6007 }),
6008 listAppDeployments: (appId: string, query: unknown) => {
6009 calls.push(["listAppDeployments", { appId, query }]);
6010 return Effect.succeed(
6011 [...versions.keys()].map((id) => ({
6012 id,
6013 type: "deployment" as const,
6014 serviceId: "service-1",
6015 url: `https://api.prisma.test/v1/deployments/${id}`,
6016 foundryVersionId: `foundry-${id}`,
6017 createdAt: "2026-01-01T00:00:00Z",
6018 })),
6019 );
6020 },
6021 deleteApp: (id: string) =>
6022 Effect.sync(() => {
6023 calls.push(["deleteApp", id]);
6024 versions.clear();
6025 }),
6026 } as unknown as PrismaManagementClient;
6027
6028 const http = HttpClient.make((request) =>
6029 readHttpBodyBytes(request.body as HttpBody.HttpBody).pipe(
6030 Effect.as(HttpClientResponse.fromWeb(request, new Response(null))),
6031 ),
6032 );
6033
6034 return Effect.gen(function* () {
6035 const provider = yield* Compute.Provider;
6036 const first = yield* provider.reconcile({
6037 id: "App",
6038 fqn: "App",
6039 instanceId: "00000000000000000000000000000000",
6040 news: {
6041 project: "project-1",
6042 appName: "api",
6043 artifactPath: fixtureArtifactV1Path,
6044 port: 3000,
6045 },
6046 olds: undefined,
6047 output: undefined,
6048 session: undefined as never,
6049 bindings: [],
6050 });
6051
6052 const callsBeforeSkip = calls.length;
6053 const firstWithSkip = yield* provider.reconcile({
6054 id: "App",
6055 fqn: "App",
6056 instanceId: "00000000000000000000000000000000",
6057 news: {
6058 project: "project-1",
6059 appName: "api",
6060 artifactPath: fixtureArtifactV1Path,
6061 port: 3000,
6062 skipPromote: true,
6063 },
6064 olds: {
6065 project: "project-1",
6066 appName: "api",
6067 artifactPath: fixtureArtifactV1Path,
6068 port: 3000,
6069 },
6070 output: first,
6071 session: undefined as never,
6072 bindings: [],
6073 });
6074 const skipCalls = calls.slice(callsBeforeSkip);
6075 expect(firstWithSkip.deploymentId).toBe("version-1");
6076 expect(firstWithSkip.promoted).toBe(true);
6077 expect(firstWithSkip.url).toBe("https://api.prisma.build");
6078 expect(skipCalls.map(([operation]) => operation)).not.toContain(
6079 "createAppDeployment",
6080 );
6081 expect(skipCalls.map(([operation]) => operation)).not.toContain(
6082 "promoteApp",
6083 );
6084 calls.splice(callsBeforeSkip);
6085
6086 const second = yield* provider.reconcile({
6087 id: "App",
6088 fqn: "App",
6089 instanceId: "00000000000000000000000000000000",
6090 news: {
6091 project: "project-1",
6092 appName: "api",
6093 artifactPath: fixtureArtifactV2Path,
6094 port: 3000,
6095 destroyOldDeployment: true,
6096 },
6097 olds: {
6098 project: "project-1",
6099 appName: "api",
6100 artifactPath: fixtureArtifactV1Path,
6101 port: 3000,
6102 skipPromote: true,
6103 },
6104 output: firstWithSkip,
6105 session: undefined as never,
6106 bindings: [],
6107 });
6108
6109 yield* provider.delete({
6110 id: "App",
6111 fqn: "App",
6112 instanceId: "00000000000000000000000000000000",
6113 olds: {
6114 project: "project-1",
6115 appName: "api",
6116 artifactPath: fixtureArtifactV2Path,
6117 port: 3000,
6118 },
6119 output: second,
6120 session: undefined as never,
6121 bindings: [],
6122 });
6123
6124 expect(first.deploymentId).toBe("version-1");
6125 expect(first.promoted).toBe(true);
6126 expect(second.deploymentId).toBe("version-2");
6127 expect(second.previousDeploymentId).toBe("version-1");
6128 expect(second.previousDeploymentAction).toBe("destroyed");
6129 expect(versions.size).toBe(0);
6130 expect(calls).toEqual([
6131 [
6132 "listBranches",
6133 { projectId: "project-1", query: { limit: "100" } },
6134 ],
6135 [
6136 "createApp",
6137 {
6138 projectId: "project-1",
6139 input: {
6140 displayName: "api",
6141 regionId: undefined,
6142 branchId: "branch-main",
6143 branchGitName: undefined,
6144 },
6145 },
6146 ],
6147 [
6148 "createAppDeployment",
6149 {
6150 appId: "service-1",
6151 input: {
6152 portMapping: { http: 3000 },
6153 skipCodeUpload: undefined,
6154 },
6155 },
6156 ],
6157 ["getDeployment", "version-1"],
6158 ["startDeployment", "version-1"],
6159 ["getDeployment", "version-1"],
6160 ["promoteApp", { appId: "service-1", deploymentId: "version-1" }],
6161 ["getApp", "service-1"],
6162 ["getApp", "service-1"],
6163 [
6164 "listBranches",
6165 { projectId: "project-1", query: { limit: "100" } },
6166 ],
6167 ["getDeployment", "version-1"],
6168 [
6169 "createAppDeployment",
6170 {
6171 appId: "service-1",
6172 input: {
6173 portMapping: { http: 3000 },
6174 skipCodeUpload: undefined,
6175 },
6176 },
6177 ],
6178 ["getDeployment", "version-2"],
6179 ["startDeployment", "version-2"],
6180 ["getDeployment", "version-2"],
6181 ["promoteApp", { appId: "service-1", deploymentId: "version-2" }],
6182 ["getApp", "service-1"],
6183 ["getDeployment", "version-1"],
6184 ["getDeployment", "version-1"],
6185 ["stopDeployment", "version-1"],
6186 ["getDeployment", "version-1"],
6187 ["deleteDeployment", "version-1"],
6188 ["deleteApp", "service-1"],
6189 ]);
6190 }).pipe(
6191 Effect.provide(computeProviderLive()),
6192 Effect.provide(
6193 Layer.succeed(PrismaClient, withDefaultBranch(client)),
6194 ),
6195 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
6196 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
6197 Effect.provide(PlatformServices),
6198 );
6199 },
6200 { tags: ["provider:prisma:branch"] },
6201 );
6202
6203 it.effect(
6204 "creates a no-upload version for skipCodeUpload env updates",
6205 () => {
6206 const calls: Array<[string, unknown]> = [];
6207 const featureEnv = {
6208 id: "env-feature",
6209 type: "environment-variable" as const,
6210 url: "https://api.prisma.test/v1/environment-variables/env-feature",
6211 projectId: "project-1",
6212 branchId: null,
6213 class: "production" as const,
6214 key: "FEATURE",
6215 valueKid: "kid-feature",
6216 isManagedBySystem: false,
6217 createdAt: "2026-01-01T00:00:00Z",
6218 updatedAt: "2026-01-01T00:00:00Z",
6219 };
6220 const client = {
6221 getApp: (id: string) => {
6222 calls.push(["getApp", id]);
6223 return Effect.succeed({
6224 id,
6225 type: "app" as const,
6226 url: "https://api.prisma.test/v1/services/service-1",
6227 name: "api",
6228 region: { id: "us-east-1", name: "US East" },
6229 projectId: "project-1",
6230 branchId: "branch-main",
6231 latestDeploymentId: "version-old",
6232 appEndpointDomain: "api.prisma.build",
6233 createdAt: "2026-01-01T00:00:00Z",
6234 });
6235 },
6236 listEnvironmentVariables: (query: { key: string }) => {
6237 calls.push(["listEnvironmentVariables", query]);
6238 return Effect.succeed(query.key === "FEATURE" ? [featureEnv] : []);
6239 },
6240 updateEnvironmentVariable: (id: string, input: unknown) => {
6241 calls.push(["updateEnvironmentVariable", { id, input }]);
6242 return Effect.succeed(featureEnv);
6243 },
6244 createAppDeployment: (appId: string, input: unknown) => {
6245 calls.push(["createAppDeployment", { appId, input }]);
6246 return Effect.succeed({
6247 id: "version-new",
6248 type: "deployment" as const,
6249 url: "https://api.prisma.test/v1/deployments/version-new",
6250 foundryVersionId: "foundry-new",
6251 uploadUrl: null,
6252 });
6253 },
6254 getDeployment: (id: string) => {
6255 calls.push(["getDeployment", id]);
6256 return Effect.succeed({
6257 id,
6258 type: "deployment" as const,
6259 serviceId: "service-1",
6260 url: `https://api.prisma.test/v1/deployments/${id}`,
6261 foundryVersionId: "foundry-new",
6262 status: "new",
6263 previewDomain: "version-new.preview.prisma.build",
6264 createdAt: "2026-01-01T00:00:00Z",
6265 });
6266 },
6267 } as unknown as PrismaManagementClient;
6268
6269 return Effect.gen(function* () {
6270 const provider = yield* Compute.Provider;
6271 const output = yield* provider.reconcile({
6272 id: "App",
6273 fqn: "App",
6274 instanceId: "00000000000000000000000000000000",
6275 news: {
6276 project: "project-1",
6277 appName: "api",
6278 branchId: "branch-main",
6279 skipCodeUpload: true,
6280 start: false,
6281 skipPromote: true,
6282 env: {
6283 FEATURE: "on",
6284 },
6285 },
6286 olds: {
6287 project: "project-1",
6288 appName: "api",
6289 branchId: "branch-main",
6290 skipCodeUpload: true,
6291 start: false,
6292 skipPromote: true,
6293 env: {
6294 FEATURE: "off",
6295 },
6296 },
6297 output: {
6298 appId: "service-1",
6299 deploymentId: "version-old",
6300 projectId: "project-1",
6301 appName: "api",
6302 regionId: "us-east-1",
6303 deploymentEndpointDomain: "version-old.preview.prisma.build",
6304 deploymentUrl: "https://version-old.preview.prisma.build",
6305 appEndpointDomain: "api.prisma.build",
6306 url: "https://api.prisma.build",
6307 promoted: true,
6308 previousDeploymentId: undefined,
6309 previousDeploymentAction: undefined,
6310 environmentVariableIds: { FEATURE: "env-feature" },
6311 artifactHash: Redacted.make("old-hash"),
6312 local: false,
6313 },
6314 session: undefined as never,
6315 bindings: [],
6316 });
6317
6318 expect(output.deploymentId).toBe("version-new");
6319 expect(output.previousDeploymentId).toBe("version-old");
6320 expect(output.previousDeploymentAction).toBe("still-active");
6321 expect(calls).toEqual([
6322 ["getApp", "service-1"],
6323 ["getDeployment", "version-old"],
6324 [
6325 "listEnvironmentVariables",
6326 {
6327 projectId: "project-1",
6328 class: "production",
6329 key: "FEATURE",
6330 limit: "100",
6331 },
6332 ],
6333 [
6334 "updateEnvironmentVariable",
6335 { id: "env-feature", input: { value: "on" } },
6336 ],
6337 [
6338 "createAppDeployment",
6339 {
6340 appId: "service-1",
6341 input: { portMapping: { http: 8080 }, skipCodeUpload: true },
6342 },
6343 ],
6344 ["getDeployment", "version-new"],
6345 ]);
6346 }).pipe(
6347 Effect.provide(computeProviderLive()),
6348 Effect.provide(
6349 Layer.succeed(PrismaClient, withDefaultBranch(client)),
6350 ),
6351 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
6352 Effect.provide(FetchHttpClient.layer),
6353 Effect.provide(PlatformServices),
6354 );
6355 },
6356 { tags: ["provider:prisma:branch"] },
6357 );
6358
6359 it.effect(
6360 "replays promotion to repair endpoint drift for a matching deployment",
6361 () => {
6362 const calls: Array<[string, unknown]> = [];
6363 let latestDeploymentId: string | null = null;
6364
6365 const service = () => ({
6366 id: "service-1",
6367 type: "app" as const,
6368 url: "https://api.prisma.test/v1/services/service-1",
6369 name: "api",
6370 region: { id: "us-east-1", name: "US East" },
6371 projectId: "project-1",
6372 branchId: "branch-main",
6373 latestDeploymentId,
6374 appEndpointDomain: "api.prisma.build",
6375 createdAt: "2026-01-01T00:00:00Z",
6376 });
6377
6378 const client = {
6379 listApps: (projectId: string, query: unknown) => {
6380 calls.push(["listApps", { projectId, query }]);
6381 return Effect.succeed([]);
6382 },
6383 createApp: (projectId: string, input: unknown) => {
6384 calls.push(["createApp", { projectId, input }]);
6385 return Effect.succeed(service());
6386 },
6387 getApp: (id: string) => {
6388 calls.push(["getApp", id]);
6389 return Effect.succeed(service());
6390 },
6391 createAppDeployment: (appId: string, input: unknown) => {
6392 calls.push(["createAppDeployment", { appId, input }]);
6393 return Effect.succeed({
6394 id: "version-1",
6395 type: "deployment" as const,
6396 url: "https://api.prisma.test/v1/deployments/version-1",
6397 foundryVersionId: "foundry-1",
6398 uploadUrl: "https://upload.prisma.test/version-1.tar.gz",
6399 });
6400 },
6401 getDeployment: (id: string) => {
6402 calls.push(["getDeployment", id]);
6403 return Effect.succeed({
6404 id,
6405 type: "deployment" as const,
6406 serviceId: "service-1",
6407 url: `https://api.prisma.test/v1/deployments/${id}`,
6408 foundryVersionId: "foundry-1",
6409 status: "running",
6410 previewDomain: "version-1.preview.prisma.build",
6411 createdAt: "2026-01-01T00:00:00Z",
6412 });
6413 },
6414 promoteApp: (
6415 appId: string,
6416 { deploymentId }: { deploymentId: string },
6417 ) =>
6418 Effect.sync(() => {
6419 calls.push(["promoteApp", { appId, deploymentId }]);
6420 latestDeploymentId = deploymentId;
6421 return {
6422 appEndpointDomain: "api.prisma.build",
6423 reassignedDomains: 0,
6424 };
6425 }),
6426 } as unknown as PrismaManagementClient;
6427 const http = HttpClient.make((request) =>
6428 Effect.succeed(
6429 HttpClientResponse.fromWeb(request, new Response(null)),
6430 ),
6431 );
6432
6433 const news = {
6434 project: "project-1",
6435 appName: "api",
6436 branchId: "branch-main",
6437 artifactPath: fixtureArtifactPath,
6438 verifyUrl: false,
6439 };
6440
6441 return Effect.gen(function* () {
6442 const provider = yield* Compute.Provider;
6443 const first = yield* provider.reconcile({
6444 id: "App",
6445 fqn: "App",
6446 instanceId: "00000000000000000000000000000000",
6447 news,
6448 olds: undefined,
6449 output: undefined,
6450 session: undefined as never,
6451 bindings: [],
6452 });
6453
6454 const second = yield* provider.reconcile({
6455 id: "App",
6456 fqn: "App",
6457 instanceId: "00000000000000000000000000000000",
6458 news,
6459 olds: news,
6460 output: first,
6461 session: undefined as never,
6462 bindings: [],
6463 });
6464
6465 expect(first.deploymentId).toBe("version-1");
6466 expect(second.deploymentId).toBe("version-1");
6467 expect(second.previousDeploymentId).toBeNull();
6468 expect(calls.filter(([name]) => name === "promoteApp")).toEqual([
6469 ["promoteApp", { appId: "service-1", deploymentId: "version-1" }],
6470 ["promoteApp", { appId: "service-1", deploymentId: "version-1" }],
6471 ]);
6472 expect(
6473 calls.filter(([name]) => name === "createAppDeployment"),
6474 ).toHaveLength(1);
6475 }).pipe(
6476 Effect.provide(computeProviderLive()),
6477 Effect.provide(
6478 Layer.succeed(PrismaClient, withDefaultBranch(client)),
6479 ),
6480 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
6481 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
6482 Effect.provide(PlatformServices),
6483 );
6484 },
6485 { tags: ["provider:prisma:branch"] },
6486 );
6487
6488 it.effect(
6489 "persists pending cleanup when destroying the old deployment fails",
6490 () => {
6491 const calls: Array<[string, unknown]> = [];
6492 let latestDeploymentId = "version-1";
6493 const client = {
6494 getApp: (id: string) => {
6495 calls.push(["getApp", id]);
6496 return Effect.succeed({
6497 id,
6498 type: "app" as const,
6499 url: `https://api.prisma.test/v1/services/${id}`,
6500 name: "api",
6501 region: { id: "us-east-1", name: "us-east-1" },
6502 projectId: "project-1",
6503 branchId: "branch-main",
6504 latestDeploymentId,
6505 appEndpointDomain: "api.prisma.build",
6506 createdAt: "2026-01-01T00:00:00Z",
6507 });
6508 },
6509 listBranches: (projectId: string, query: unknown) => {
6510 calls.push(["listBranches", { projectId, query }]);
6511 return Effect.succeed([
6512 {
6513 id: "branch-main",
6514 type: "branch" as const,
6515 url: "https://api.prisma.test/v1/branches/branch-main",
6516 gitName: "main",
6517 isDefault: true,
6518 createdAt: "2026-01-01T00:00:00Z",
6519 updatedAt: "2026-01-01T00:00:00Z",
6520 project: {
6521 id: "project-1",
6522 url: "https://api.prisma.test/v1/projects/project-1",
6523 name: "project",
6524 },
6525 },
6526 ]);
6527 },
6528 createAppDeployment: (appId: string, input: unknown) => {
6529 calls.push(["createAppDeployment", { appId, input }]);
6530 return Effect.succeed({
6531 id: "version-2",
6532 type: "deployment" as const,
6533 url: "https://api.prisma.test/v1/deployments/version-2",
6534 foundryVersionId: "foundry-version-2",
6535 uploadUrl: "https://upload.prisma.test/version-2.tar.gz",
6536 });
6537 },
6538 getDeployment: (id: string) => {
6539 calls.push(["getDeployment", id]);
6540 return Effect.succeed({
6541 id,
6542 type: "deployment" as const,
6543 serviceId: "service-1",
6544 url: `https://api.prisma.test/v1/deployments/${id}`,
6545 foundryVersionId: `foundry-${id}`,
6546 status: id === "version-1" ? "stopped" : "running",
6547 previewDomain: `${id}.preview.prisma.build`,
6548 createdAt: "2026-01-01T00:00:00Z",
6549 });
6550 },
6551 startDeployment: (id: string) =>
6552 Effect.sync(() => {
6553 calls.push(["startDeployment", id]);
6554 return { previewDomain: `${id}.preview.prisma.build` };
6555 }),
6556 promoteApp: (
6557 appId: string,
6558 { deploymentId }: { deploymentId: string },
6559 ) =>
6560 Effect.sync(() => {
6561 calls.push(["promoteApp", { appId, deploymentId }]);
6562 latestDeploymentId = deploymentId;
6563 return {
6564 appEndpointDomain: "api.prisma.build",
6565 reassignedDomains: 0,
6566 };
6567 }),
6568 stopDeployment: (id: string) =>
6569 Effect.sync(() => {
6570 calls.push(["stopDeployment", id]);
6571 }),
6572 deleteDeployment: (id: string) =>
6573 Effect.gen(function* () {
6574 calls.push(["deleteDeployment", id]);
6575 return yield* Effect.fail(
6576 new PrismaApiError({
6577 method: "DELETE",
6578 path: `/v1/deployments/${id}`,
6579 status: 400,
6580 message: "Internal Server Error",
6581 }),
6582 );
6583 }),
6584 } as unknown as PrismaManagementClient;
6585
6586 const http = HttpClient.make((request) =>
6587 Effect.succeed(
6588 HttpClientResponse.fromWeb(request, new Response(null)),
6589 ),
6590 );
6591
6592 return Effect.gen(function* () {
6593 const provider = yield* Compute.Provider;
6594 const output = yield* provider.reconcile({
6595 id: "App",
6596 fqn: "App",
6597 instanceId: "00000000000000000000000000000000",
6598 news: {
6599 project: "project-1",
6600 appName: "api",
6601 artifactPath: fixtureArtifactV2Path,
6602 port: 3000,
6603 destroyOldDeployment: true,
6604 },
6605 olds: {
6606 project: "project-1",
6607 appName: "api",
6608 artifactPath: fixtureArtifactV1Path,
6609 port: 3000,
6610 },
6611 output: {
6612 appId: "service-1",
6613 deploymentId: "version-1",
6614 projectId: "project-1",
6615 appName: "api",
6616 regionId: "us-east-1",
6617 deploymentEndpointDomain: "version-1.preview.prisma.build",
6618 deploymentUrl: "https://version-1.preview.prisma.build",
6619 appEndpointDomain: "api.prisma.build",
6620 url: "https://api.prisma.build",
6621 promoted: true,
6622 previousDeploymentId: undefined,
6623 previousDeploymentAction: undefined,
6624 artifactHash: Redacted.make("old-hash"),
6625 local: false,
6626 },
6627 session: undefined as never,
6628 bindings: [],
6629 });
6630
6631 expect(output.deploymentId).toBe("version-2");
6632 expect(output.previousDeploymentAction).toBe("still-active");
6633 expect(output.pendingDeploymentCleanup).toEqual({
6634 deploymentId: "version-1",
6635 action: "destroy",
6636 });
6637 expect(calls).toContainEqual(["deleteDeployment", "version-1"]);
6638 }).pipe(
6639 Effect.provide(computeProviderLive()),
6640 Effect.provide(
6641 Layer.succeed(PrismaClient, withDefaultBranch(client)),
6642 ),
6643 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
6644 Effect.provideService(HttpClient.HttpClient, http),
6645 Effect.provide(PlatformServices),
6646 );
6647 },
6648 { tags: ["provider:prisma:branch"] },
6649 );
6650
6651 it.effect(
6652 "preserves a newly created deployment when promotion commit is ambiguous",
6653 () => {
6654 const calls: Array<[string, unknown]> = [];
6655 const versions = new Map([["version-new", "new"]]);
6656 const client = {
6657 getApp: (id: string) => {
6658 calls.push(["getApp", id]);
6659 return Effect.succeed({
6660 id,
6661 type: "app" as const,
6662 url: `https://api.prisma.test/v1/services/${id}`,
6663 name: "api",
6664 region: { id: "us-east-1", name: "US East" },
6665 projectId: "project-1",
6666 branchId: "branch-main",
6667 latestDeploymentId: null,
6668 appEndpointDomain: "api.prisma.build",
6669 createdAt: "2026-01-01T00:00:00Z",
6670 });
6671 },
6672 createAppDeployment: (appId: string, input: unknown) => {
6673 calls.push(["createAppDeployment", { appId, input }]);
6674 return Effect.succeed({
6675 id: "version-new",
6676 type: "deployment" as const,
6677 url: "https://api.prisma.test/v1/deployments/version-new",
6678 foundryVersionId: "foundry-new",
6679 uploadUrl: "https://upload.prisma.test/version-new.tar.gz",
6680 });
6681 },
6682 getDeployment: (id: string) => {
6683 calls.push(["getDeployment", id]);
6684 return Effect.succeed({
6685 id,
6686 type: "deployment" as const,
6687 serviceId: "service-1",
6688 url: `https://api.prisma.test/v1/deployments/${id}`,
6689 foundryVersionId: "foundry-new",
6690 status: versions.get(id) ?? "new",
6691 previewDomain: `${id}.preview.prisma.build`,
6692 createdAt: "2026-01-01T00:00:00Z",
6693 });
6694 },
6695 startDeployment: (id: string) =>
6696 Effect.sync(() => {
6697 calls.push(["startDeployment", id]);
6698 versions.set(id, "running");
6699 return { previewDomain: `${id}.preview.prisma.build` };
6700 }),
6701 promoteApp: (
6702 appId: string,
6703 { deploymentId }: { deploymentId: string },
6704 ) =>
6705 Effect.gen(function* () {
6706 calls.push(["promoteApp", { appId, deploymentId }]);
6707 return yield* Effect.fail(
6708 new PrismaApiError({
6709 method: "POST",
6710 path: `/v1/services/${appId}/promote`,
6711 status: 400,
6712 message: "promote failed",
6713 }),
6714 );
6715 }),
6716 rollbackApp: () =>
6717 Effect.fail(
6718 new PrismaApiError({
6719 method: "POST",
6720 path: "/v1/services/service-1/rollback",
6721 status: 400,
6722 message: "promotion recovery failed",
6723 }),
6724 ),
6725 stopDeployment: (id: string) =>
6726 Effect.sync(() => {
6727 calls.push(["stopDeployment", id]);
6728 versions.set(id, "stopped");
6729 }),
6730 deleteDeployment: (id: string) =>
6731 Effect.sync(() => {
6732 calls.push(["deleteDeployment", id]);
6733 versions.delete(id);
6734 }),
6735 } as unknown as PrismaManagementClient;
6736
6737 const http = HttpClient.make((request) =>
6738 Effect.succeed(
6739 HttpClientResponse.fromWeb(request, new Response(null)),
6740 ),
6741 );
6742
6743 return Effect.gen(function* () {
6744 const provider = yield* Compute.Provider;
6745 const error = yield* provider
6746 .reconcile({
6747 id: "App",
6748 fqn: "App",
6749 instanceId: "00000000000000000000000000000000",
6750 news: {
6751 project: "project-1",
6752 appName: "api",
6753 artifactPath: fixtureArtifactV1Path,
6754 branchId: "branch-main",
6755 },
6756 olds: undefined,
6757 output: {
6758 appId: "service-1",
6759 deploymentId: undefined,
6760 projectId: "project-1",
6761 appName: "api",
6762 regionId: "us-east-1",
6763 deploymentEndpointDomain: undefined,
6764 deploymentUrl: undefined,
6765 appEndpointDomain: "api.prisma.build",
6766 url: "https://api.prisma.build",
6767 promoted: false,
6768 previousDeploymentId: undefined,
6769 previousDeploymentAction: undefined,
6770 artifactHash: undefined,
6771 local: false,
6772 },
6773 session: undefined as never,
6774 bindings: [],
6775 })
6776 .pipe(Effect.flip);
6777
6778 expect(error).toBeInstanceOf(AggregateError);
6779 expect((error as AggregateError).message).toContain("ambiguous");
6780 expect(versions.has("version-new")).toBe(true);
6781 expect(calls).toEqual([
6782 ["getApp", "service-1"],
6783 [
6784 "createAppDeployment",
6785 {
6786 appId: "service-1",
6787 input: {
6788 portMapping: { http: 8080 },
6789 skipCodeUpload: undefined,
6790 },
6791 },
6792 ],
6793 ["getDeployment", "version-new"],
6794 ["startDeployment", "version-new"],
6795 ["getDeployment", "version-new"],
6796 ["promoteApp", { appId: "service-1", deploymentId: "version-new" }],
6797 ["getApp", "service-1"],
6798 ["getApp", "service-1"],
6799 ]);
6800 }).pipe(
6801 Effect.provide(computeProviderLive()),
6802 Effect.provide(
6803 Layer.succeed(PrismaClient, withDefaultBranch(client)),
6804 ),
6805 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
6806 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
6807 Effect.provide(PlatformServices),
6808 );
6809 },
6810 { tags: ["provider:prisma:branch"] },
6811 );
6812
6813 it.effect(
6814 "deletes env vars removed from Compute props on update",
6815 () => {
6816 const calls: Array<[string, unknown]> = [];
6817 const byKey = new Map([
6818 [
6819 "TOKEN",
6820 {
6821 id: "env-token",
6822 type: "environment-variable" as const,
6823 url: "https://api.prisma.test/v1/environment-variables/env-token",
6824 projectId: "project-1",
6825 branchId: null,
6826 class: "production" as const,
6827 key: "TOKEN",
6828 valueKid: "kid-1",
6829 isManagedBySystem: false,
6830 createdAt: "2026-01-01T00:00:00Z",
6831 updatedAt: "2026-01-01T00:00:00Z",
6832 },
6833 ],
6834 [
6835 "KEEP",
6836 {
6837 id: "env-keep",
6838 type: "environment-variable" as const,
6839 url: "https://api.prisma.test/v1/environment-variables/env-keep",
6840 projectId: "project-1",
6841 branchId: null,
6842 class: "production" as const,
6843 key: "KEEP",
6844 valueKid: "kid-2",
6845 isManagedBySystem: false,
6846 createdAt: "2026-01-01T00:00:00Z",
6847 updatedAt: "2026-01-01T00:00:00Z",
6848 },
6849 ],
6850 ]);
6851 const client = {
6852 getApp: (id: string) => {
6853 calls.push(["getApp", id]);
6854 return Effect.succeed({
6855 id,
6856 type: "app" as const,
6857 url: "https://api.prisma.test/v1/services/service-1",
6858 name: "api",
6859 region: { id: "us-east-1", name: "US East" },
6860 projectId: "project-1",
6861 branchId: "branch-main",
6862 latestDeploymentId: "version-old",
6863 appEndpointDomain: "api.prisma.build",
6864 createdAt: "2026-01-01T00:00:00Z",
6865 });
6866 },
6867 listEnvironmentVariables: (query: { key: string }) => {
6868 calls.push(["listEnvironmentVariables", query]);
6869 const variable = byKey.get(query.key);
6870 return Effect.succeed(variable ? [variable] : []);
6871 },
6872 deleteEnvironmentVariable: (id: string) => {
6873 calls.push(["deleteEnvironmentVariable", id]);
6874 return Effect.void;
6875 },
6876 updateEnvironmentVariable: (id: string, input: unknown) => {
6877 calls.push(["updateEnvironmentVariable", { id, input }]);
6878 return Effect.succeed(byKey.get("KEEP"));
6879 },
6880 createEnvironmentVariable: (input: unknown) => {
6881 calls.push(["createEnvironmentVariable", input]);
6882 return Effect.succeed({
6883 id: "env-new",
6884 type: "environment-variable" as const,
6885 url: "https://api.prisma.test/v1/environment-variables/env-new",
6886 projectId: "project-1",
6887 branchId: null,
6888 class: "production" as const,
6889 key: "NEW",
6890 valueKid: "kid-3",
6891 isManagedBySystem: false,
6892 createdAt: "2026-01-01T00:00:00Z",
6893 updatedAt: "2026-01-01T00:00:00Z",
6894 });
6895 },
6896 createAppDeployment: (appId: string, input: unknown) => {
6897 calls.push(["createAppDeployment", { appId, input }]);
6898 return Effect.succeed({
6899 id: "version-new",
6900 type: "deployment" as const,
6901 url: "https://api.prisma.test/v1/deployments/version-new",
6902 foundryVersionId: "foundry-new",
6903 uploadUrl: "https://upload.prisma.test/version-new.tar.gz",
6904 });
6905 },
6906 getDeployment: (id: string) => {
6907 calls.push(["getDeployment", id]);
6908 return Effect.succeed({
6909 id,
6910 type: "deployment" as const,
6911 serviceId: "service-1",
6912 url: `https://api.prisma.test/v1/deployments/${id}`,
6913 foundryVersionId: "foundry-new",
6914 status: "new",
6915 previewDomain: "version-new.preview.prisma.build",
6916 createdAt: "2026-01-01T00:00:00Z",
6917 });
6918 },
6919 } as unknown as PrismaManagementClient;
6920
6921 const http = HttpClient.make((request) =>
6922 Effect.succeed(
6923 HttpClientResponse.fromWeb(request, new Response(null)),
6924 ),
6925 );
6926
6927 return Effect.gen(function* () {
6928 const provider = yield* Compute.Provider;
6929 const output = yield* provider.reconcile({
6930 id: "App",
6931 fqn: "App",
6932 instanceId: "00000000000000000000000000000000",
6933 news: {
6934 project: "project-1",
6935 appName: "api",
6936 artifactPath: fixtureArtifactV2Path,
6937 branchId: "branch-main",
6938 start: false,
6939 skipPromote: true,
6940 env: {
6941 KEEP: "new-value",
6942 NEW: "created",
6943 },
6944 },
6945 olds: {
6946 project: "project-1",
6947 appName: "api",
6948 artifactPath: fixtureArtifactV1Path,
6949 branchId: "branch-main",
6950 start: false,
6951 skipPromote: true,
6952 env: {
6953 KEEP: "old-value",
6954 TOKEN: Redacted.make("secret"),
6955 ALREADY_ABSENT: null,
6956 },
6957 },
6958 output: {
6959 appId: "service-1",
6960 deploymentId: "version-old",
6961 projectId: "project-1",
6962 appName: "api",
6963 regionId: "us-east-1",
6964 deploymentEndpointDomain: "version-old.preview.prisma.build",
6965 deploymentUrl: "https://version-old.preview.prisma.build",
6966 appEndpointDomain: "api.prisma.build",
6967 url: "https://api.prisma.build",
6968 promoted: true,
6969 previousDeploymentId: undefined,
6970 previousDeploymentAction: undefined,
6971 environmentVariableIds: {
6972 KEEP: "env-keep",
6973 TOKEN: "env-token",
6974 },
6975 artifactHash: Redacted.make("old-hash"),
6976 local: false,
6977 },
6978 session: undefined as never,
6979 bindings: [],
6980 });
6981
6982 expect(output.deploymentId).toBe("version-new");
6983 expect(calls).toEqual([
6984 ["getApp", "service-1"],
6985 ["getDeployment", "version-old"],
6986 [
6987 "listEnvironmentVariables",
6988 {
6989 projectId: "project-1",
6990 class: "production",
6991 key: "KEEP",
6992 limit: "100",
6993 },
6994 ],
6995 [
6996 "listEnvironmentVariables",
6997 {
6998 projectId: "project-1",
6999 class: "production",
7000 key: "NEW",
7001 limit: "100",
7002 },
7003 ],
7004 [
7005 "updateEnvironmentVariable",
7006 { id: "env-keep", input: { value: "new-value" } },
7007 ],
7008 [
7009 "createEnvironmentVariable",
7010 {
7011 projectId: "project-1",
7012 class: "production",
7013 key: "NEW",
7014 value: "created",
7015 },
7016 ],
7017 [
7018 "listEnvironmentVariables",
7019 {
7020 projectId: "project-1",
7021 class: "production",
7022 key: "TOKEN",
7023 limit: "100",
7024 },
7025 ],
7026 ["deleteEnvironmentVariable", "env-token"],
7027 [
7028 "createAppDeployment",
7029 {
7030 appId: "service-1",
7031 input: {
7032 portMapping: { http: 8080 },
7033 skipCodeUpload: undefined,
7034 },
7035 },
7036 ],
7037 ["getDeployment", "version-new"],
7038 ]);
7039 }).pipe(
7040 Effect.provide(computeProviderLive()),
7041 Effect.provide(
7042 Layer.succeed(PrismaClient, withDefaultBranch(client)),
7043 ),
7044 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
7045 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
7046 Effect.provide(PlatformServices),
7047 );
7048 },
7049 { tags: ["provider:prisma:branch"] },
7050 );
7051
7052 it.effect("returns an empty tail stream before a deployment exists", () =>
7053 Effect.gen(function* () {
7054 const provider = yield* Provider.findProvider(Compute);
7055 const chunks = yield* Stream.runCollect(
7056 provider.tail!({
7057 id: "App",
7058 fqn: "App",
7059 instanceId: "00000000000000000000000000000000",
7060 props: {
7061 project: "project-1",
7062 appName: "api",
7063 },
7064 output: {
7065 appId: "service-1",
7066 deploymentId: undefined,
7067 projectId: "project-1",
7068 appName: "api",
7069 regionId: "us-east-1",
7070 deploymentEndpointDomain: undefined,
7071 deploymentUrl: undefined,
7072 appEndpointDomain: undefined,
7073 url: undefined,
7074 promoted: false,
7075 previousDeploymentId: undefined,
7076 previousDeploymentAction: undefined,
7077 artifactHash: undefined,
7078 local: false,
7079 },
7080 }),
7081 );
7082
7083 expect(chunks).toEqual([]);
7084 }).pipe(
7085 Effect.provide(computeProviderLive()),
7086 Effect.provide(
7087 Layer.succeed(PrismaClient, {} as unknown as PrismaManagementClient),
7088 ),
7089 Effect.provide(apiRoutedHttp({} as unknown as PrismaManagementClient)),
7090 Effect.provide(FetchHttpClient.layer),
7091 Effect.provide(PlatformServices),
7092 ),
7093 );
7094
7095 it.effect(
7096 "deletes Compute env vars on provider destroy",
7097 () => {
7098 const calls: Array<[string, unknown]> = [];
7099 const client = {
7100 listEnvironmentVariables: (query: unknown) => {
7101 calls.push(["listEnvironmentVariables", query]);
7102 return Effect.succeed([
7103 {
7104 id: "env-token",
7105 type: "environment-variable" as const,
7106 url: "https://api.prisma.test/v1/environment-variables/env-token",
7107 projectId: "project-1",
7108 branchId: null,
7109 class: "production" as const,
7110 key: "TOKEN",
7111 valueKid: "kid-1",
7112 isManagedBySystem: false,
7113 createdAt: "2026-01-01T00:00:00Z",
7114 updatedAt: "2026-01-01T00:00:00Z",
7115 },
7116 ]);
7117 },
7118 deleteEnvironmentVariable: (id: string) => {
7119 calls.push(["deleteEnvironmentVariable", id]);
7120 return Effect.void;
7121 },
7122 listAppDeployments: (appId: string, query: unknown) => {
7123 calls.push(["listAppDeployments", { appId, query }]);
7124 return Effect.succeed([]);
7125 },
7126 deleteApp: (id: string) => {
7127 calls.push(["deleteApp", id]);
7128 return Effect.void;
7129 },
7130 } as unknown as PrismaManagementClient;
7131
7132 return Effect.gen(function* () {
7133 const provider = yield* Compute.Provider;
7134 yield* provider.delete({
7135 id: "App",
7136 fqn: "App",
7137 instanceId: "00000000000000000000000000000000",
7138 olds: {
7139 project: "project-1",
7140 appName: "api",
7141 env: {
7142 TOKEN: Redacted.make("secret"),
7143 ALREADY_ABSENT: null,
7144 SKIP_ME: undefined,
7145 },
7146 },
7147 output: {
7148 appId: "service-1",
7149 deploymentId: "version-1",
7150 projectId: "project-1",
7151 appName: "api",
7152 regionId: "us-east-1",
7153 deploymentEndpointDomain: "version-1.preview.prisma.build",
7154 deploymentUrl: "https://version-1.preview.prisma.build",
7155 appEndpointDomain: "api.prisma.build",
7156 url: "https://api.prisma.build",
7157 promoted: true,
7158 previousDeploymentId: undefined,
7159 previousDeploymentAction: undefined,
7160 environmentVariableIds: { TOKEN: "env-token" },
7161 artifactHash: Redacted.make("hash-1"),
7162 local: false,
7163 },
7164 session: undefined as never,
7165 bindings: [],
7166 });
7167
7168 expect(calls).toEqual([
7169 [
7170 "listEnvironmentVariables",
7171 {
7172 projectId: "project-1",
7173 class: "production",
7174 key: "TOKEN",
7175 limit: "100",
7176 },
7177 ],
7178 ["deleteEnvironmentVariable", "env-token"],
7179 ["deleteApp", "service-1"],
7180 ]);
7181 }).pipe(
7182 Effect.provide(computeProviderLive()),
7183 Effect.provide(
7184 Layer.succeed(PrismaClient, withDefaultBranch(client)),
7185 ),
7186 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
7187 Effect.provide(FetchHttpClient.layer),
7188 Effect.provide(PlatformServices),
7189 );
7190 },
7191 { tags: ["provider:prisma:branch"] },
7192 );
7193
7194 it.effect(
7195 "skips system-managed Compute env vars on provider destroy",
7196 () => {
7197 const calls: Array<[string, unknown]> = [];
7198 const byKey = new Map([
7199 [
7200 "TOKEN",
7201 {
7202 id: "env-token",
7203 type: "environment-variable" as const,
7204 url: "https://api.prisma.test/v1/environment-variables/env-token",
7205 projectId: "project-1",
7206 branchId: null,
7207 class: "production" as const,
7208 key: "TOKEN",
7209 valueKid: "kid-token",
7210 isManagedBySystem: false,
7211 createdAt: "2026-01-01T00:00:00Z",
7212 updatedAt: "2026-01-01T00:00:00Z",
7213 },
7214 ],
7215 [
7216 "PRISMA_INTERNAL_URL",
7217 {
7218 id: "env-system",
7219 type: "environment-variable" as const,
7220 url: "https://api.prisma.test/v1/environment-variables/env-system",
7221 projectId: "project-1",
7222 branchId: null,
7223 class: "production" as const,
7224 key: "PRISMA_INTERNAL_URL",
7225 valueKid: "kid-system",
7226 isManagedBySystem: true,
7227 createdAt: "2026-01-01T00:00:00Z",
7228 updatedAt: "2026-01-01T00:00:00Z",
7229 },
7230 ],
7231 ]);
7232 const client = {
7233 listEnvironmentVariables: (query: { key: string }) => {
7234 calls.push(["listEnvironmentVariables", query]);
7235 return Effect.succeed(
7236 byKey.get(query.key) ? [byKey.get(query.key)] : [],
7237 );
7238 },
7239 deleteEnvironmentVariable: (id: string) => {
7240 calls.push(["deleteEnvironmentVariable", id]);
7241 return Effect.void;
7242 },
7243 listAppDeployments: (appId: string, query: unknown) => {
7244 calls.push(["listAppDeployments", { appId, query }]);
7245 return Effect.succeed([]);
7246 },
7247 deleteApp: (id: string) => {
7248 calls.push(["deleteApp", id]);
7249 return Effect.void;
7250 },
7251 } as unknown as PrismaManagementClient;
7252
7253 return Effect.gen(function* () {
7254 const provider = yield* Compute.Provider;
7255 yield* provider.delete({
7256 id: "App",
7257 fqn: "App",
7258 instanceId: "00000000000000000000000000000000",
7259 olds: {
7260 project: "project-1",
7261 appName: "api",
7262 env: {
7263 TOKEN: Redacted.make("secret"),
7264 PRISMA_INTERNAL_URL: Redacted.make("prisma-owned"),
7265 },
7266 },
7267 output: {
7268 appId: "service-1",
7269 deploymentId: "version-1",
7270 projectId: "project-1",
7271 appName: "api",
7272 regionId: "us-east-1",
7273 deploymentEndpointDomain: "version-1.preview.prisma.build",
7274 deploymentUrl: "https://version-1.preview.prisma.build",
7275 appEndpointDomain: "api.prisma.build",
7276 url: "https://api.prisma.build",
7277 promoted: true,
7278 previousDeploymentId: undefined,
7279 previousDeploymentAction: undefined,
7280 environmentVariableIds: {
7281 TOKEN: "env-token",
7282 PRISMA_INTERNAL_URL: "env-system",
7283 },
7284 artifactHash: Redacted.make("hash-1"),
7285 local: false,
7286 },
7287 session: undefined as never,
7288 bindings: [],
7289 });
7290
7291 expect(calls).toEqual([
7292 [
7293 "listEnvironmentVariables",
7294 {
7295 projectId: "project-1",
7296 class: "production",
7297 key: "TOKEN",
7298 limit: "100",
7299 },
7300 ],
7301 ["deleteEnvironmentVariable", "env-token"],
7302 [
7303 "listEnvironmentVariables",
7304 {
7305 projectId: "project-1",
7306 class: "production",
7307 key: "PRISMA_INTERNAL_URL",
7308 limit: "100",
7309 },
7310 ],
7311 ["deleteApp", "service-1"],
7312 ]);
7313 }).pipe(
7314 Effect.provide(computeProviderLive()),
7315 Effect.provide(
7316 Layer.succeed(PrismaClient, withDefaultBranch(client)),
7317 ),
7318 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
7319 Effect.provide(FetchHttpClient.layer),
7320 Effect.provide(PlatformServices),
7321 );
7322 },
7323 { tags: ["provider:prisma:branch"] },
7324 );
7325
7326 it.effect(
7327 "deletes Compute env vars when old props are missing",
7328 () => {
7329 const calls: Array<[string, unknown]> = [];
7330 const client = {
7331 listEnvironmentVariables: (query: { key: string }) => {
7332 calls.push(["listEnvironmentVariables", query]);
7333 return Effect.succeed(
7334 query.key === "TOKEN"
7335 ? [
7336 {
7337 id: "env-token",
7338 type: "environment-variable" as const,
7339 url: "https://api.prisma.test/v1/environment-variables/env-token",
7340 projectId: "project-1",
7341 branchId: null,
7342 class: "production" as const,
7343 key: "TOKEN",
7344 valueKid: "kid-1",
7345 isManagedBySystem: false,
7346 createdAt: "2026-01-01T00:00:00Z",
7347 updatedAt: "2026-01-01T00:00:00Z",
7348 },
7349 ]
7350 : [],
7351 );
7352 },
7353 deleteEnvironmentVariable: (id: string) => {
7354 calls.push(["deleteEnvironmentVariable", id]);
7355 return Effect.void;
7356 },
7357 listAppDeployments: (appId: string, query: unknown) => {
7358 calls.push(["listAppDeployments", { appId, query }]);
7359 return Effect.succeed([]);
7360 },
7361 deleteApp: (id: string) => {
7362 calls.push(["deleteApp", id]);
7363 return Effect.void;
7364 },
7365 } as unknown as PrismaManagementClient;
7366
7367 return Effect.gen(function* () {
7368 const provider = yield* Compute.Provider;
7369 yield* provider.delete({
7370 id: "App",
7371 fqn: "App",
7372 instanceId: "00000000000000000000000000000000",
7373 olds: undefined as never,
7374 output: {
7375 appId: "service-1",
7376 deploymentId: "version-1",
7377 projectId: "project-1",
7378 appName: "api",
7379 regionId: "us-east-1",
7380 deploymentEndpointDomain: "version-1.preview.prisma.build",
7381 deploymentUrl: "https://version-1.preview.prisma.build",
7382 appEndpointDomain: "api.prisma.build",
7383 url: "https://api.prisma.build",
7384 promoted: true,
7385 previousDeploymentId: undefined,
7386 previousDeploymentAction: undefined,
7387 environmentKeys: ["TOKEN"],
7388 environmentVariableIds: { TOKEN: "env-token" },
7389 environmentClass: "preview",
7390 artifactHash: Redacted.make("hash-1"),
7391 local: false,
7392 },
7393 session: undefined as never,
7394 bindings: [],
7395 });
7396
7397 expect(calls).toEqual([
7398 [
7399 "listEnvironmentVariables",
7400 {
7401 projectId: "project-1",
7402 class: "preview",
7403 key: "TOKEN",
7404 limit: "100",
7405 },
7406 ],
7407 ["deleteEnvironmentVariable", "env-token"],
7408 ["deleteApp", "service-1"],
7409 ]);
7410 }).pipe(
7411 Effect.provide(computeProviderLive()),
7412 Effect.provide(
7413 Layer.succeed(PrismaClient, withDefaultBranch(client)),
7414 ),
7415 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
7416 Effect.provide(FetchHttpClient.layer),
7417 Effect.provide(PlatformServices),
7418 );
7419 },
7420 { tags: ["provider:prisma:branch"] },
7421 );
7422
7423 it.effect(
7424 "continues Compute destroy when managed env vars are already gone",
7425 () => {
7426 const calls: Array<[string, unknown]> = [];
7427 const client = {
7428 listEnvironmentVariables: (query: unknown) =>
7429 Effect.gen(function* () {
7430 calls.push(["listEnvironmentVariables", query]);
7431 return yield* Effect.fail(
7432 new PrismaApiError({
7433 method: "GET",
7434 path: "/v1/environment-variables",
7435 status: 404,
7436 message: "project not found",
7437 }),
7438 );
7439 }),
7440 listAppDeployments: (appId: string, query: unknown) => {
7441 calls.push(["listAppDeployments", { appId, query }]);
7442 return Effect.succeed([]);
7443 },
7444 deleteApp: (id: string) => {
7445 calls.push(["deleteApp", id]);
7446 return Effect.void;
7447 },
7448 } as unknown as PrismaManagementClient;
7449
7450 return Effect.gen(function* () {
7451 const provider = yield* Compute.Provider;
7452 yield* provider.delete({
7453 id: "App",
7454 fqn: "App",
7455 instanceId: "00000000000000000000000000000000",
7456 olds: {
7457 project: "project-1",
7458 appName: "api",
7459 env: {
7460 TOKEN: Redacted.make("secret"),
7461 },
7462 },
7463 output: {
7464 appId: "service-1",
7465 deploymentId: "version-1",
7466 projectId: "project-1",
7467 appName: "api",
7468 regionId: "us-east-1",
7469 deploymentEndpointDomain: "version-1.preview.prisma.build",
7470 deploymentUrl: "https://version-1.preview.prisma.build",
7471 appEndpointDomain: "api.prisma.build",
7472 url: "https://api.prisma.build",
7473 promoted: true,
7474 previousDeploymentId: undefined,
7475 previousDeploymentAction: undefined,
7476 environmentVariableIds: { TOKEN: "env-token" },
7477 artifactHash: Redacted.make("hash-1"),
7478 local: false,
7479 },
7480 session: undefined as never,
7481 bindings: [],
7482 });
7483
7484 expect(calls).toEqual([
7485 [
7486 "listEnvironmentVariables",
7487 {
7488 projectId: "project-1",
7489 class: "production",
7490 key: "TOKEN",
7491 limit: "100",
7492 },
7493 ],
7494 ["deleteApp", "service-1"],
7495 ]);
7496 }).pipe(
7497 Effect.provide(computeProviderLive()),
7498 Effect.provide(
7499 Layer.succeed(PrismaClient, withDefaultBranch(client)),
7500 ),
7501 Effect.provide(apiRoutedHttp(withDefaultBranch(client))),
7502 Effect.provide(FetchHttpClient.layer),
7503 Effect.provide(PlatformServices),
7504 );
7505 },
7506 { tags: ["provider:prisma:branch"] },
7507 );
7508
7509 it.effect("tails Compute logs through the provider", () =>
7510 withWebSocketServer((server) =>
7511 Effect.gen(function* () {
7512 const url = yield* listenUrl(server);
7513 let authorization: string | undefined;
7514 let requestUrl: string | undefined;
7515
7516 server.on("connection", (socket, request) => {
7517 authorization = request.headers.authorization;
7518 requestUrl = request.url;
7519 socket.send(
7520 JSON.stringify({
7521 type: "log",
7522 text: "compute app log",
7523 byteStart: 0,
7524 byteEnd: 15,
7525 }),
7526 );
7527 socket.send(
7528 JSON.stringify({
7529 type: "terminal",
7530 kind: "end",
7531 code: "vm_stopped",
7532 message: "done",
7533 retryable: false,
7534 cursor: null,
7535 }),
7536 );
7537 });
7538
7539 const provider = yield* Provider.findProvider(Compute).pipe(
7540 Effect.provide(computeProviderLive()),
7541 Effect.provide(makeFakeManagementApi(unhandled).layer),
7542 );
7543 const lines = yield* provider.tail!({
7544 id: "App",
7545 fqn: "App",
7546 instanceId: "00000000000000000000000000000000",
7547 props: {
7548 project: "project-1",
7549 appName: "api",
7550 },
7551 output: {
7552 appId: "service-1",
7553 deploymentId: "version-1",
7554 projectId: "project-1",
7555 appName: "api",
7556 regionId: "us-east-1",
7557 deploymentEndpointDomain: "version-1.preview.prisma.build",
7558 deploymentUrl: "https://version-1.preview.prisma.build",
7559 appEndpointDomain: "api.prisma.build",
7560 url: "https://api.prisma.build",
7561 promoted: true,
7562 previousDeploymentId: undefined,
7563 previousDeploymentAction: undefined,
7564 artifactHash: Redacted.make("hash-1"),
7565 local: false,
7566 },
7567 }).pipe(
7568 // The carved-out logs client resolves the distilled Credentials
7569 // service directly: the test WebSocket server is the API base.
7570 Stream.provideService(
7571 Credentials,
7572 Effect.succeed({
7573 apiToken: Redacted.make("app-token"),
7574 apiBaseUrl: url,
7575 }),
7576 ),
7577 Stream.runCollect,
7578 );
7579
7580 expect(lines.map((line) => line.message)).toEqual([
7581 "compute app log",
7582 ]);
7583 expect(authorization).toBe("Bearer app-token");
7584 expect(requestUrl).toBe("/v1/deployments/version-1/logs");
7585 }).pipe(
7586 Effect.provide(FetchHttpClient.layer),
7587 Effect.provide(PlatformServices),
7588 ),
7589 ),
7590 );
7591 },
7592);
7593
7594const withWebSocketServer = <A, E, R>(
7595 f: (server: WebSocketServer) => Effect.Effect<A, E, R>,
7596) =>
7597 Effect.acquireUseRelease(
7598 Effect.sync(() => new WebSocketServer({ host: "127.0.0.1", port: 0 })),
7599 f,
7600 (server) =>
7601 Effect.callback<void>((resume) => {
7602 server.close(() => resume(Effect.void));
7603 }).pipe(Effect.ignore),
7604 );
7605
7606const listenUrl = (server: WebSocketServer) =>
7607 Effect.callback<string, Error>((resume) => {
7608 const complete = () => {
7609 cleanup();
7610 const address = server.address();
7611 if (address && typeof address === "object") {
7612 resume(Effect.succeed(`http://127.0.0.1:${address.port}`));
7613 } else {
7614 resume(Effect.fail(new Error("WebSocket server has no TCP address")));
7615 }
7616 };
7617 const fail = (cause: unknown) => {
7618 cleanup();
7619 resume(
7620 Effect.fail(cause instanceof Error ? cause : new Error(String(cause))),
7621 );
7622 };
7623 const cleanup = () => {
7624 server.off("listening", complete);
7625 server.off("error", fail);
7626 };
7627
7628 if (server.address()) {
7629 complete();
7630 return;
7631 }
7632
7633 server.once("listening", complete);
7634 server.once("error", fail);
7635 return Effect.sync(cleanup);
7636 });