1import { providers } from "@/Neon/Providers.ts";
2import * as Test from "@/Test/Alchemy.ts";
3import { getProject, getProjectBranchFunction } from "@distilled.cloud/neon";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Path from "effect/Path";
8import * as Semaphore from "effect/Semaphore";
9import * as HttpClient from "effect/http/HttpClient";
10import {
11 bodyContaining,
12 exampleRoot,
13 updatedBodyContaining,
14} from "./Fixture.ts";
15import { functionRolloutTimeout } from "../FunctionRollout.ts";
16import { frameworks } from "./Frameworks.ts";
17import { browserRoundtrip } from "./Browser.ts";
18
19const { test } = Test.make({ providers: providers() });
20const deployments = Semaphore.makeUnsafe(1);
21
22describe.concurrent(
23 "Neon Website complete lifecycle",
24 { tags: ["provider:neon", "provider:neon:website", "live"] },
25 () => {
26 for (const { slug, name, website } of frameworks) {
27 // Each live lifecycle deploys, polls rollout, and browser-verifies
28 // (~3 minutes); skip the whole matrix under --fast.
29 test.provider.skipIf(!!process.env.FAST)(
30 slug,
31 (stack) =>
32 Effect.gen(function* () {
33 yield* stack.destroy();
34 const fs = yield* FileSystem.FileSystem;
35 const path = yield* Path.Path;
36 const rootDir = yield* exampleRoot(slug);
37 const asset = path.join(
38 rootDir,
39 slug === "sveltekit" ? "static" : "public",
40 "example.json",
41 );
42 const original = yield* fs.readFileString(asset);
43 yield* Effect.addFinalizer(() =>
44 fs.writeFileString(asset, original).pipe(Effect.orDie),
45 );
46 const deploy = stack
47 .deploy(
48 Effect.gen(function* () {
49 return yield* website("Web", {
50 rootDir,
51 env: { GREETING: `Hello from ${name} on Neon!` },
52 });
53 }),
54 )
55 .pipe(deployments.withPermit);
56 const site = yield* deploy;
57 expect(site.url).toMatch(/^https:\/\//);
58 const url = String(site.url).replace(/\/+$/, "");
59 const fn = site.function!;
60 yield* Effect.logInfo(
61 `Website ${slug}: deployed project=${fn.projectId} function=${fn.functionId} deployment=${fn.activeDeploymentId} url=${site.url}`,
62 );
63 yield* Effect.addFinalizer(() =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66 expect(
67 yield* getProject({ project_id: fn.projectId }).pipe(
68 Effect.as(false),
69 Effect.catchTag("NotFound", () => Effect.succeed(true)),
70 ),
71 ).toBe(true);
72 yield* Effect.logInfo(`Website ${slug}: cleanup verified`);
73 }).pipe(Effect.orDie),
74 );
75 yield* bodyContaining(`${url}/`, name);
76 yield* browserRoundtrip(url, slug).pipe(Effect.scoped);
77 const head = yield* HttpClient.head(`${url}/example.json`);
78 expect(head.status).toBe(200);
79 expect(yield* head.text).toBe("");
80 const found = yield* getProjectBranchFunction({
81 project_id: fn.projectId,
82 branch_id: fn.branchId,
83 slug: fn.slug,
84 });
85 expect(found.function.active_deployment?.id).toBe(
86 fn.activeDeploymentId,
87 );
88 const unchanged = yield* deploy;
89 expect(unchanged.function!.activeDeploymentId).toBe(
90 fn.activeDeploymentId,
91 );
92 yield* Effect.logInfo(`Website ${slug}: initial no-op verified`);
93 yield* fs.writeFileString(
94 asset,
95 '{"framework":"Neon Website lifecycle updated"}',
96 );
97 const updated = yield* deploy;
98 expect(updated.function!.functionId).toBe(fn.functionId);
99 expect(updated.url).toBe(site.url);
100 expect(updated.function!.activeDeploymentId).not.toBe(
101 fn.activeDeploymentId,
102 );
103 yield* Effect.logInfo(
104 `Website ${slug}: update accepted deployment=${updated.function!.activeDeploymentId}`,
105 );
106 yield* updatedBodyContaining(
107 `${String(updated.url).replace(/\/+$/, "")}/example.json`,
108 "Neon Website lifecycle updated",
109 );
110 yield* Effect.logInfo(`Website ${slug}: updated content verified`);
111 const settled = yield* deploy;
112 expect(settled.function!.activeDeploymentId).toBe(
113 updated.function!.activeDeploymentId,
114 );
115 yield* stack.destroy();
116 expect(
117 yield* getProject({ project_id: fn.projectId }).pipe(
118 Effect.as(false),
119 Effect.catchTag("NotFound", () => Effect.succeed(true)),
120 ),
121 ).toBe(true);
122 }).pipe(Effect.scoped),
123 { timeout: functionRolloutTimeout },
124 );
125 }
126 },
127);