| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { expect, test } from "bun:test"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Path from "effect/Path"; |
| 6 | import nativeAI from "../src/ai/native.ts"; |
| 7 | import bare from "../src/forms/bare.ts"; |
| 8 | import hono from "../src/forms/hono.ts"; |
| 9 | |
| 10 | const 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 | |
| 19 | test("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 | |
| 37 | const 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 | |
| 50 | test("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 | |
| 85 | test("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 | |
| 104 | const 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 | |
| 152 | for (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 | |
| 168 | for (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 | |
| 191 | for (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 | |
| 209 | test.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 | ); |