1import type { AIGateway } from "@/Neon/AIGateway.ts";
2import { backendEnvKey } from "@/Neon/BackendConnection.ts";
3import { QueryAIGateway, QueryAIGatewayHttp } from "@/Neon/QueryAIGateway.ts";
4import { FunctionEnvironment } from "@/Neon/FunctionEnvironment.ts";
5import * as Output from "@/Output.ts";
6import { RuntimeContext } from "@/RuntimeContext.ts";
7import { expect, test } from "alchemy-test";
8import * as ConfigProvider from "effect/ConfigProvider";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Redacted from "effect/Redacted";
12import * as Result from "effect/Result";
13
14const gateway: AIGateway = {
15 FQN: "Gateway",
16 LogicalId: "Gateway",
17 Props: { branch: { projectId: "project", branchId: "branch" } },
18 projectId: Output.literal("project"),
19 branchId: Output.literal("branch"),
20 baseUrl: Output.literal("https://branch.invalid"),
21 credential: undefined,
22};
23
24const runtimeMode = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
25 Effect.gen(function* () {
26 const previous = yield* Effect.sync(() => {
27 const previous = globalThis.__ALCHEMY_RUNTIME__;
28 globalThis.__ALCHEMY_RUNTIME__ = true;
29 return previous;
30 });
31 return yield* effect.pipe(
32 Effect.ensuring(
33 Effect.sync(() => {
34 globalThis.__ALCHEMY_RUNTIME__ = previous;
35 }),
36 ),
37 );
38 });
39
40const services = (injected: boolean, environment?: Record<string, string>) =>
41 QueryAIGatewayHttp.pipe(
42 Layer.provideMerge(
43 Layer.mergeAll(
44 RuntimeContext.phantom,
45 Layer.succeed(
46 ConfigProvider.ConfigProvider,
47 ConfigProvider.fromUnknown({
48 [backendEnvKey(gateway.FQN, "AI_GATEWAY_URL")]:
49 "https://branch.invalid",
50 [backendEnvKey(gateway.FQN, "AI_GATEWAY_TOKEN")]:
51 "managed-scoped-token",
52 [backendEnvKey(gateway.FQN, "AI_GATEWAY_INJECTED")]: injected
53 ? "yes"
54 : "no",
55 }),
56 ),
57 environment
58 ? Layer.succeed(FunctionEnvironment, environment)
59 : Layer.empty,
60 ),
61 ),
62 );
63
64for (const mode of ["injected", "managed", "managed-with-injection"] as const) {
65 test.effect(
66 `HTTP AI client uses ${mode} credentials without account keys`,
67 () =>
68 runtimeMode(
69 Effect.gen(function* () {
70 const client = yield* QueryAIGateway(gateway);
71 expect(Layer.isLayer(client.model({ model: "gpt-5-mini" }))).toBe(
72 true,
73 );
74 expect(yield* client.chatBaseUrl).toBe("https://branch.invalid/v1");
75 expect(yield* client.responsesBaseUrl).toBe(
76 "https://branch.invalid/openai/v1",
77 );
78 expect(yield* client.anthropicBaseUrl).toBe(
79 "https://branch.invalid/anthropic",
80 );
81 const token = yield* client.token;
82 expect(Redacted.isRedacted(token)).toBe(true);
83 expect(Redacted.value(token)).toBe(
84 mode === "injected"
85 ? "injected-scoped-token"
86 : "managed-scoped-token",
87 );
88 expect(JSON.stringify(token)).not.toContain("scoped-token");
89 }).pipe(
90 Effect.provide(
91 services(
92 mode === "injected",
93 mode === "managed"
94 ? undefined
95 : {
96 NEON_AI_GATEWAY_BASE_URL: "https://branch.invalid/",
97 NEON_AI_GATEWAY_TOKEN: "injected-scoped-token",
98 NEON_API_KEY: "DO_NOT_BIND_ACCOUNT_KEY",
99 },
100 ),
101 ),
102 ),
103 ),
104 {
105 tags: [
106 "unit",
107 "provider:neon",
108 "provider:neon:aigateway",
109 "provider:neon:backendconnection",
110 "provider:neon:function",
111 "provider:neon:languagemodel",
112 "local",
113 ],
114 exclusive: true,
115 },
116 );
117}
118
119for (const [name, environment, message] of [
120 [
121 "wrong branch",
122 {
123 NEON_AI_GATEWAY_BASE_URL: "https://other.invalid",
124 NEON_AI_GATEWAY_TOKEN: "other-token",
125 },
126 "does not match",
127 ],
128 [
129 "missing grant",
130 {
131 NEON_AI_GATEWAY_BASE_URL: "https://branch.invalid",
132 NEON_API_KEY: "DO_NOT_BIND_ACCOUNT_KEY",
133 },
134 "did not inject",
135 ],
136 ["missing environment", undefined, "does not match"],
137] as const) {
138 test.effect(
139 `HTTP AI client rejects ${name} instead of falling back to another token`,
140 () =>
141 runtimeMode(
142 Effect.gen(function* () {
143 const client = yield* QueryAIGateway(gateway);
144 const result = yield* Effect.result(Effect.sandbox(client.token));
145 expect(Result.isFailure(result)).toBe(true);
146 if (Result.isFailure(result)) {
147 expect(String(result.failure)).toContain(message);
148 expect(String(result.failure)).not.toContain(
149 "DO_NOT_BIND_ACCOUNT_KEY",
150 );
151 }
152 }).pipe(Effect.provide(services(true, environment))),
153 ),
154 {
155 tags: [
156 "unit",
157 "provider:neon",
158 "provider:neon:aigateway",
159 "provider:neon:backendconnection",
160 "provider:neon:function",
161 "provider:neon:languagemodel",
162 "local",
163 ],
164 exclusive: true,
165 },
166 );
167}