1import { PrismaApiError, type PrismaManagementClient } from "@/Prisma/Client";
2import {
3 startDeploymentIdempotent,
4 stopDeploymentIdempotent,
5} from "@/Prisma/Internal/DeploymentActions";
6import { describe, expect, it } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import {
9 type Captured,
10 dispatchTo,
11 makeFakeManagementApi,
12 unhandled,
13} from "./fixtures/FakeManagementApi.ts";
14
15const conflict = (action: "start" | "stop") =>
16 new PrismaApiError({
17 method: "POST",
18 path: `/v1/deployments/deployment-1/${action}`,
19 status: 409,
20 message: "state conflict",
21 });
22
23const version = (status: string) => ({
24 id: "deployment-1",
25 type: "deployment" as const,
26 serviceId: "service-1",
27 url: "https://api.prisma.test/v1/deployments/deployment-1",
28 foundryVersionId: "foundry-1",
29 status,
30 previewDomain: null,
31 createdAt: "2026-01-01T00:00:00Z",
32});
33
34/**
35 * Serve the Management API from the same hermetic client-shaped handlers
36 * these tests declare, for the deployment routes the actions now reach
37 * through distilled operations. An injected `PrismaApiError` becomes its
38 * real status, `undefined` becomes a 404, everything else a `{ data }`
39 * envelope.
40 */
41const clientBackedApi = (client: any) =>
42 makeFakeManagementApi((request: Captured) => {
43 // segments[0] is the "v1" prefix.
44 const [head, id, tail] = request.pathname
45 .split("/")
46 .filter((segment) => segment.length > 0)
47 .slice(1);
48 const { call, callVoid } = dispatchTo(request);
49
50 if (head === "deployments" && id !== undefined) {
51 if (tail === "start") return call(client.startDeployment, [id]);
52 if (tail === "stop") return callVoid(client.stopDeployment, [id]);
53 if (request.method === "GET") return call(client.getDeployment, [id]);
54 }
55 return unhandled(request);
56 });
57
58describe(
59 "Prisma deployment actions",
60 { tags: ["unit", "provider:prisma", "provider:prisma:deployment", "local"] },
61 () => {
62 it.effect(
63 "does not hide a start conflict for an unuploaded version",
64 () => {
65 const error = conflict("start");
66 const client = {
67 startDeployment: () => Effect.fail(error),
68 getDeployment: () => Effect.succeed(version("new")),
69 } as unknown as PrismaManagementClient;
70
71 return Effect.gen(function* () {
72 const observed = yield* startDeploymentIdempotent(
73 "deployment-1",
74 ).pipe(Effect.flip);
75 // Over the wire the injected conflict decodes into the typed error.
76 expect(observed._tag).toBe("Conflict");
77 expect(observed.message).toBe("state conflict");
78 }).pipe(Effect.provide(clientBackedApi(client).layer));
79 },
80 );
81
82 it.effect("accepts a start conflict only after observing progress", () => {
83 const client = {
84 startDeployment: () => Effect.fail(conflict("start")),
85 getDeployment: () => Effect.succeed(version("provisioning")),
86 } as unknown as PrismaManagementClient;
87
88 return startDeploymentIdempotent("deployment-1").pipe(
89 Effect.provide(clientBackedApi(client).layer),
90 );
91 });
92
93 it.effect(
94 "accepts a stop conflict while teardown is already progressing",
95 () => {
96 const client = {
97 stopDeployment: () => Effect.fail(conflict("stop")),
98 getDeployment: () => Effect.succeed(version("stopping")),
99 } as unknown as PrismaManagementClient;
100
101 return stopDeploymentIdempotent("deployment-1").pipe(
102 Effect.provide(clientBackedApi(client).layer),
103 );
104 },
105 );
106 },
107);