t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/AI/SearchBindings.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

142 lines
4.9 KiB
TypeScript

import * as Cloudflare from "@/Cloudflare";
import * as Test from "@/Test/Alchemy";
import { expect } from "alchemy-test";
import * as Effect from "effect/Effect";
import { MinimumLogLevel } from "effect/References";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import Stack from "./fixtures/bindings-stack.ts";
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
providers: Cloudflare.providers(),
});
const logLevel = Effect.provideService(
MinimumLogLevel,
process.env.DEBUG ? "Debug" : "Info",
);
const stack = beforeAll(deploy(Stack), { timeout: 120_000 });
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), { timeout: 120_000 });
// Deploying the Worker succeeding at all proves Cloudflare accepted both the
// `ai_search` and `ai_search_namespace` bindings. The `/bindings` route then
// confirms they are injected and shaped correctly at runtime.
test(
"worker deploys with ai_search + ai_search_namespace bindings injected",
Effect.gen(function* () {
const { url } = yield* stack;
expect(url).toBeTypeOf("string");
const client = yield* HttpClient.HttpClient;
const res = yield* client.get(`${url}/bindings`).pipe(
Effect.flatMap((res) =>
res.status === 200
? Effect.succeed(res)
: Effect.fail(new Error(`Worker not ready: ${res.status}`)),
),
// Cap the backoff at 3s so a fresh worker that takes a while to start
// serving 200s keeps getting polled steadily, rather than the
// unbounded exponential delay overshooting the test timeout.
Effect.retry({
schedule: Schedule.min([
Schedule.exponential("500 millis"),
Schedule.spaced("3 seconds"),
]),
times: 40,
}),
);
const body = (yield* res.json) as {
search: string;
searchChatCompletions: string;
searchSearch: string;
ns: string;
nsGet: string;
};
// Single-instance `ai_search` binding resolves to an `AiSearchInstance`
// object exposing `search()` / `chatCompletions()`.
expect(body.search).toBe("object");
expect(body.searchChatCompletions).toBe("function");
expect(body.searchSearch).toBe("function");
// `ai_search_namespace` binding resolves to an `AiSearchNamespace` with
// `.get()`.
expect(body.ns).toBe("object");
expect(body.nsGet).toBe("function");
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:ai",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"live",
],
timeout: 240_000,
},
);
// The Effect worker attaches the same two binding flavors via
// `Cloudflare.AI.Search.Search(...)` / `Cloudflare.AI.SearchNamespace(...)`
// and reads them through the Effect-native client. Resolving each client's
// `raw` runtime handle proves the Effect-first path wires through to the
// live runtime bindings.
test(
"effect worker resolves ai_search + ai_search_namespace via Effect clients",
Effect.gen(function* () {
const { effectUrl } = yield* stack;
expect(effectUrl).toBeTypeOf("string");
const client = yield* HttpClient.HttpClient;
const res = yield* client.get(`${effectUrl}/bindings`).pipe(
Effect.flatMap((res) =>
res.status === 200
? Effect.succeed(res)
: Effect.fail(new Error(`Worker not ready: ${res.status}`)),
),
// Cap the backoff at 3s so a fresh worker that takes a while to start
// serving 200s keeps getting polled steadily, rather than the
// unbounded exponential delay overshooting the test timeout.
Effect.retry({
schedule: Schedule.min([
Schedule.exponential("500 millis"),
Schedule.spaced("3 seconds"),
]),
times: 40,
}),
);
const body = (yield* res.json) as {
mode: string;
searchRaw: string;
searchChatCompletions: string;
searchSearch: string;
nsRaw: string;
nsChatCompletions: string;
};
expect(body.mode).toBe("effect");
// `Cloudflare.AI.Search.Search(...).raw` resolves to the runtime
// `AiSearchInstance` exposing `search()` / `chatCompletions()`.
expect(body.searchRaw).toBe("object");
expect(body.searchChatCompletions).toBe("function");
expect(body.searchSearch).toBe("function");
// `Cloudflare.AI.SearchNamespace(...).raw` resolves to the runtime
// `AiSearchNamespace`; `.get(name)` scopes to an instance exposing
// `chatCompletions()`. The namespace handle may be a callable runtime
// proxy (`typeof` `"function"`) or an object.
expect(["object", "function"]).toContain(body.nsRaw);
expect(body.nsChatCompletions).toBe("function");
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:ai",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"live",
],
timeout: 240_000,
},
);