1import * as CloudflareClient from "@effect/ai-cloudflare/CloudflareClient"
2import { assert, describe, it } from "@effect/vitest"
3import { Config, ConfigProvider, Duration, Effect, Layer, Redacted } from "effect"
4import * as HttpClient from "effect/http/HttpClient"
5import * as HttpClientError from "effect/http/HttpClientError"
6import * as HttpClientRequest from "effect/http/HttpClientRequest"
7import * as HttpClientResponse from "effect/http/HttpClientResponse"
8
9const payload = {
10 model: "clef-flash",
11 state: "Checkout is down",
12 questions: { urgent: { type: "noul", instructions: "Does this need immediate attention?" } }
13} as const
14
15const result = {
16 model: "clef-flash",
17 answers: { urgent: { type: "noul", noul: 0.99 } },
18 usage: { input_tokens: 120, output_tokens: 0 }
19} as const
20
21const httpLayer = (
22 handler: (
23 request: HttpClientRequest.HttpClientRequest
24 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>
25) =>
26 Layer.succeed(
27 HttpClient.HttpClient,
28 HttpClient.makeWith(
29 Effect.flatMap(handler),
30 Effect.succeed as HttpClient.HttpClient.Preprocess<HttpClientError.HttpClientError, never>
31 )
32 )
33
34const clientLayer = (response: (request: HttpClientRequest.HttpClientRequest) => Response) =>
35 CloudflareClient.layer({ accountId: "test-account", apiKey: Redacted.make("test-token") }).pipe(
36 Layer.provide(httpLayer((request) => Effect.succeed(HttpClientResponse.fromWeb(request, response(request)))))
37 )
38
39const createDecisions = Effect.gen(function*() {
40 const client = yield* CloudflareClient.CloudflareClient
41 return yield* client.createDecisions(payload)
42})
43
44describe("CloudflareClient", () => {
45 it.effect("posts decisions to the account model endpoint and unwraps the result envelope", () =>
46 Effect.gen(function*() {
47 const response = yield* createDecisions
48 assert.deepStrictEqual(response, result)
49 }).pipe(Effect.provide(clientLayer((request) => {
50 assert.strictEqual(request.method, "POST")
51 assert.strictEqual(
52 request.url,
53 "https://api.cloudflare.com/client/v4/accounts/test-account/ai/run/@cf/cloudflare/clef-flash"
54 )
55 assert.strictEqual(request.headers.authorization, "Bearer test-token")
56 assert.strictEqual(request.body._tag, "Uint8Array")
57 if (request.body._tag === "Uint8Array") {
58 assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(request.body.body)), payload)
59 }
60 return Response.json({ success: true, result, errors: [], messages: [] })
61 }))))
62
63 it.effect("layerConfig reads Cloudflare credentials from the environment", () =>
64 createDecisions.pipe(
65 Effect.provide(CloudflareClient.layerConfig({
66 apiUrl: Config.String("TEST_API_URL"),
67 transformClient: HttpClient.mapRequest(HttpClientRequest.setHeader("x-test", "configured"))
68 })),
69 Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({
70 env: {
71 CLOUDFLARE_ACCOUNT_ID: "configured-account",
72 CLOUDFLARE_API_TOKEN: "configured-token",
73 TEST_API_URL: "https://example.com/api"
74 }
75 }))),
76 Effect.provide(httpLayer((request) => {
77 assert.strictEqual(
78 request.url,
79 "https://example.com/api/accounts/configured-account/ai/run/@cf/cloudflare/clef-flash"
80 )
81 assert.strictEqual(request.headers.authorization, "Bearer configured-token")
82 assert.strictEqual(request.headers["x-test"], "configured")
83 return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ success: true, result })))
84 }))
85 ))
86
87 describe("error mapping", () => {
88 for (
89 const [status, tag] of [
90 [401, "AuthenticationError"],
91 [404, "InvalidRequestError"],
92 [422, "InvalidRequestError"],
93 [200, "InternalProviderError"]
94 ] as const
95 ) {
96 it.effect(`maps a failure envelope with HTTP ${status} to ${tag}`, () =>
97 Effect.gen(function*() {
98 const error = yield* Effect.flip(createDecisions)
99 assert.strictEqual(error.module, "CloudflareClient")
100 assert.strictEqual(error.method, "createDecisions")
101 assert.strictEqual(error.reason._tag, tag)
102 if ("http" in error.reason) {
103 assert.strictEqual(error.reason.http?.response?.status, status)
104 assert.notInclude(JSON.stringify(error.reason.http?.request), "test-token")
105 }
106 if ("description" in error.reason) {
107 assert.include(error.reason.description, "Request failed")
108 assert.include(error.reason.description, "3040")
109 assert.include(error.reason.description, "test-ray")
110 }
111 }).pipe(Effect.provide(clientLayer(() =>
112 Response.json(
113 { success: false, result: null, errors: [{ code: 3040, message: "Request failed" }] },
114 { status, headers: { "cf-ray": "test-ray" } }
115 )
116 ))))
117 }
118
119 it.effect("maps a failure envelope without errors with HTTP 200 to InternalProviderError", () =>
120 Effect.gen(function*() {
121 const error = yield* Effect.flip(createDecisions)
122 assert.strictEqual(error.reason._tag, "InternalProviderError")
123 }).pipe(Effect.provide(clientLayer(() => Response.json({ success: false, result: null })))))
124
125 it.effect("maps 429 to RateLimitError with Retry-After and Cloudflare metadata", () =>
126 Effect.gen(function*() {
127 const error = yield* Effect.flip(createDecisions)
128 assert.strictEqual(error.reason._tag, "RateLimitError")
129 if (error.reason._tag === "RateLimitError") {
130 assert.strictEqual(Duration.toMillis(error.reason.retryAfter!), 2000)
131 assert.deepStrictEqual(error.reason.metadata, {
132 cloudflare: { rayId: "test-ray", errors: [{ code: 3040, message: "Request failed" }] }
133 })
134 }
135 }).pipe(Effect.provide(clientLayer(() =>
136 Response.json(
137 { success: false, result: null, errors: [{ code: 3040, message: "Request failed" }] },
138 { status: 429, headers: { "cf-ray": "test-ray", "retry-after": "2" } }
139 )
140 ))))
141
142 it.effect("maps a non-JSON failure body by HTTP status", () =>
143 Effect.gen(function*() {
144 const error = yield* Effect.flip(createDecisions)
145 assert.strictEqual(error.reason._tag, "InternalProviderError")
146 if (error.reason._tag === "InternalProviderError") {
147 assert.include(error.reason.description, "Bad gateway")
148 }
149 }).pipe(Effect.provide(clientLayer(() => new Response("Bad gateway", { status: 502 })))))
150
151 it.effect("maps a success body without a result envelope to InvalidOutputError", () =>
152 Effect.gen(function*() {
153 const error = yield* Effect.flip(createDecisions)
154 assert.strictEqual(error.reason._tag, "InvalidOutputError")
155 }).pipe(Effect.provide(clientLayer(() => Response.json(result)))))
156
157 it.effect("maps TransportError to NetworkError", () =>
158 Effect.gen(function*() {
159 const error = yield* Effect.flip(createDecisions)
160 assert.strictEqual(error.reason._tag, "NetworkError")
161 }).pipe(
162 Effect.provide(CloudflareClient.layer({ accountId: "test-account", apiKey: Redacted.make("test-token") })),
163 Effect.provide(httpLayer((request) =>
164 Effect.fail(
165 new HttpClientError.HttpClientError({
166 reason: new HttpClientError.TransportError({ request, cause: new Error("Connection refused") })
167 })
168 )
169 ))
170 ))
171 })
172})