1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, test } from "bun:test";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import nativeAI from "../src/ai/native.ts";
7import bare from "../src/forms/bare.ts";
8import hono from "../src/forms/hono.ts";
9
10const source = Effect.fn(function* (file: string) {
11 const fs = yield* FileSystem.FileSystem;
12 const path = yield* Path.Path;
13 const filename = yield* path.fromFileUrl(
14 new URL(`../${file}`, import.meta.url),
15 );
16 return yield* fs.readFileString(filename);
17});
18
19test("all three stack configurations import without running deployment effects", () =>
20 Effect.runPromise(
21 Effect.gen(function* () {
22 const effect = yield* Effect.tryPromise(
23 () => import("../alchemy.run.ts"),
24 );
25 const native = yield* Effect.tryPromise(
26 () => import("../alchemy.native.ts"),
27 );
28 const preview = yield* Effect.tryPromise(
29 () => import("../alchemy.preview.ts"),
30 );
31 expect(effect.default).toBeDefined();
32 expect(native.default).toBeDefined();
33 expect(preview.default).toBeDefined();
34 }),
35 ));
36
37const files = [
38 "src/resources.ts",
39 "src/Api.ts",
40 "src/Events.ts",
41 "src/website.ts",
42 "src/features.ts",
43 "src/ai/resources.ts",
44 "src/ai/index.ts",
45 "src/ai/EffectApi.ts",
46 "src/forms/index.ts",
47 "src/forms/LayerApi.ts",
48];
49
50test("composition declares one Project and one Backend for every feature module", () =>
51 Effect.runPromise(
52 Effect.gen(function* () {
53 const modules = yield* Effect.forEach(files, source);
54 const combined = modules.join("\n");
55 expect(combined.match(/Neon\.Project\(/g)).toHaveLength(1);
56 expect(combined.match(/Neon\.Branch\(/g)).toHaveLength(1);
57 expect(combined).not.toContain("Alchemy.Stack(");
58 expect(modules[0]).toContain('Neon.Project("Project"');
59 expect(modules[0]).toContain('Neon.Branch("Backend"');
60 expect(modules[0]).toContain('access: "private"');
61 expect(modules[0]).toContain('access: "public_read"');
62 expect(modules[0]).toContain('key: "config/settings.json"');
63 expect(modules[0]).toContain("schema: Schema.Struct(");
64 expect(modules[1]).toContain("Neon.ConnectAuth(auth)");
65 expect(modules[1]).toContain("Neon.ReadObject(settings)");
66 for (const file of ["alchemy.run.ts", "alchemy.native.ts"]) {
67 const stack = yield* source(file);
68 expect(stack).toContain("yield* features");
69 expect(stack).not.toContain("Neon.Project(");
70 }
71 const features = yield* source("src/features.ts");
72 expect(features).toContain('enableAI === "true" ? yield* ai : undefined');
73 expect(features).toContain(
74 'enableForms === "true" ? yield* forms : undefined',
75 );
76 expect(features.match(/Config\.withDefault\("false"\)/g)).toHaveLength(2);
77 for (const file of ["src/ai/index.ts", "src/ai/EffectApi.ts"]) {
78 const ai = yield* source(file);
79 expect(ai).toContain('Config.String("NEON_AI_ALLOW_PAID")');
80 expect(ai).toContain('Config.withDefault("false")');
81 }
82 }).pipe(Effect.provide(NodeServices.layer)),
83 ));
84
85test("cron writes are idempotent and the preview only enables its own upload trigger", () =>
86 Effect.runPromise(
87 Effect.gen(function* () {
88 const events = yield* source("src/Events.ts");
89 const migration = yield* source("migrations/0002_scheduled_runs.sql");
90 expect(events).toContain(
91 'Neon.CronEventSource("Nightly", { cron: "0 2 * * *" }',
92 );
93 expect(events).toContain("ON CONFLICT DO NOTHING");
94 expect(migration).toContain("invocation_id text PRIMARY KEY");
95 const preview = yield* source("alchemy.preview.ts");
96 expect(preview).toContain('"NeonUploadPreview"');
97 expect(preview).toContain('name: "PreviewUploads"');
98 expect(preview).not.toContain("yield* features");
99 expect(preview).not.toContain("CronEventSource");
100 expect(preview).not.toContain("Neon.CustomDomain(");
101 }).pipe(Effect.provide(NodeServices.layer)),
102 ));
103
104const invokeNativeAI = Effect.fn(function* (options: {
105 path?: string;
106 method?: string;
107 authorization?: string;
108 body?: string;
109 paid?: string;
110}) {
111 const names = ["EXAMPLE_API_KEY", "AI_MODEL", "AI_ALLOW_PAID"] as const;
112 return yield* Effect.acquireUseRelease(
113 Effect.sync(() => {
114 const previous = names.map((name) => [name, process.env[name]] as const);
115 process.env.EXAMPLE_API_KEY = "offline-example-secret";
116 process.env.AI_MODEL = "never-call-this-model";
117 if (options.paid === undefined) delete process.env.AI_ALLOW_PAID;
118 else process.env.AI_ALLOW_PAID = options.paid;
119 return previous;
120 }),
121 () =>
122 Effect.gen(function* () {
123 const method = options.method ?? "POST";
124 const request = yield* Effect.sync(
125 () =>
126 new Request(`https://example.invalid${options.path ?? "/chat"}`, {
127 method,
128 body:
129 method === "POST"
130 ? (options.body ?? '{"prompt":"Hello"}')
131 : undefined,
132 headers: {
133 "content-type": "application/json",
134 ...(options.authorization === undefined
135 ? {}
136 : { authorization: options.authorization }),
137 },
138 }),
139 );
140 return yield* Effect.tryPromise(() => nativeAI.fetch(request));
141 }),
142 (previous) =>
143 Effect.sync(() => {
144 for (const [name, value] of previous) {
145 if (value === undefined) delete process.env[name];
146 else process.env[name] = value;
147 }
148 }),
149 );
150});
151
152for (const authorization of [undefined, "Bearer wrong"]) {
153 test.serial(
154 `native AI authenticates before parsing: ${authorization ?? "missing"}`,
155 () =>
156 Effect.runPromise(
157 Effect.gen(function* () {
158 const response = yield* invokeNativeAI({
159 authorization,
160 body: "not-json",
161 });
162 expect(response.status).toBe(401);
163 }),
164 ),
165 );
166}
167
168for (const body of [
169 "{",
170 "{}",
171 "null",
172 '{"prompt":4}',
173 '{"prompt":" "}',
174 JSON.stringify({ prompt: "x".repeat(4001) }),
175]) {
176 test.serial(
177 `native AI rejects invalid prompts before the paid gate: ${body.slice(0, 30)}`,
178 () =>
179 Effect.runPromise(
180 Effect.gen(function* () {
181 const response = yield* invokeNativeAI({
182 authorization: "Bearer offline-example-secret",
183 body,
184 });
185 expect(response.status).toBe(400);
186 }),
187 ),
188 );
189}
190
191for (const paid of [undefined, "false", "1"]) {
192 test.serial(
193 `native AI refuses inference without exact opt-in: ${paid ?? "unset"}`,
194 () =>
195 Effect.runPromise(
196 Effect.gen(function* () {
197 const response = yield* invokeNativeAI({
198 authorization: "Bearer offline-example-secret",
199 paid,
200 });
201 expect(response.status).toBe(503);
202 const body = yield* Effect.tryPromise(() => response.text());
203 expect(body).not.toContain("offline-example-secret");
204 }),
205 ),
206 );
207}
208
209test.serial(
210 "native streaming UI and public handler forms remain usable offline",
211 () =>
212 Effect.runPromise(
213 Effect.gen(function* () {
214 const page = yield* invokeNativeAI({ path: "/", method: "GET" });
215 expect(page.status).toBe(200);
216 const html = yield* Effect.tryPromise(() => page.text());
217 expect(html).toContain('id="cancel"');
218 expect(html).toContain("AbortController");
219 expect(html).toContain("text-delta");
220 expect(html).not.toContain("offline-example-secret");
221 expect(
222 (yield* invokeNativeAI({ path: "/missing", method: "GET" })).status,
223 ).toBe(404);
224 const request = yield* Effect.sync(
225 () => new Request("https://example.invalid/health"),
226 );
227 const bareResponse = yield* Effect.sync(() => bare(request));
228 expect(yield* Effect.tryPromise(() => bareResponse.json())).toEqual({
229 path: "/health",
230 });
231 const result = yield* Effect.sync(() => hono.fetch(request));
232 const honoResponse =
233 result instanceof Response
234 ? result
235 : yield* Effect.tryPromise(() => result);
236 expect(yield* Effect.tryPromise(() => honoResponse.json())).toEqual({
237 ok: true,
238 });
239 }),
240 ),
241);