1import * as Provider from "@/Provider";
2import {
3 Deployment as PrismaDeployment,
4 DeploymentProvider,
5 MAX_DEPLOYMENT_ARTIFACT_BYTES,
6 readUploadArtifact,
7 uploadArtifact,
8 validateDeploymentArtifactBytes,
9} from "@/Prisma/Deployment";
10import * as Output from "@/Output";
11import {
12 PrismaApiError,
13 PrismaClient,
14 type PrismaManagementClient,
15} from "@/Prisma/Client";
16import { executeArtifactUpload } from "@/Prisma/Internal/ArtifactUpload";
17import {
18 PrismaHttpClientLive,
19 PrismaUploadClient,
20} from "@/Prisma/Internal/HttpClient";
21import { PlatformServices } from "@/Util/PlatformServices";
22import { sha256, sha256Object } from "@/Util/sha256";
23import { describe, expect, it } from "alchemy-test";
24import * as Effect from "effect/Effect";
25import * as FileSystem from "effect/FileSystem";
26import * as Layer from "effect/Layer";
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 HttpClientError from "effect/http/HttpClientError";
34import * as HttpClientResponse from "effect/http/HttpClientResponse";
35import {
36 dispatchTo,
37 failure,
38 makeFakeManagementApi,
39 notFound,
40 unhandled,
41} from "./fixtures/FakeManagementApi.ts";
42import {
43 createServer as createHttpServer,
44 type RequestListener,
45 type Server as NodeHttpServer,
46} from "node:http";
47import { WebSocketServer } from "ws";
48import { AlchemyContext } from "@/AlchemyContext";
49import { Credentials } from "@/Prisma/Credentials";
50import { encodeState } from "@/State/StateEncoding";
51
52const currentClient = <T extends object>(client: T): PrismaManagementClient => {
53 return client as unknown as PrismaManagementClient;
54};
55
56const fixtureArtifactPath = `${import.meta.dirname}/fixtures/artifact-archive.bin`;
57const sameArtifactPath = `${import.meta.dirname}/fixtures/artifact-same-version.bin`;
58
59const liveProviderContext = Layer.succeed(AlchemyContext, {
60 dotAlchemy: ".alchemy-test",
61 dev: false,
62 adopt: false,
63});
64
65const deploymentProviderLive = () =>
66 DeploymentProvider().pipe(Layer.provide(liveProviderContext));
67
68/**
69 * Serve the Management API from the same hermetic client-shaped handlers this
70 * suite declares, for the routes Deployment now reaches through distilled
71 * operations (list/create under /v1/services/{serviceId}/deployments). The handlers
72 * are synchronous, so the fake runs them directly: an injected
73 * `PrismaApiError` becomes its real status, `undefined` becomes a 404,
74 * everything else a `{ data }` (or `{ data, pagination }`) envelope. The
75 * observe/start/promote/delete paths still resolve the hand-rolled client
76 * (their helpers are D3), so the `PrismaClient` layer stays provided
77 * alongside; tests that stub uploads provide `PrismaUploadClient` because
78 * the ambient `HttpClient` is now this management fake.
79 */
80const clientBackedApi = (client: any) =>
81 makeFakeManagementApi((request) => {
82 // segments[0] is the "v1" prefix.
83 const [head, id, tail] = request.pathname
84 .split("/")
85 .filter((segment) => segment.length > 0)
86 .slice(1);
87 const body = request.bodyJson as any;
88 const query = Object.fromEntries(new URLSearchParams(request.search));
89 const { call, callVoid, list } = dispatchTo(request);
90
91 if (head === "services" && id !== undefined) {
92 if (tail === "deployments") {
93 return request.method === "GET"
94 ? call(client.listAppDeployments, [id, query], list)
95 : call(client.createAppDeployment, [id, body]);
96 }
97 if (tail === "promote") return call(client.promoteApp, [id, body]);
98 if (tail === "rollback") return call(client.rollbackApp, [id, body]);
99 if (tail === undefined && request.method === "GET") {
100 return call(client.getApp, [id]);
101 }
102 }
103 if (head === "deployments" && id !== undefined) {
104 if (tail === "start") return call(client.startDeployment, [id]);
105 if (tail === "stop") return callVoid(client.stopDeployment, [id]);
106 if (request.method === "GET") return call(client.getDeployment, [id]);
107 if (request.method === "DELETE") {
108 return callVoid(client.deleteDeployment, [id]);
109 }
110 }
111 return unhandled(request);
112 });
113
114describe(
115 "Prisma Deployment",
116 { tags: ["unit", "provider:prisma", "provider:prisma:deployment", "local"] },
117 () => {
118 it.effect("redacts signed upload URLs from transport failures", () => {
119 const secret = "SIGNED_QUERY_SECRET_SENTINEL";
120 const http = HttpClient.make((request) =>
121 Effect.fail(new Error(`transport failed for ${request.url}`) as never),
122 );
123
124 return Effect.gen(function* () {
125 const error = yield* uploadArtifact(
126 `https://upload.prisma.test/artifact?signature=${secret}`,
127 new Uint8Array([1]),
128 "application/octet-stream",
129 ).pipe(Effect.flip);
130
131 expect(error.message).toContain("transport failed");
132 expect(String(error)).not.toContain(secret);
133 expect(String(error)).not.toContain("upload.prisma.test");
134 expect(JSON.stringify(error)).not.toContain(secret);
135 }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, http)));
136 });
137
138 it.effect("rejects final artifacts above the upload byte limit", () =>
139 Effect.gen(function* () {
140 const error = yield* validateDeploymentArtifactBytes(
141 new Uint8Array(9),
142 8,
143 ).pipe(Effect.flip);
144
145 expect(error.message).toContain(
146 "exceeds the 8 byte upload safety limit",
147 );
148 }),
149 );
150
151 it.effect("does not allow callers to raise the upload hard limit", () =>
152 Effect.gen(function* () {
153 const error = yield* validateDeploymentArtifactBytes(
154 new Uint8Array(1),
155 MAX_DEPLOYMENT_ARTIFACT_BYTES + 1,
156 ).pipe(Effect.flip);
157
158 expect(error.message).toContain("hard limit");
159 }),
160 );
161
162 it.effect("rejects symbolic-link artifact paths", () =>
163 Effect.gen(function* () {
164 const fs = yield* FileSystem.FileSystem;
165 const path = yield* Path.Path;
166 const root = yield* fs.makeTempDirectory({
167 prefix: "alchemy-prisma-artifact-symlink-",
168 });
169 const target = path.join(root, "artifact.tar.gz");
170 const link = path.join(root, "artifact-link.tar.gz");
171 yield* fs.writeFileString(target, "archive");
172 yield* fs.symlink(target, link);
173
174 const error = yield* readUploadArtifact({
175 artifactPath: link,
176 output: "file",
177 }).pipe(Effect.flip);
178
179 expect(error.message).toContain("symbolic link");
180 }).pipe(Effect.provide(PlatformServices)),
181 );
182
183 it.effect("refuses to upload a file changed after validation", () => {
184 const http = HttpClient.make((request) => {
185 const body = request.body as HttpBody.HttpBody;
186 const consume =
187 body._tag === "Stream" ? Stream.runDrain(body.stream) : Effect.void;
188 return consume.pipe(
189 Effect.mapError(
190 (cause) =>
191 new HttpClientError.HttpClientError({
192 reason: new HttpClientError.TransportError({
193 request,
194 cause,
195 description: "test request body stream failed",
196 }),
197 }),
198 ),
199 Effect.as(HttpClientResponse.fromWeb(request, new Response(null))),
200 );
201 });
202
203 return Effect.gen(function* () {
204 const fs = yield* FileSystem.FileSystem;
205 const path = yield* Path.Path;
206 const root = yield* fs.makeTempDirectory({
207 prefix: "alchemy-prisma-artifact-mutation-",
208 });
209 const artifactPath = path.join(root, "artifact.tar.gz");
210 yield* fs.writeFileString(artifactPath, "first!!");
211 const artifact = yield* readUploadArtifact({
212 artifactPath,
213 output: "file",
214 });
215 yield* fs.writeFileString(artifactPath, "second!");
216
217 const error = yield* uploadArtifact(
218 "https://upload.prisma.test/artifact.tar.gz?signature=secret",
219 artifact!,
220 "application/gzip",
221 ).pipe(Effect.flip);
222
223 expect(error.message).toContain("transport failed");
224 expect(String(error)).not.toContain("signature=secret");
225 }).pipe(
226 Effect.provide(Layer.succeed(HttpClient.HttpClient, http)),
227 Effect.provide(PlatformServices),
228 );
229 });
230
231 it.effect(
232 "streams file-backed artifacts with a fixed Content-Length",
233 () => {
234 let contentLength: string | undefined;
235 let uploadedBytes = 0;
236
237 return withHttpServer(
238 (request, response) => {
239 contentLength = request.headers["content-length"];
240 request.on("data", (chunk: Buffer) => {
241 uploadedBytes += chunk.byteLength;
242 });
243 request.on("end", () => {
244 response.statusCode = contentLength === undefined ? 411 : 204;
245 response.end();
246 });
247 },
248 (uploadUrl) =>
249 Effect.gen(function* () {
250 const fs = yield* FileSystem.FileSystem;
251 const path = yield* Path.Path;
252 const root = yield* fs.makeTempDirectory({
253 prefix: "alchemy-prisma-fixed-length-upload-",
254 });
255 const artifactPath = path.join(root, "artifact.tar.gz");
256 yield* fs.writeFileString(artifactPath, "fixed-length-archive");
257 const artifact = yield* readUploadArtifact({
258 artifactPath,
259 output: "file",
260 });
261
262 yield* executeArtifactUpload(
263 uploadUrl,
264 artifact!,
265 "application/gzip",
266 );
267
268 expect(contentLength).toBe(String(artifact!.size));
269 expect(uploadedBytes).toBe(artifact!.size);
270 }).pipe(
271 Effect.provide(PrismaHttpClientLive),
272 Effect.provide(PlatformServices),
273 Effect.scoped,
274 ),
275 );
276 },
277 );
278
279 it.effect(
280 "fails when Prisma omits an upload URL for version artifacts",
281 () => {
282 const calls: Array<[string, unknown?]> = [];
283 const client = {
284 createAppDeployment: () => {
285 calls.push(["createAppDeployment"]);
286 return Effect.succeed({
287 id: "version-1",
288 type: "deployment" as const,
289 url: "https://api.prisma.test/v1/deployments/version-1",
290 foundryVersionId: "foundry-1",
291 uploadUrl: null,
292 });
293 },
294 getDeployment: (id: string) => {
295 calls.push(["getDeployment", id]);
296 return Effect.succeed({
297 id,
298 type: "deployment" as const,
299 serviceId: "service-1",
300 url: `https://api.prisma.test/v1/deployments/${id}`,
301 foundryVersionId: "foundry-1",
302 status: "new",
303 previewDomain: null,
304 createdAt: "2026-01-01T00:00:00Z",
305 });
306 },
307 deleteDeployment: (id: string) => {
308 calls.push(["deleteDeployment", id]);
309 return Effect.void;
310 },
311 } as unknown as PrismaManagementClient;
312
313 return Effect.gen(function* () {
314 const provider = yield* PrismaDeployment.Provider;
315 const error = yield* provider
316 .reconcile({
317 id: "Version",
318 fqn: "Version",
319 instanceId: "00000000000000000000000000000000",
320 news: {
321 app: "service-1",
322 artifactPath: fixtureArtifactPath,
323 },
324 olds: undefined,
325 output: undefined,
326 session: undefined as never,
327 bindings: [],
328 })
329 .pipe(Effect.flip);
330
331 expect(error).toBeInstanceOf(Error);
332 expect(error.message).toContain("did not return an upload URL");
333 expect(calls).toContainEqual(["deleteDeployment", "version-1"]);
334 }).pipe(
335 Effect.provide(deploymentProviderLive()),
336 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
337 Effect.provide(clientBackedApi(client).layer),
338 Effect.provide(PlatformServices),
339 );
340 },
341 );
342
343 it.effect("deletes created deployment when artifact upload fails", () => {
344 const calls: Array<[string, unknown?]> = [];
345 const client = {
346 createAppDeployment: () => {
347 calls.push(["createAppDeployment"]);
348 return Effect.succeed({
349 id: "version-1",
350 type: "deployment" as const,
351 url: "https://api.prisma.test/v1/deployments/version-1",
352 foundryVersionId: "foundry-1",
353 uploadUrl: "https://upload.prisma.test/version.tar.gz",
354 });
355 },
356 getDeployment: (id: string) => {
357 calls.push(["getDeployment", id]);
358 return Effect.succeed({
359 id,
360 type: "deployment" as const,
361 serviceId: "service-1",
362 url: `https://api.prisma.test/v1/deployments/${id}`,
363 foundryVersionId: "foundry-1",
364 status: "new",
365 previewDomain: null,
366 createdAt: "2026-01-01T00:00:00Z",
367 });
368 },
369 deleteDeployment: (id: string) => {
370 calls.push(["deleteDeployment", id]);
371 return Effect.void;
372 },
373 } as unknown as PrismaManagementClient;
374 const http = HttpClient.make((request) =>
375 Effect.succeed(
376 HttpClientResponse.fromWeb(
377 request,
378 new Response("SIGNED_UPLOAD_SECRET_SENTINEL", { status: 500 }),
379 ),
380 ),
381 );
382
383 return Effect.gen(function* () {
384 const provider = yield* PrismaDeployment.Provider;
385 const error = yield* provider
386 .reconcile({
387 id: "Version",
388 fqn: "Version",
389 instanceId: "00000000000000000000000000000000",
390 news: {
391 app: "service-1",
392 artifactPath: fixtureArtifactPath,
393 },
394 olds: undefined,
395 output: undefined,
396 session: undefined as never,
397 bindings: [],
398 })
399 .pipe(Effect.flip);
400
401 expect(error).toBeInstanceOf(Error);
402 expect(error.message).toContain("artifact upload failed");
403 expect(error.message).toContain("HTTP 500");
404 expect(error.message).toContain("29 bytes");
405 expect(error.message).not.toContain("SIGNED_UPLOAD_SECRET_SENTINEL");
406 expect(calls).toContainEqual(["deleteDeployment", "version-1"]);
407 }).pipe(
408 Effect.provide(deploymentProviderLive()),
409 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
410 Effect.provide(clientBackedApi(client).layer),
411 Effect.provide(Layer.succeed(PrismaUploadClient, http)),
412 Effect.provide(PlatformServices),
413 );
414 });
415
416 it.effect(
417 "preserves reconcile and cleanup failures for an orphaned deployment",
418 () => {
419 const startError = new PrismaApiError({
420 method: "POST",
421 path: "/v1/deployments/version-1/start",
422 status: 400,
423 message: "start failed",
424 });
425 const deleteError = new PrismaApiError({
426 method: "DELETE",
427 path: "/v1/deployments/version-1",
428 status: 400,
429 message: "cleanup failed",
430 });
431 const client = {
432 createAppDeployment: () =>
433 Effect.succeed({
434 id: "version-1",
435 type: "deployment" as const,
436 url: "https://api.prisma.test/v1/deployments/version-1",
437 foundryVersionId: "foundry-1",
438 uploadUrl: null,
439 }),
440 getDeployment: (id: string) =>
441 Effect.succeed({
442 id,
443 type: "deployment" as const,
444 serviceId: "service-1",
445 url: `https://api.prisma.test/v1/deployments/${id}`,
446 foundryVersionId: "foundry-1",
447 status: "new",
448 previewDomain: null,
449 createdAt: "2026-01-01T00:00:00Z",
450 }),
451 startDeployment: () => Effect.fail(startError),
452 deleteDeployment: () => Effect.fail(deleteError),
453 } as unknown as PrismaManagementClient;
454
455 return Effect.gen(function* () {
456 const provider = yield* PrismaDeployment.Provider;
457 const error = yield* provider
458 .reconcile({
459 id: "Version",
460 fqn: "Version",
461 instanceId: "00000000000000000000000000000000",
462 news: {
463 app: "service-1",
464 skipCodeUpload: true,
465 start: true,
466 },
467 olds: undefined,
468 output: undefined,
469 session: undefined as never,
470 bindings: [],
471 })
472 .pipe(Effect.flip);
473
474 expect(error).toBeInstanceOf(AggregateError);
475 expect((error as AggregateError).message).toContain("version-1");
476 expect((error as AggregateError).message).toContain(
477 "DELETE /v1/deployments/version-1",
478 );
479 expect((error as AggregateError).errors).toHaveLength(2);
480 // Over the wire the injected failures decode into the typed errors.
481 expect(
482 ((error as AggregateError).errors[0] as Error).message,
483 ).toContain("start failed");
484 expect(
485 ((error as AggregateError).errors[1] as Error).message,
486 ).toContain("cleanup failed");
487 }).pipe(
488 Effect.provide(deploymentProviderLive()),
489 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
490 Effect.provide(clientBackedApi(client).layer),
491 Effect.provide(PlatformServices),
492 );
493 },
494 );
495
496 it.effect("deletes created deployment when start fails", () => {
497 const calls: Array<[string, unknown?]> = [];
498 const client = {
499 createAppDeployment: () => {
500 calls.push(["createAppDeployment"]);
501 return Effect.succeed({
502 id: "version-1",
503 type: "deployment" as const,
504 url: "https://api.prisma.test/v1/deployments/version-1",
505 foundryVersionId: "foundry-1",
506 uploadUrl: null,
507 });
508 },
509 getDeployment: (id: string) => {
510 calls.push(["getDeployment", id]);
511 return Effect.succeed({
512 id,
513 type: "deployment" as const,
514 serviceId: "service-1",
515 url: `https://api.prisma.test/v1/deployments/${id}`,
516 foundryVersionId: "foundry-1",
517 status: "new",
518 previewDomain: null,
519 createdAt: "2026-01-01T00:00:00Z",
520 });
521 },
522 startDeployment: (id: string) => {
523 calls.push(["startDeployment", id]);
524 return Effect.fail(
525 new PrismaApiError({
526 method: "POST",
527 path: `/v1/deployments/${id}/start`,
528 status: 400,
529 message: "start failed",
530 }),
531 );
532 },
533 deleteDeployment: (id: string) => {
534 calls.push(["deleteDeployment", id]);
535 return Effect.void;
536 },
537 } as unknown as PrismaManagementClient;
538
539 return Effect.gen(function* () {
540 const provider = yield* PrismaDeployment.Provider;
541 const error = yield* provider
542 .reconcile({
543 id: "Version",
544 fqn: "Version",
545 instanceId: "00000000000000000000000000000000",
546 news: {
547 app: "service-1",
548 skipCodeUpload: true,
549 start: true,
550 },
551 olds: undefined,
552 output: undefined,
553 session: undefined as never,
554 bindings: [],
555 })
556 .pipe(Effect.flip);
557
558 // Over the wire the injected failure decodes into the typed error.
559 expect(error._tag).toBe("BadRequest");
560 expect(error.message).toBe("start failed");
561 expect(calls).toContainEqual(["deleteDeployment", "version-1"]);
562 }).pipe(
563 Effect.provide(deploymentProviderLive()),
564 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
565 Effect.provide(clientBackedApi(client).layer),
566 Effect.provide(PlatformServices),
567 );
568 });
569
570 it.effect(
571 "preserves a deployment when promotion commit state is ambiguous",
572 () => {
573 const calls: Array<[string, unknown?]> = [];
574 let status = "new";
575 const client = {
576 createAppDeployment: () => {
577 calls.push(["createAppDeployment"]);
578 return Effect.succeed({
579 id: "version-1",
580 type: "deployment" as const,
581 url: "https://api.prisma.test/v1/deployments/version-1",
582 foundryVersionId: "foundry-1",
583 uploadUrl: null,
584 });
585 },
586 getDeployment: (id: string) => {
587 calls.push(["getDeployment", id]);
588 return Effect.succeed({
589 id,
590 type: "deployment" as const,
591 serviceId: "service-1",
592 url: `https://api.prisma.test/v1/deployments/${id}`,
593 foundryVersionId: "foundry-1",
594 status,
595 previewDomain: "version-1.preview.prisma.build",
596 createdAt: "2026-01-01T00:00:00Z",
597 });
598 },
599 listAppDeployments: () =>
600 Effect.succeed([
601 {
602 id: "version-1",
603 type: "deployment" as const,
604 serviceId: "service-1",
605 url: "https://api.prisma.test/v1/deployments/version-1",
606 foundryVersionId: "foundry-1",
607 createdAt: "2026-01-01T00:00:00Z",
608 },
609 ]),
610 startDeployment: (id: string) =>
611 Effect.sync(() => {
612 calls.push(["startDeployment", id]);
613 status = "running";
614 return { previewDomain: "version-1.preview.prisma.build" };
615 }),
616 getApp: (id: string) => {
617 calls.push(["getApp", id]);
618 return Effect.succeed({
619 id,
620 type: "app" as const,
621 url: `https://api.prisma.test/v1/services/${id}`,
622 name: "api",
623 region: { id: "us-east-1", name: "US East" },
624 projectId: "project-1",
625 branchId: null,
626 latestDeploymentId: null,
627 appEndpointDomain: "api.prisma.build",
628 createdAt: "2026-01-01T00:00:00Z",
629 });
630 },
631 promoteApp: (
632 appId: string,
633 { deploymentId }: { deploymentId: string },
634 ) => {
635 calls.push(["promoteApp", { appId, deploymentId }]);
636 return Effect.fail(
637 new PrismaApiError({
638 method: "POST",
639 path: `/v1/services/${appId}/promote`,
640 status: 400,
641 message: "promote failed",
642 }),
643 );
644 },
645 rollbackApp: () =>
646 Effect.fail(
647 new PrismaApiError({
648 method: "POST",
649 path: "/v1/services/service-1/rollback",
650 status: 400,
651 message: "promotion recovery failed",
652 }),
653 ),
654 stopDeployment: (id: string) =>
655 Effect.sync(() => {
656 calls.push(["stopDeployment", id]);
657 status = "stopped";
658 }),
659 deleteDeployment: (id: string) => {
660 calls.push(["deleteDeployment", id]);
661 return Effect.void;
662 },
663 } as unknown as PrismaManagementClient;
664
665 return Effect.gen(function* () {
666 const provider = yield* PrismaDeployment.Provider;
667 const error = yield* provider
668 .reconcile({
669 id: "Version",
670 fqn: "Version",
671 instanceId: "00000000000000000000000000000000",
672 news: {
673 app: "service-1",
674 skipCodeUpload: true,
675 promote: true,
676 },
677 olds: undefined,
678 output: undefined,
679 session: undefined as never,
680 bindings: [],
681 })
682 .pipe(Effect.flip);
683
684 expect(error).toBeInstanceOf(AggregateError);
685 expect((error as AggregateError).message).toContain("commit state");
686 expect(calls).not.toContainEqual(["deleteDeployment", "version-1"]);
687 }).pipe(
688 Effect.provide(deploymentProviderLive()),
689 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
690 Effect.provide(clientBackedApi(client).layer),
691 Effect.provide(PlatformServices),
692 );
693 },
694 );
695
696 it.effect("uploads version artifact bytes from artifactPath", () => {
697 let uploaded:
698 | { url: string; contentType: string | undefined; bytes: Uint8Array }
699 | undefined;
700 const client = {
701 createAppDeployment: (_appId: string, input: unknown) =>
702 Effect.succeed({
703 id: "version-1",
704 type: "deployment" as const,
705 url: "https://api.prisma.test/v1/deployments/version-1",
706 foundryVersionId: "foundry-1",
707 uploadUrl: "https://upload.prisma.test/version.tar.gz",
708 input,
709 }),
710 getDeployment: (id: string) =>
711 Effect.succeed({
712 id,
713 type: "deployment" as const,
714 serviceId: "service-1",
715 url: "https://api.prisma.test/v1/deployments/version-1",
716 foundryVersionId: "foundry-1",
717 status: "new",
718 previewDomain: "version-1.preview.prisma.build",
719 createdAt: "2026-01-01T00:00:00Z",
720 }),
721 } as unknown as PrismaManagementClient;
722 const http = HttpClient.make((request) =>
723 Effect.gen(function* () {
724 const body = request.body as HttpBody.HttpBody;
725 const bytes =
726 body._tag === "Uint8Array"
727 ? body.body
728 : body._tag === "Stream"
729 ? yield* Stream.runCollect(body.stream).pipe(
730 Effect.orDie,
731 Effect.map((chunks) => {
732 const output = new Uint8Array(
733 chunks.reduce(
734 (total, chunk) => total + chunk.byteLength,
735 0,
736 ),
737 );
738 let offset = 0;
739 for (const chunk of chunks) {
740 output.set(chunk, offset);
741 offset += chunk.byteLength;
742 }
743 return output;
744 }),
745 )
746 : new Uint8Array();
747 uploaded = {
748 url: request.url,
749 contentType:
750 body._tag === "Uint8Array" || body._tag === "Stream"
751 ? body.contentType
752 : undefined,
753 bytes,
754 };
755 return HttpClientResponse.fromWeb(request, new Response(null));
756 }),
757 );
758
759 return Effect.gen(function* () {
760 const fs = yield* FileSystem.FileSystem;
761 const path = yield* Path.Path;
762 const root = yield* fs.makeTempDirectory({
763 prefix: "alchemy-prisma-version-artifact-",
764 });
765 const artifactPath = path.join(root, "version.tar.gz");
766 yield* fs.writeFileString(artifactPath, "version-archive");
767
768 const provider = yield* PrismaDeployment.Provider;
769 const output = yield* provider.reconcile({
770 id: "Version",
771 fqn: "Version",
772 instanceId: "00000000000000000000000000000000",
773 news: {
774 app: "service-1",
775 artifactPath,
776 },
777 olds: undefined,
778 output: undefined,
779 session: undefined as never,
780 bindings: [],
781 });
782
783 expect(output.deploymentId).toBe("version-1");
784 expect(output.artifactHash).toBeDefined();
785 expect(uploaded?.url).toBe("https://upload.prisma.test/version.tar.gz");
786 expect(uploaded?.contentType).toBe("application/octet-stream");
787 expect(new TextDecoder().decode(uploaded?.bytes)).toBe(
788 "version-archive",
789 );
790 }).pipe(
791 Effect.provide(deploymentProviderLive()),
792 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
793 Effect.provide(clientBackedApi(client).layer),
794 Effect.provide(Layer.succeed(PrismaUploadClient, http)),
795 Effect.provide(PlatformServices),
796 );
797 });
798
799 it.effect("reads a saved deployment through the canonical route", () => {
800 const calls: Array<[string, unknown?]> = [];
801 const client = {
802 listAppDeployments: (appId: string) =>
803 Effect.sync(() => {
804 calls.push(["listAppDeployments", appId]);
805 return [
806 {
807 id: "version-1",
808 type: "deployment" as const,
809 serviceId: "service-1",
810 url: "https://api.prisma.test/v1/deployments/version-1",
811 foundryVersionId: "foundry-1",
812 createdAt: "2026-01-01T00:00:00Z",
813 },
814 ];
815 }),
816 getDeployment: (id: string) =>
817 Effect.sync(() => {
818 calls.push(["getDeployment", id]);
819 return {
820 id,
821 type: "deployment" as const,
822 serviceId: "service-1",
823 url: `https://api.prisma.test/v1/deployments/${id}`,
824 foundryVersionId: "foundry-1",
825 status: "stopped",
826 previewDomain: "version-1.preview.prisma.build",
827 createdAt: "2026-01-01T00:00:00Z",
828 };
829 }),
830 } as unknown as PrismaManagementClient;
831
832 return Effect.gen(function* () {
833 const provider = yield* Provider.findProvider(PrismaDeployment);
834 const output = yield* provider.read!({
835 id: "Version",
836 fqn: "Version",
837 instanceId: "00000000000000000000000000000000",
838 olds: {
839 app: "service-1",
840 },
841 output: {
842 deploymentId: "version-1",
843 appId: "service-1",
844 foundryVersionId: "foundry-1",
845 status: "running",
846 previewDomain: undefined,
847 artifactHash: undefined,
848 appEndpointDomain: undefined,
849 createdAt: "2026-01-01T00:00:00Z",
850 },
851 });
852
853 expect(output?.deploymentId).toBe("version-1");
854 expect(output?.status).toBe("stopped");
855 expect(calls).toEqual([
856 ["getDeployment", "version-1"],
857 ["listAppDeployments", "service-1"],
858 ]);
859 }).pipe(
860 Effect.provide(deploymentProviderLive()),
861 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
862 Effect.provide(clientBackedApi(client).layer),
863 Effect.provide(PlatformServices),
864 );
865 });
866
867 it.effect(
868 "does not adopt an unrelated latest deployment on a cold read",
869 () => {
870 const client = {
871 listAppDeployments: () =>
872 Effect.die("cold reads must not enumerate or adopt deployments"),
873 getDeployment: () =>
874 Effect.die("cold reads have no persisted deployment identity"),
875 } as unknown as PrismaManagementClient;
876
877 return Effect.gen(function* () {
878 const provider = yield* Provider.findProvider(PrismaDeployment);
879 const output = yield* provider.read!({
880 id: "Deployment",
881 fqn: "Deployment",
882 instanceId: "00000000000000000000000000000000",
883 olds: { app: "app-1" },
884 output: undefined,
885 });
886
887 expect(output).toBeUndefined();
888 }).pipe(
889 Effect.provide(deploymentProviderLive()),
890 Effect.provide(Layer.succeed(PrismaClient, client)),
891 Effect.provide(clientBackedApi(client).layer),
892 Effect.provide(PlatformServices),
893 );
894 },
895 );
896
897 it.effect("uses the output App ID when reading a saved deployment", () => {
898 const calls: Array<[string, unknown?]> = [];
899 const client = {
900 listAppDeployments: (appId: string) =>
901 Effect.sync(() => {
902 calls.push(["listAppDeployments", appId]);
903 return [
904 {
905 id: "version-1",
906 type: "deployment" as const,
907 serviceId: "service-from-output",
908 url: "https://api.prisma.test/v1/deployments/version-1",
909 foundryVersionId: "foundry-1",
910 createdAt: "2026-01-01T00:00:00Z",
911 },
912 ];
913 }),
914 getDeployment: (id: string) =>
915 Effect.sync(() => {
916 calls.push(["getDeployment", id]);
917 return {
918 id,
919 type: "deployment" as const,
920 serviceId: "service-from-output",
921 url: `https://api.prisma.test/v1/deployments/${id}`,
922 foundryVersionId: "foundry-1",
923 status: "running",
924 previewDomain: "version-1.preview.prisma.build",
925 createdAt: "2026-01-01T00:00:00Z",
926 };
927 }),
928 } as unknown as PrismaManagementClient;
929
930 return Effect.gen(function* () {
931 const provider = yield* Provider.findProvider(PrismaDeployment);
932 const output = yield* provider.read!({
933 id: "Version",
934 fqn: "Version",
935 instanceId: "00000000000000000000000000000000",
936 olds: {
937 app: "service-from-olds",
938 },
939 output: {
940 deploymentId: "version-1",
941 appId: "service-from-output",
942 foundryVersionId: "foundry-1",
943 status: "stopped",
944 previewDomain: undefined,
945 artifactHash: undefined,
946 appEndpointDomain: undefined,
947 createdAt: "2026-01-01T00:00:00Z",
948 },
949 });
950
951 expect(output?.appId).toBe("service-from-output");
952 expect(output?.status).toBe("running");
953 expect(calls).toEqual([
954 ["getDeployment", "version-1"],
955 ["listAppDeployments", "service-from-output"],
956 ]);
957 }).pipe(
958 Effect.provide(deploymentProviderLive()),
959 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
960 Effect.provide(clientBackedApi(client).layer),
961 Effect.provide(PlatformServices),
962 );
963 });
964
965 it.effect(
966 "falls back to the foundry version ID when the saved deployment is gone",
967 () => {
968 const calls: Array<[string, unknown?]> = [];
969 const notFound = (path: string) =>
970 new PrismaApiError({
971 method: "GET",
972 path,
973 status: 404,
974 message: "not found",
975 });
976 const client = {
977 listAppDeployments: (appId: string, query: unknown) =>
978 Effect.sync(() => {
979 calls.push(["listAppDeployments", { appId, query }]);
980 return [
981 {
982 id: "version-new",
983 type: "deployment" as const,
984 serviceId: "service-from-output",
985 url: "https://api.prisma.test/v1/deployments/version-new",
986 foundryVersionId: "foundry-1",
987 createdAt: "2026-01-01T00:00:00Z",
988 },
989 ];
990 }),
991 getDeployment: (id: string) =>
992 Effect.gen(function* () {
993 calls.push(["getDeployment", id]);
994 if (id === "version-old") {
995 return yield* Effect.fail(notFound(`/v1/deployments/${id}`));
996 }
997 return {
998 id,
999 type: "deployment" as const,
1000 serviceId: "service-from-output",
1001 url: `https://api.prisma.test/v1/deployments/${id}`,
1002 foundryVersionId: "foundry-1",
1003 status: "running",
1004 previewDomain: "version-new.preview.prisma.build",
1005 createdAt: "2026-01-01T00:00:00Z",
1006 };
1007 }),
1008 } as unknown as PrismaManagementClient;
1009
1010 return Effect.gen(function* () {
1011 const provider = yield* Provider.findProvider(PrismaDeployment);
1012 const output = yield* provider.read!({
1013 id: "Version",
1014 fqn: "Version",
1015 instanceId: "00000000000000000000000000000000",
1016 olds: {
1017 app: "service-from-olds",
1018 },
1019 output: {
1020 deploymentId: "version-old",
1021 appId: "service-from-output",
1022 foundryVersionId: "foundry-1",
1023 status: "stopped",
1024 previewDomain: undefined,
1025 artifactHash: undefined,
1026 appEndpointDomain: undefined,
1027 createdAt: "2026-01-01T00:00:00Z",
1028 },
1029 });
1030
1031 expect(output?.deploymentId).toBe("version-new");
1032 expect(output?.appId).toBe("service-from-output");
1033 expect(calls).toEqual([
1034 ["getDeployment", "version-old"],
1035 [
1036 "listAppDeployments",
1037 // Over the wire, query params arrive as strings.
1038 { appId: "service-from-output", query: { limit: "100" } },
1039 ],
1040 ["getDeployment", "version-new"],
1041 ]);
1042 }).pipe(
1043 Effect.provide(deploymentProviderLive()),
1044 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1045 Effect.provide(clientBackedApi(client).layer),
1046 Effect.provide(PlatformServices),
1047 );
1048 },
1049 );
1050
1051 it.effect("rejects ambiguous Foundry-version recovery matches", () => {
1052 const client = {
1053 getDeployment: (id: string) =>
1054 Effect.fail(
1055 new PrismaApiError({
1056 method: "GET",
1057 path: `/v1/deployments/${id}`,
1058 status: 404,
1059 message: "not found",
1060 }),
1061 ),
1062 listAppDeployments: () =>
1063 Effect.succeed([
1064 {
1065 id: "deployment-a",
1066 type: "deployment" as const,
1067 serviceId: "service-1",
1068 url: "https://api.prisma.test/v1/deployments/deployment-a",
1069 foundryVersionId: "foundry-1",
1070 createdAt: "2026-01-01T00:00:00Z",
1071 },
1072 {
1073 id: "deployment-b",
1074 type: "deployment" as const,
1075 serviceId: "service-1",
1076 url: "https://api.prisma.test/v1/deployments/deployment-b",
1077 foundryVersionId: "foundry-1",
1078 createdAt: "2026-01-01T00:00:01Z",
1079 },
1080 ]),
1081 } as unknown as PrismaManagementClient;
1082
1083 return Effect.gen(function* () {
1084 const provider = yield* Provider.findProvider(PrismaDeployment);
1085 const error = yield* provider.read!({
1086 id: "Deployment",
1087 fqn: "Deployment",
1088 instanceId: "00000000000000000000000000000000",
1089 olds: { app: "app-1" },
1090 output: {
1091 deploymentId: "deployment-missing",
1092 appId: "app-1",
1093 foundryVersionId: "foundry-1",
1094 status: "new",
1095 previewDomain: null,
1096 appEndpointDomain: undefined,
1097 createdAt: undefined,
1098 },
1099 }).pipe(Effect.flip);
1100
1101 expect(error.message).toContain("ambiguous recovery match");
1102 }).pipe(
1103 Effect.provide(deploymentProviderLive()),
1104 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1105 Effect.provide(clientBackedApi(client).layer),
1106 Effect.provide(PlatformServices),
1107 );
1108 });
1109
1110 it.effect("replaces when artifactPath contents change", () => {
1111 const client = {} as PrismaManagementClient;
1112
1113 return Effect.gen(function* () {
1114 const fs = yield* FileSystem.FileSystem;
1115 const path = yield* Path.Path;
1116 const root = yield* fs.makeTempDirectory({
1117 prefix: "alchemy-prisma-version-diff-",
1118 });
1119 const artifactPath = path.join(root, "version.tar.gz");
1120 yield* fs.writeFileString(artifactPath, "new-version");
1121
1122 const provider = yield* PrismaDeployment.Provider;
1123 const diff = yield* provider.diff!({
1124 id: "Version",
1125 fqn: "Version",
1126 instanceId: "00000000000000000000000000000000",
1127 olds: {
1128 app: "service-1",
1129 artifactPath,
1130 },
1131 news: {
1132 app: "service-1",
1133 artifactPath,
1134 },
1135 oldBindings: [],
1136 newBindings: [],
1137 output: {
1138 deploymentId: "version-1",
1139 appId: "service-1",
1140 foundryVersionId: "foundry-1",
1141 status: "new",
1142 previewDomain: null,
1143 artifactHash: "old-hash",
1144 appEndpointDomain: undefined,
1145 createdAt: "2026-01-01T00:00:00Z",
1146 },
1147 } as never);
1148
1149 expect(diff).toEqual({ action: "replace" });
1150 }).pipe(
1151 Effect.provide(deploymentProviderLive()),
1152 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1153 Effect.provide(clientBackedApi(client).layer),
1154 Effect.provide(PlatformServices),
1155 );
1156 });
1157
1158 it.effect("replaces changed artifacts even when app is unresolved", () => {
1159 const client = {} as PrismaManagementClient;
1160
1161 return Effect.gen(function* () {
1162 const fs = yield* FileSystem.FileSystem;
1163 const path = yield* Path.Path;
1164 const root = yield* fs.makeTempDirectory({
1165 prefix: "alchemy-prisma-version-unresolved-diff-",
1166 });
1167 const artifactPath = path.join(root, "version.tar.gz");
1168 yield* fs.writeFileString(artifactPath, "new-version");
1169
1170 const provider = yield* PrismaDeployment.Provider;
1171 const diff = yield* provider.diff!({
1172 id: "Version",
1173 fqn: "Version",
1174 instanceId: "00000000000000000000000000000000",
1175 olds: {
1176 app: "service-1",
1177 artifactPath,
1178 },
1179 news: {
1180 app: Output.asOutput("service-1"),
1181 artifactPath,
1182 },
1183 oldBindings: [],
1184 newBindings: [],
1185 output: {
1186 deploymentId: "version-1",
1187 appId: "service-1",
1188 foundryVersionId: "foundry-1",
1189 status: "new",
1190 previewDomain: null,
1191 artifactHash: "old-hash",
1192 appEndpointDomain: undefined,
1193 createdAt: "2026-01-01T00:00:00Z",
1194 },
1195 } as never);
1196
1197 expect(diff).toEqual({ action: "replace" });
1198 }).pipe(
1199 Effect.provide(deploymentProviderLive()),
1200 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1201 Effect.provide(clientBackedApi(client).layer),
1202 Effect.provide(PlatformServices),
1203 );
1204 });
1205
1206 it.effect(
1207 "replaces when the persisted App changes from an unresolved old prop",
1208 () => {
1209 const client = {} as PrismaManagementClient;
1210
1211 return Effect.gen(function* () {
1212 const artifact = new TextEncoder().encode("same-version\n");
1213 const artifactHash = yield* sha256Object({
1214 artifact: yield* sha256(artifact),
1215 contentType: "application/octet-stream",
1216 });
1217 const provider = yield* PrismaDeployment.Provider;
1218 const diff = yield* provider.diff!({
1219 id: "Deployment",
1220 fqn: "Deployment",
1221 instanceId: "00000000000000000000000000000000",
1222 olds: {
1223 app: Output.asOutput("app-old"),
1224 artifactPath: sameArtifactPath,
1225 },
1226 news: {
1227 app: "app-new",
1228 artifactPath: sameArtifactPath,
1229 },
1230 oldBindings: [],
1231 newBindings: [],
1232 output: {
1233 deploymentId: "deployment-1",
1234 appId: "app-old",
1235 foundryVersionId: "foundry-1",
1236 status: "new",
1237 previewDomain: null,
1238 artifactHash,
1239 appEndpointDomain: undefined,
1240 createdAt: "2026-01-01T00:00:00Z",
1241 },
1242 } as never);
1243
1244 expect(diff).toEqual({ action: "replace" });
1245 }).pipe(
1246 Effect.provide(deploymentProviderLive()),
1247 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1248 Effect.provide(clientBackedApi(client).layer),
1249 Effect.provide(PlatformServices),
1250 );
1251 },
1252 );
1253
1254 it.effect("does not replace when artifact bytes are unchanged", () => {
1255 const client = {} as PrismaManagementClient;
1256
1257 return Effect.gen(function* () {
1258 const artifact = new TextEncoder().encode("same-version\n");
1259 const artifactHash = yield* sha256Object({
1260 artifact: yield* sha256(artifact),
1261 contentType: "application/octet-stream",
1262 });
1263
1264 const provider = yield* PrismaDeployment.Provider;
1265 const diff = yield* provider.diff!({
1266 id: "Version",
1267 fqn: "Version",
1268 instanceId: "00000000000000000000000000000000",
1269 olds: {
1270 app: "service-1",
1271 artifactPath: sameArtifactPath,
1272 },
1273 news: {
1274 app: "service-1",
1275 artifactPath: sameArtifactPath,
1276 },
1277 oldBindings: [],
1278 newBindings: [],
1279 output: {
1280 deploymentId: "version-1",
1281 appId: "service-1",
1282 foundryVersionId: "foundry-1",
1283 status: "new",
1284 previewDomain: null,
1285 artifactHash,
1286 appEndpointDomain: undefined,
1287 createdAt: "2026-01-01T00:00:00Z",
1288 },
1289 } as never);
1290
1291 expect(diff).toBeUndefined();
1292 }).pipe(
1293 Effect.provide(deploymentProviderLive()),
1294 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1295 Effect.provide(clientBackedApi(client).layer),
1296 Effect.provide(PlatformServices),
1297 );
1298 });
1299
1300 it.effect("replays promotion to repair Compute endpoint drift", () => {
1301 const calls: Array<[string, unknown?]> = [];
1302 let latestDeploymentId: string | null = null;
1303 let status = "new";
1304
1305 const service = () => ({
1306 id: "service-1",
1307 type: "app" as const,
1308 url: "https://api.prisma.test/v1/services/service-1",
1309 name: "api",
1310 region: { id: "us-east-1", name: "US East" },
1311 projectId: "project-1",
1312 branchId: null,
1313 latestDeploymentId,
1314 appEndpointDomain: "api.prisma.build",
1315 createdAt: "2026-01-01T00:00:00Z",
1316 });
1317
1318 const client = {
1319 createAppDeployment: (appId: string, input: unknown) => {
1320 calls.push(["createAppDeployment", { appId, input }]);
1321 return Effect.succeed({
1322 id: "version-1",
1323 type: "deployment" as const,
1324 url: "https://api.prisma.test/v1/deployments/version-1",
1325 foundryVersionId: "foundry-1",
1326 uploadUrl: null,
1327 });
1328 },
1329 getDeployment: (id: string) => {
1330 calls.push(["getDeployment", id]);
1331 return Effect.succeed({
1332 id,
1333 type: "deployment" as const,
1334 serviceId: "service-1",
1335 url: `https://api.prisma.test/v1/deployments/${id}`,
1336 foundryVersionId: "foundry-1",
1337 status,
1338 previewDomain: "version-1.preview.prisma.build",
1339 createdAt: "2026-01-01T00:00:00Z",
1340 });
1341 },
1342 listAppDeployments: () =>
1343 Effect.succeed([
1344 {
1345 id: "version-1",
1346 type: "deployment" as const,
1347 serviceId: "service-1",
1348 url: "https://api.prisma.test/v1/deployments/version-1",
1349 foundryVersionId: "foundry-1",
1350 createdAt: "2026-01-01T00:00:00Z",
1351 },
1352 ]),
1353 startDeployment: (id: string) =>
1354 Effect.sync(() => {
1355 calls.push(["startDeployment", id]);
1356 status = "running";
1357 return { previewDomain: "version-1.preview.prisma.build" };
1358 }),
1359 getApp: (id: string) => {
1360 calls.push(["getApp", id]);
1361 return Effect.succeed(service());
1362 },
1363 promoteApp: (
1364 appId: string,
1365 { deploymentId }: { deploymentId: string },
1366 ) =>
1367 Effect.sync(() => {
1368 calls.push(["promoteApp", { appId, deploymentId }]);
1369 latestDeploymentId = deploymentId;
1370 return { appEndpointDomain: "api.prisma.build" };
1371 }),
1372 } as unknown as PrismaManagementClient;
1373
1374 const news = {
1375 app: "service-1",
1376 skipCodeUpload: true,
1377 promote: true,
1378 };
1379
1380 return Effect.gen(function* () {
1381 const provider = yield* PrismaDeployment.Provider;
1382 const first = yield* provider.reconcile({
1383 id: "Version",
1384 fqn: "Version",
1385 instanceId: "00000000000000000000000000000000",
1386 news,
1387 olds: undefined,
1388 output: undefined,
1389 session: undefined as never,
1390 bindings: [],
1391 });
1392
1393 // Simulate an out-of-band stop between deployments. Reconcile must
1394 // restore both running state and App routing even though props match.
1395 status = "stopped";
1396
1397 const second = yield* provider.reconcile({
1398 id: "Version",
1399 fqn: "Version",
1400 instanceId: "00000000000000000000000000000000",
1401 news,
1402 olds: news,
1403 output: first,
1404 session: undefined as never,
1405 bindings: [],
1406 });
1407
1408 expect(first.deploymentId).toBe("version-1");
1409 expect(second.deploymentId).toBe("version-1");
1410 expect(second.appEndpointDomain).toBe("api.prisma.build");
1411 expect(calls.filter(([name]) => name === "startDeployment")).toEqual([
1412 ["startDeployment", "version-1"],
1413 ["startDeployment", "version-1"],
1414 ]);
1415 expect(calls.filter(([name]) => name === "promoteApp")).toEqual([
1416 ["promoteApp", { appId: "service-1", deploymentId: "version-1" }],
1417 ["promoteApp", { appId: "service-1", deploymentId: "version-1" }],
1418 ]);
1419 expect(
1420 calls.filter(([name]) => name === "createAppDeployment"),
1421 ).toHaveLength(1);
1422 }).pipe(
1423 Effect.provide(deploymentProviderLive()),
1424 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1425 Effect.provide(clientBackedApi(client).layer),
1426 Effect.provide(PlatformServices),
1427 );
1428 });
1429
1430 it.effect(
1431 "replaces a terminal failed deployment before deleting the failed generation",
1432 () => {
1433 const calls: Array<[string, unknown?]> = [];
1434 let latestDeploymentId: string | null = "version-failed";
1435 const deployments = new Map<
1436 string,
1437 {
1438 id: string;
1439 type: "deployment";
1440 serviceId: string;
1441 url: string;
1442 foundryVersionId: string;
1443 status: string;
1444 previewDomain: string | null;
1445 createdAt: string;
1446 }
1447 >([
1448 [
1449 "version-failed",
1450 {
1451 id: "version-failed",
1452 type: "deployment",
1453 serviceId: "service-1",
1454 url: "https://api.prisma.test/v1/deployments/version-failed",
1455 foundryVersionId: "foundry-failed",
1456 status: "failed",
1457 previewDomain: "version-failed.preview.prisma.build",
1458 createdAt: "2026-01-01T00:00:00Z",
1459 },
1460 ],
1461 ]);
1462 const client = {
1463 createAppDeployment: (appId: string, input: unknown) =>
1464 Effect.sync(() => {
1465 calls.push(["createAppDeployment", { appId, input }]);
1466 deployments.set("version-replacement", {
1467 id: "version-replacement",
1468 type: "deployment",
1469 serviceId: "service-1",
1470 url: "https://api.prisma.test/v1/deployments/version-replacement",
1471 foundryVersionId: "foundry-replacement",
1472 status: "new",
1473 previewDomain: "version-replacement.preview.prisma.build",
1474 createdAt: "2026-01-02T00:00:00Z",
1475 });
1476 return {
1477 id: "version-replacement",
1478 type: "deployment" as const,
1479 url: "https://api.prisma.test/v1/deployments/version-replacement",
1480 foundryVersionId: "foundry-replacement",
1481 uploadUrl: null,
1482 };
1483 }),
1484 getDeployment: (id: string) =>
1485 Effect.sync(() => {
1486 calls.push(["getDeployment", id]);
1487 return deployments.get(id)!;
1488 }),
1489 listAppDeployments: () =>
1490 Effect.succeed(
1491 Array.from(deployments.values()).map(
1492 ({
1493 id,
1494 type,
1495 serviceId,
1496 url,
1497 foundryVersionId,
1498 createdAt,
1499 }) => ({
1500 id,
1501 type,
1502 serviceId,
1503 url,
1504 foundryVersionId,
1505 createdAt,
1506 }),
1507 ),
1508 ),
1509 startDeployment: (id: string) =>
1510 Effect.sync(() => {
1511 calls.push(["startDeployment", id]);
1512 deployments.get(id)!.status = "running";
1513 return {
1514 previewDomain: deployments.get(id)!.previewDomain!,
1515 };
1516 }),
1517 promoteApp: (
1518 appId: string,
1519 { deploymentId }: { deploymentId: string },
1520 ) =>
1521 Effect.sync(() => {
1522 calls.push(["promoteApp", { appId, deploymentId }]);
1523 expect(deployments.has("version-failed")).toBe(true);
1524 latestDeploymentId = deploymentId;
1525 return {
1526 appEndpointDomain: "api.prisma.build",
1527 reassignedDomains: 0,
1528 };
1529 }),
1530 getApp: (appId: string) =>
1531 Effect.succeed({
1532 id: appId,
1533 type: "app" as const,
1534 url: `https://api.prisma.test/v1/services/${appId}`,
1535 name: "api",
1536 region: { id: "us-east-1", name: "US East" },
1537 projectId: "project-1",
1538 branchId: "branch-main",
1539 latestDeploymentId,
1540 appEndpointDomain: "api.prisma.build",
1541 createdAt: "2026-01-01T00:00:00Z",
1542 }),
1543 deleteDeployment: (id: string) =>
1544 Effect.sync(() => {
1545 calls.push(["deleteDeployment", id]);
1546 deployments.delete(id);
1547 }),
1548 } as unknown as PrismaManagementClient;
1549 const news = {
1550 app: "service-1",
1551 skipCodeUpload: true,
1552 promote: true,
1553 } as const;
1554 const failedOutput: PrismaDeployment["Attributes"] = {
1555 deploymentId: "version-failed",
1556 appId: "service-1",
1557 foundryVersionId: "foundry-failed",
1558 status: "failed",
1559 previewDomain: "version-failed.preview.prisma.build",
1560 artifactHash: undefined,
1561 appEndpointDomain: "api.prisma.build",
1562 createdAt: "2026-01-01T00:00:00Z",
1563 };
1564
1565 return Effect.gen(function* () {
1566 const provider = yield* PrismaDeployment.Provider;
1567 const action = yield* provider.diff!({
1568 id: "Version",
1569 fqn: "Version",
1570 instanceId: "00000000000000000000000000000000",
1571 olds: news,
1572 news,
1573 oldBindings: [],
1574 newBindings: [],
1575 output: failedOutput,
1576 } as never);
1577 expect(action).toEqual({ action: "replace" });
1578
1579 // The engine executes replacement create-before-delete. Reconcile the
1580 // replacement to its requested running/promoted state before invoking
1581 // delete for the failed generation.
1582 const replacement = yield* provider.reconcile({
1583 id: "Version",
1584 fqn: "Version",
1585 instanceId: "11111111111111111111111111111111",
1586 news,
1587 olds: undefined,
1588 output: undefined,
1589 session: undefined as never,
1590 bindings: [],
1591 });
1592 expect(replacement.deploymentId).toBe("version-replacement");
1593 expect(replacement.status).toBe("running");
1594 expect(deployments.has("version-failed")).toBe(true);
1595
1596 yield* provider.delete({
1597 id: "Version",
1598 fqn: "Version",
1599 instanceId: "00000000000000000000000000000000",
1600 olds: news,
1601 output: failedOutput,
1602 session: undefined as never,
1603 bindings: [],
1604 });
1605
1606 const createIndex = calls.findIndex(
1607 ([operation]) => operation === "createAppDeployment",
1608 );
1609 const promoteIndex = calls.findIndex(
1610 ([operation]) => operation === "promoteApp",
1611 );
1612 const deleteIndex = calls.findIndex(
1613 ([operation, id]) =>
1614 operation === "deleteDeployment" && id === "version-failed",
1615 );
1616 expect(createIndex).toBeGreaterThanOrEqual(0);
1617 expect(createIndex).toBeLessThan(promoteIndex);
1618 expect(promoteIndex).toBeLessThan(deleteIndex);
1619 expect(deployments.has("version-failed")).toBe(false);
1620 expect(deployments.get("version-replacement")?.status).toBe(
1621 "running",
1622 );
1623 expect(calls).not.toContainEqual([
1624 "startDeployment",
1625 "version-failed",
1626 ]);
1627 }).pipe(
1628 Effect.provide(deploymentProviderLive()),
1629 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1630 Effect.provide(clientBackedApi(client).layer),
1631 Effect.provide(PlatformServices),
1632 );
1633 },
1634 );
1635
1636 it.effect("rejects promotion when start is explicitly disabled", () => {
1637 const client = {} as PrismaManagementClient;
1638
1639 return Effect.gen(function* () {
1640 const provider = yield* PrismaDeployment.Provider;
1641 const error = yield* provider
1642 .reconcile({
1643 id: "Version",
1644 fqn: "Version",
1645 instanceId: "00000000000000000000000000000000",
1646 news: {
1647 app: "service-1",
1648 skipCodeUpload: true,
1649 start: false,
1650 promote: true,
1651 },
1652 olds: undefined,
1653 output: undefined,
1654 session: undefined as never,
1655 bindings: [],
1656 })
1657 .pipe(Effect.flip);
1658
1659 expect(error).toBeInstanceOf(Error);
1660 expect(error.message).toContain(
1661 "promote cannot be combined with start: false",
1662 );
1663 }).pipe(
1664 Effect.provide(deploymentProviderLive()),
1665 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1666 Effect.provide(clientBackedApi(client).layer),
1667 Effect.provide(PlatformServices),
1668 );
1669 });
1670
1671 it.effect("rejects invalid deployment HTTP ports", () => {
1672 const client = {} as PrismaManagementClient;
1673
1674 return Effect.gen(function* () {
1675 const provider = yield* PrismaDeployment.Provider;
1676 for (const http of [0, 65_536, 1.5]) {
1677 const error = yield* provider
1678 .reconcile({
1679 id: "Deployment",
1680 fqn: "Deployment",
1681 instanceId: "00000000000000000000000000000000",
1682 news: {
1683 app: "app-1",
1684 skipCodeUpload: true,
1685 portMapping: { http },
1686 },
1687 olds: undefined,
1688 output: undefined,
1689 session: undefined as never,
1690 bindings: [],
1691 })
1692 .pipe(Effect.flip);
1693 expect(error.message).toContain(
1694 "portMapping.http must be an integer between 1 and 65535",
1695 );
1696 }
1697 }).pipe(
1698 Effect.provide(deploymentProviderLive()),
1699 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1700 Effect.provide(clientBackedApi(client).layer),
1701 Effect.provide(PlatformServices),
1702 );
1703 });
1704
1705 it.effect(
1706 "allows null HTTP port to request the Foundry 8080 default",
1707 () => {
1708 const calls: Array<[string, unknown?]> = [];
1709 const client = {
1710 createAppDeployment: (appId: string, input: unknown) =>
1711 Effect.sync(() => {
1712 calls.push(["createAppDeployment", { appId, input }]);
1713 return {
1714 id: "deployment-1",
1715 type: "deployment" as const,
1716 url: "https://api.prisma.test/v1/deployments/deployment-1",
1717 foundryVersionId: "foundry-1",
1718 uploadUrl: null,
1719 };
1720 }),
1721 getDeployment: () =>
1722 Effect.succeed({
1723 id: "deployment-1",
1724 type: "deployment" as const,
1725 serviceId: "service-1",
1726 url: "https://api.prisma.test/v1/deployments/deployment-1",
1727 foundryVersionId: "foundry-1",
1728 status: "new",
1729 previewDomain: null,
1730 createdAt: "2026-01-01T00:00:00Z",
1731 }),
1732 } as unknown as PrismaManagementClient;
1733
1734 return Effect.gen(function* () {
1735 const provider = yield* PrismaDeployment.Provider;
1736 const output = yield* provider.reconcile({
1737 id: "Deployment",
1738 fqn: "Deployment",
1739 instanceId: "00000000000000000000000000000000",
1740 news: {
1741 app: "app-1",
1742 skipCodeUpload: true,
1743 portMapping: { http: null },
1744 },
1745 olds: undefined,
1746 output: undefined,
1747 session: undefined as never,
1748 bindings: [],
1749 });
1750
1751 expect(output.deploymentId).toBe("deployment-1");
1752 expect(calls).toEqual([
1753 [
1754 "createAppDeployment",
1755 {
1756 appId: "app-1",
1757 input: { portMapping: { http: null }, skipCodeUpload: true },
1758 },
1759 ],
1760 ]);
1761 }).pipe(
1762 Effect.provide(deploymentProviderLive()),
1763 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1764 Effect.provide(clientBackedApi(client).layer),
1765 Effect.provide(PlatformServices),
1766 );
1767 },
1768 );
1769
1770 it.effect("forces reconcile while start or promotion is asserted", () => {
1771 const client = {} as PrismaManagementClient;
1772
1773 return Effect.gen(function* () {
1774 const provider = yield* PrismaDeployment.Provider;
1775 const base = {
1776 id: "Deployment",
1777 fqn: "Deployment",
1778 instanceId: "00000000000000000000000000000000",
1779 oldBindings: [],
1780 newBindings: [],
1781 output: {
1782 deploymentId: "deployment-1",
1783 appId: "app-1",
1784 foundryVersionId: "foundry-1",
1785 status: "running",
1786 previewDomain: null,
1787 appEndpointDomain: undefined,
1788 createdAt: undefined,
1789 },
1790 };
1791 const start = yield* provider.diff!({
1792 ...base,
1793 olds: { app: "app-1", skipCodeUpload: true, start: true },
1794 news: { app: "app-1", skipCodeUpload: true, start: true },
1795 } as never);
1796 const promote = yield* provider.diff!({
1797 ...base,
1798 olds: { app: "app-1", skipCodeUpload: true, promote: true },
1799 news: { app: "app-1", skipCodeUpload: true, promote: true },
1800 } as never);
1801
1802 expect(start).toEqual({ action: "update" });
1803 expect(promote).toEqual({ action: "update" });
1804 }).pipe(
1805 Effect.provide(deploymentProviderLive()),
1806 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1807 Effect.provide(clientBackedApi(client).layer),
1808 Effect.provide(PlatformServices),
1809 );
1810 });
1811
1812 it.effect("rejects a deployment with no artifact source", () => {
1813 const client = {} as PrismaManagementClient;
1814
1815 return Effect.gen(function* () {
1816 const provider = yield* PrismaDeployment.Provider;
1817 const error = yield* provider
1818 .reconcile({
1819 id: "Deployment",
1820 fqn: "Deployment",
1821 instanceId: "00000000000000000000000000000000",
1822 news: { app: "app-1" },
1823 olds: undefined,
1824 output: undefined,
1825 session: undefined as never,
1826 bindings: [],
1827 })
1828 .pipe(Effect.flip);
1829
1830 expect(error.message).toContain(
1831 "requires artifactPath or skipCodeUpload: true",
1832 );
1833 }).pipe(
1834 Effect.provide(deploymentProviderLive()),
1835 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1836 Effect.provide(clientBackedApi(client).layer),
1837 Effect.provide(PlatformServices),
1838 );
1839 });
1840
1841 it.effect("starts a deployment through the canonical route", () => {
1842 const calls: Array<[string, unknown?]> = [];
1843 let status = "new";
1844 const client = {
1845 createAppDeployment: (appId: string, input: unknown) => {
1846 calls.push(["createAppDeployment", { appId, input }]);
1847 return Effect.succeed({
1848 id: "version-1",
1849 type: "deployment" as const,
1850 url: "https://api.prisma.test/v1/deployments/version-1",
1851 foundryVersionId: "foundry-1",
1852 uploadUrl: null,
1853 });
1854 },
1855 getDeployment: (id: string) => {
1856 calls.push(["getDeployment", id]);
1857 return Effect.succeed({
1858 id,
1859 type: "deployment" as const,
1860 serviceId: "service-1",
1861 url: `https://api.prisma.test/v1/deployments/${id}`,
1862 foundryVersionId: "foundry-1",
1863 status,
1864 previewDomain: "version-1.preview.prisma.build",
1865 createdAt: "2026-01-01T00:00:00Z",
1866 });
1867 },
1868 startDeployment: (id: string) =>
1869 Effect.sync(() => {
1870 calls.push(["startDeployment", id]);
1871 status = "running";
1872 return { previewDomain: "version-1.preview.prisma.build" };
1873 }),
1874 } as unknown as PrismaManagementClient;
1875
1876 return Effect.gen(function* () {
1877 const provider = yield* PrismaDeployment.Provider;
1878 const output = yield* provider.reconcile({
1879 id: "Version",
1880 fqn: "Version",
1881 instanceId: "00000000000000000000000000000000",
1882 news: {
1883 app: "service-1",
1884 skipCodeUpload: true,
1885 start: true,
1886 },
1887 olds: undefined,
1888 output: undefined,
1889 session: undefined as never,
1890 bindings: [],
1891 });
1892
1893 expect(output.status).toBe("running");
1894 expect(calls).toEqual([
1895 [
1896 "createAppDeployment",
1897 {
1898 appId: "service-1",
1899 input: { portMapping: undefined, skipCodeUpload: true },
1900 },
1901 ],
1902 ["getDeployment", "version-1"],
1903 ["startDeployment", "version-1"],
1904 ["getDeployment", "version-1"],
1905 ]);
1906 }).pipe(
1907 Effect.provide(deploymentProviderLive()),
1908 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
1909 Effect.provide(clientBackedApi(client).layer),
1910 Effect.provide(PlatformServices),
1911 );
1912 });
1913
1914 it.effect("tails Deployment logs through the provider", () =>
1915 withWebSocketServer((server) =>
1916 Effect.gen(function* () {
1917 const url = yield* listenUrl(server);
1918 let authorization: string | undefined;
1919 let requestUrl: string | undefined;
1920
1921 server.on("connection", (socket, request) => {
1922 authorization = request.headers.authorization;
1923 requestUrl = request.url;
1924 socket.send(
1925 JSON.stringify({
1926 type: "log",
1927 text: "direct version log",
1928 byteStart: 0,
1929 byteEnd: 18,
1930 }),
1931 );
1932 socket.send(
1933 JSON.stringify({
1934 type: "terminal",
1935 kind: "end",
1936 code: "vm_stopped",
1937 message: "done",
1938 retryable: false,
1939 cursor: null,
1940 }),
1941 );
1942 });
1943
1944 const provider = yield* Provider.findProvider(PrismaDeployment).pipe(
1945 Effect.provide(deploymentProviderLive()),
1946 Effect.provide(makeFakeManagementApi(unhandled).layer),
1947 Effect.provide(PlatformServices),
1948 );
1949 const lines = yield* provider.tail!({
1950 id: "Deployment",
1951 fqn: "Deployment",
1952 instanceId: "00000000000000000000000000000000",
1953 props: {
1954 app: "service-1",
1955 },
1956 output: {
1957 deploymentId: "version-1",
1958 appId: "service-1",
1959 foundryVersionId: "foundry-1",
1960 status: "running",
1961 previewDomain: "version-1.preview.prisma.build",
1962 appEndpointDomain: undefined,
1963 createdAt: "2026-01-01T00:00:00Z",
1964 },
1965 }).pipe(
1966 // The carved-out logs client resolves the distilled Credentials
1967 // service directly: the test WebSocket server is the API base.
1968 Stream.provideService(
1969 Credentials,
1970 Effect.succeed({
1971 apiToken: Redacted.make("version-token"),
1972 apiBaseUrl: url,
1973 }),
1974 ),
1975 Stream.runCollect,
1976 );
1977
1978 expect(lines.map((line) => line.message)).toEqual([
1979 "direct version log",
1980 ]);
1981 expect(authorization).toBe("Bearer version-token");
1982 expect(requestUrl).toBe("/v1/deployments/version-1/logs");
1983 }).pipe(Effect.provide(FetchHttpClient.layer)),
1984 ),
1985 );
1986 it.effect(
1987 "records a redacted triggers fingerprint and replaces when a value changes",
1988 () => {
1989 const secret = "REDEPLOY_SECRET_SENTINEL";
1990 const client = redeployClient();
1991
1992 return Effect.gen(function* () {
1993 const provider = yield* PrismaDeployment.Provider;
1994 const output = yield* provider.reconcile({
1995 id: "Deployment",
1996 fqn: "Deployment",
1997 instanceId: "00000000000000000000000000000000",
1998 news: {
1999 app: "app-1",
2000 skipCodeUpload: true,
2001 triggers: { DATABASE_URL: Redacted.make(secret) },
2002 },
2003 olds: undefined,
2004 output: undefined,
2005 session: undefined as never,
2006 bindings: [],
2007 });
2008
2009 expect(Redacted.isRedacted(output.triggersHash!)).toBe(true);
2010 const fingerprint = Redacted.value(output.triggersHash!);
2011 expect(fingerprint).not.toBe(secret);
2012 expect(fingerprint).not.toContain(secret);
2013 // encodeState unwraps Redacted, so this is what a state store writes.
2014 expect(JSON.stringify(encodeState(output))).not.toContain(secret);
2015
2016 const unchanged = yield* provider.diff!({
2017 ...triggersDiffBase(output.triggersHash),
2018 olds: {
2019 app: "app-1",
2020 skipCodeUpload: true,
2021 triggers: { DATABASE_URL: Redacted.make(secret) },
2022 },
2023 news: {
2024 app: "app-1",
2025 skipCodeUpload: true,
2026 triggers: { DATABASE_URL: Redacted.make(secret) },
2027 },
2028 } as never);
2029 const changed = yield* provider.diff!({
2030 ...triggersDiffBase(output.triggersHash),
2031 olds: {
2032 app: "app-1",
2033 skipCodeUpload: true,
2034 triggers: { DATABASE_URL: Redacted.make(secret) },
2035 },
2036 news: {
2037 app: "app-1",
2038 skipCodeUpload: true,
2039 triggers: { DATABASE_URL: Redacted.make(`${secret}-rotated`) },
2040 },
2041 } as never);
2042
2043 expect(unchanged).toBeUndefined();
2044 expect(changed).toEqual({ action: "replace" });
2045 }).pipe(
2046 Effect.provide(deploymentProviderLive()),
2047 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
2048 Effect.provide(clientBackedApi(client).layer),
2049 Effect.provide(PlatformServices),
2050 );
2051 },
2052 );
2053
2054 it.effect("replaces when a plain triggers value changes", () => {
2055 const client = {} as PrismaManagementClient;
2056
2057 return Effect.gen(function* () {
2058 const provider = yield* PrismaDeployment.Provider;
2059 const diff = yield* provider.diff!({
2060 ...triggersDiffBase(Redacted.make("recorded-fingerprint")),
2061 olds: {
2062 app: "app-1",
2063 skipCodeUpload: true,
2064 triggers: { FEATURE_FLAG: "off" },
2065 },
2066 news: {
2067 app: "app-1",
2068 skipCodeUpload: true,
2069 triggers: { FEATURE_FLAG: "on" },
2070 },
2071 } as never);
2072
2073 expect(diff).toEqual({ action: "replace" });
2074 }).pipe(
2075 Effect.provide(deploymentProviderLive()),
2076 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
2077 Effect.provide(clientBackedApi(client).layer),
2078 Effect.provide(PlatformServices),
2079 );
2080 });
2081
2082 it.effect(
2083 "keeps a deployment recorded before triggers existed when nothing changed",
2084 () => {
2085 const client = {} as PrismaManagementClient;
2086
2087 return Effect.gen(function* () {
2088 const provider = yield* PrismaDeployment.Provider;
2089 const diff = yield* provider.diff!({
2090 ...triggersDiffBase(undefined),
2091 olds: {
2092 app: "app-1",
2093 skipCodeUpload: true,
2094 triggers: { FEATURE_FLAG: "on" },
2095 },
2096 news: {
2097 app: "app-1",
2098 skipCodeUpload: true,
2099 triggers: { FEATURE_FLAG: "on" },
2100 },
2101 } as never);
2102
2103 expect(diff).toBeUndefined();
2104 }).pipe(
2105 Effect.provide(deploymentProviderLive()),
2106 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
2107 Effect.provide(clientBackedApi(client).layer),
2108 Effect.provide(PlatformServices),
2109 );
2110 },
2111 );
2112
2113 it.effect(
2114 "replaces when triggers cannot be resolved while planning",
2115 () => {
2116 const client = {} as PrismaManagementClient;
2117
2118 return Effect.gen(function* () {
2119 const provider = yield* PrismaDeployment.Provider;
2120 const recorded = yield* provider.diff!({
2121 ...triggersDiffBase(Redacted.make("recorded-fingerprint")),
2122 olds: {
2123 app: "app-1",
2124 skipCodeUpload: true,
2125 triggers: { DATABASE_URL: "postgres://old" },
2126 },
2127 news: {
2128 app: "app-1",
2129 skipCodeUpload: true,
2130 triggers: { DATABASE_URL: Output.asOutput("postgres://new") },
2131 },
2132 } as never);
2133 const neverRecorded = yield* provider.diff!({
2134 ...triggersDiffBase(undefined),
2135 olds: {
2136 app: "app-1",
2137 skipCodeUpload: true,
2138 triggers: { DATABASE_URL: "postgres://old" },
2139 },
2140 news: {
2141 app: "app-1",
2142 skipCodeUpload: true,
2143 triggers: { DATABASE_URL: Output.asOutput("postgres://new") },
2144 },
2145 } as never);
2146
2147 expect(recorded).toEqual({ action: "replace" });
2148 expect(neverRecorded).toBeUndefined();
2149 }).pipe(
2150 Effect.provide(deploymentProviderLive()),
2151 Effect.provide(Layer.succeed(PrismaClient, currentClient(client))),
2152 Effect.provide(clientBackedApi(client).layer),
2153 Effect.provide(PlatformServices),
2154 );
2155 },
2156 );
2157 },
2158);
2159
2160const redeployClient = () =>
2161 ({
2162 createAppDeployment: () =>
2163 Effect.succeed({
2164 id: "deployment-1",
2165 type: "deployment" as const,
2166 url: "https://api.prisma.test/v1/deployments/deployment-1",
2167 foundryVersionId: "foundry-1",
2168 }),
2169 getDeployment: (id: string) =>
2170 Effect.succeed({
2171 id,
2172 type: "deployment" as const,
2173 serviceId: "app-1",
2174 url: "https://api.prisma.test/v1/deployments/deployment-1",
2175 foundryVersionId: "foundry-1",
2176 status: "new",
2177 previewDomain: null,
2178 createdAt: "2026-01-01T00:00:00Z",
2179 }),
2180 }) as unknown as PrismaManagementClient;
2181
2182const triggersDiffBase = (
2183 triggersHash: Redacted.Redacted<string> | undefined,
2184) => ({
2185 id: "Deployment",
2186 fqn: "Deployment",
2187 instanceId: "00000000000000000000000000000000",
2188 oldBindings: [],
2189 newBindings: [],
2190 output: {
2191 deploymentId: "deployment-1",
2192 appId: "app-1",
2193 foundryVersionId: "foundry-1",
2194 status: "new",
2195 previewDomain: null,
2196 artifactHash: undefined,
2197 triggersHash,
2198 appEndpointDomain: undefined,
2199 createdAt: "2026-01-01T00:00:00Z",
2200 },
2201});
2202
2203const withWebSocketServer = <A, E, R>(
2204 f: (server: WebSocketServer) => Effect.Effect<A, E, R>,
2205) =>
2206 Effect.acquireUseRelease(
2207 Effect.sync(() => new WebSocketServer({ host: "127.0.0.1", port: 0 })),
2208 f,
2209 (server) =>
2210 Effect.callback<void>((resume) => {
2211 server.close(() => resume(Effect.void));
2212 }).pipe(Effect.ignore),
2213 );
2214
2215const listenUrl = (server: WebSocketServer) =>
2216 Effect.callback<string, Error>((resume) => {
2217 const complete = () => {
2218 cleanup();
2219 const address = server.address();
2220 if (address && typeof address === "object") {
2221 resume(Effect.succeed(`http://127.0.0.1:${address.port}`));
2222 } else {
2223 resume(Effect.fail(new Error("WebSocket server has no TCP address")));
2224 }
2225 };
2226 const fail = (cause: unknown) => {
2227 cleanup();
2228 resume(
2229 Effect.fail(cause instanceof Error ? cause : new Error(String(cause))),
2230 );
2231 };
2232 const cleanup = () => {
2233 server.off("listening", complete);
2234 server.off("error", fail);
2235 };
2236
2237 if (server.address()) {
2238 complete();
2239 return;
2240 }
2241
2242 server.once("listening", complete);
2243 server.once("error", fail);
2244 return Effect.sync(cleanup);
2245 });
2246
2247const withHttpServer = <A, E, R>(
2248 handler: RequestListener,
2249 f: (url: string) => Effect.Effect<A, E, R>,
2250) =>
2251 Effect.acquireUseRelease(
2252 Effect.sync(() => createHttpServer(handler)),
2253 (server) => Effect.flatMap(listenHttpServerUrl(server), f),
2254 (server) =>
2255 Effect.callback<void>((resume) => {
2256 server.close(() => resume(Effect.void));
2257 }).pipe(Effect.ignore),
2258 );
2259
2260const listenHttpServerUrl = (server: NodeHttpServer) =>
2261 Effect.callback<string, Error>((resume) => {
2262 const complete = () => {
2263 cleanup();
2264 const address = server.address();
2265 if (address && typeof address === "object") {
2266 resume(Effect.succeed(`http://127.0.0.1:${address.port}`));
2267 } else {
2268 resume(Effect.fail(new Error("HTTP server has no TCP address")));
2269 }
2270 };
2271 const fail = (cause: unknown) => {
2272 cleanup();
2273 resume(
2274 Effect.fail(cause instanceof Error ? cause : new Error(String(cause))),
2275 );
2276 };
2277 const cleanup = () => {
2278 server.off("listening", complete);
2279 server.off("error", fail);
2280 };
2281
2282 server.once("listening", complete);
2283 server.once("error", fail);
2284 server.listen(0, "127.0.0.1");
2285 return Effect.sync(cleanup);
2286 });