1import { AIGateway } from "@/Neon/AIGateway.ts";
2import { Branch } from "@/Neon/Branch.ts";
3import { Credential } from "@/Neon/Credential.ts";
4import { Project } from "@/Neon/Project.ts";
5import { providers } from "@/Neon/Providers.ts";
6import * as Test from "@/Test/Alchemy";
7import * as SDK from "@distilled.cloud/neon";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as Redacted from "effect/Redacted";
11import * as Schema from "effect/Schema";
12import * as HttpClient from "effect/http/HttpClient";
13import * as HttpClientRequest from "effect/http/HttpClientRequest";
14
15const { test } = Test.make({ providers: providers() });
16
17test.provider(
18 "gateway discovery performs no fake CRUD and scoped credentials are revoked",
19 (stack) =>
20 Effect.gen(function* () {
21 yield* stack.destroy();
22 const deployed = yield* stack.deploy(
23 Effect.gen(function* () {
24 const project = yield* Project("GatewayProject", {
25 region: "aws-us-east-2",
26 });
27 const branch = yield* Branch("GatewayBranch", { project });
28 const credential = yield* Credential("GatewayCredential", {
29 branch,
30 scopes: ["ai_gateway:invoke"],
31 });
32 const gateway = yield* AIGateway("Gateway", { branch, credential });
33 return { branch, credential, baseUrl: gateway.baseUrl };
34 }),
35 );
36 const request = {
37 project_id: deployed.branch.projectId,
38 branch_id: deployed.branch.branchId,
39 };
40 expect(
41 (yield* SDK.getProjectBranchAiGateway(request)).base_url.replace(
42 /\/$/,
43 "",
44 ),
45 ).toBe(deployed.baseUrl);
46 const http = yield* HttpClient.HttpClient;
47 const models = yield* http.execute(
48 HttpClientRequest.get(`${deployed.baseUrl}/v1/models`).pipe(
49 HttpClientRequest.bearerToken(deployed.credential.apiToken),
50 ),
51 );
52 expect(models.status).toBe(200);
53 const catalog = yield* models.json.pipe(
54 Effect.flatMap(
55 Schema.decodeUnknownEffect(
56 Schema.Struct({
57 data: Schema.Array(
58 Schema.Struct({ id: Schema.String, enabled: Schema.Boolean }),
59 ),
60 }),
61 ),
62 ),
63 );
64 expect(catalog.data.length).toBeGreaterThan(0);
65 yield* Effect.log("Enabled Neon AI Gateway models", {
66 models: catalog.data
67 .filter((model) => model.enabled)
68 .map((model) => model.id),
69 });
70 const list = yield* SDK.listCredentials(request);
71 expect(
72 list.credentials.find(
73 (entry) => entry.token_id === deployed.credential.tokenId,
74 )?.scopes,
75 ).toContain("ai_gateway:invoke");
76 yield* stack.destroy();
77 expect(
78 yield* SDK.getProjectBranch(request).pipe(
79 Effect.as(false),
80 Effect.catchTag("NotFound", () => Effect.succeed(true)),
81 ),
82 ).toBe(true);
83 yield* stack.destroy();
84 }),
85 {
86 tags: [
87 "provider:neon",
88 "provider:neon:aigateway",
89 "provider:neon:branch",
90 "provider:neon:credential",
91 "provider:neon:project",
92 "live",
93 ],
94 timeout: 120_000,
95 },
96);
97
98test.provider.skipIf(
99 process.env.NEON_TEST_AI_PAID !== "1" || !process.env.NEON_TEST_AI_MODEL,
100)(
101 "paid model invocation is explicitly gated and never purchases credits",
102 (stack) =>
103 Effect.gen(function* () {
104 yield* stack.destroy();
105 const deployed = yield* stack.deploy(
106 Effect.gen(function* () {
107 const project = yield* Project("PaidGatewayProject", {
108 region: "aws-us-east-2",
109 });
110 const credential = yield* Credential("PaidGatewayCredential", {
111 project,
112 scopes: ["ai_gateway:invoke"],
113 });
114 const gateway = yield* AIGateway("PaidGateway", {
115 project,
116 credential,
117 });
118 return { credential, baseUrl: gateway.baseUrl };
119 }),
120 );
121 const http = yield* HttpClient.HttpClient;
122 const response = yield* http.execute(
123 HttpClientRequest.post(`${deployed.baseUrl}/v1/chat/completions`).pipe(
124 HttpClientRequest.bearerToken(deployed.credential.apiToken),
125 HttpClientRequest.bodyJsonUnsafe({
126 model: process.env.NEON_TEST_AI_MODEL,
127 messages: [{ role: "user", content: "Reply with hello." }],
128 max_tokens: 8,
129 }),
130 ),
131 );
132 if (response.status !== 200) {
133 const body = yield* response.json;
134 const error =
135 typeof body === "object" && body !== null && "error" in body
136 ? body.error
137 : body;
138 const message =
139 typeof error === "object" && error !== null && "message" in error
140 ? error.message
141 : undefined;
142 yield* Effect.log("Neon AI Gateway inference rejected", {
143 status: response.status,
144 message:
145 typeof message === "string"
146 ? message
147 .replaceAll(
148 Redacted.value(deployed.credential.apiToken),
149 "[REDACTED]",
150 )
151 .slice(0, 1000)
152 : "No error message",
153 });
154 }
155 expect(response.status).toBe(200);
156 yield* stack.destroy();
157 }).pipe(Effect.ensuring(stack.destroy().pipe(Effect.orDie))),
158 {
159 tags: [
160 "provider:neon",
161 "provider:neon:aigateway",
162 "provider:neon:credential",
163 "provider:neon:project",
164 "live",
165 ],
166 timeout: 90_000,
167 },
168);