Skip to content

Commit 665ebd8

Browse files
add vitest fixture that documents AI/Vectorize bindings with vitest (#8624)
* add ai fixture * Update vitest.config.ts * Update fixtures/vitest-pool-workers-examples/ai-vectorize/global-setup.ts Co-authored-by: Carmen Popoviciu <[email protected]> * add config warning * rename worker * lint * changeset * Update config.ts Co-authored-by: Carmen Popoviciu <[email protected]> * Update packages/vitest-pool-workers/src/pool/config.ts * fix --------- Co-authored-by: Carmen Popoviciu <[email protected]>
1 parent 3f41730 commit 665ebd8

File tree

13 files changed

+205
-0
lines changed

13 files changed

+205
-0
lines changed

.changeset/warm-poets-feel.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cloudflare/vitest-pool-workers": patch
3+
---
4+
5+
fix: Add usage charge warning when Vectorize and AI bindings are used in Vitest
6+
7+
Vectorize and AI bindings can now be used with Vitest. However, because they have no local simulators, they will access your account and incur usage charges, even in testing. Therefore we recommend mocking any usage of these bindings when testing.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# 🤖 AI and Vectorize
2+
3+
This Worker uses the AI and Vectorize bindings. @cloudflare/vitest-pool-workers@^0.8.1 is required to use AI and Vectorize bindings in the Vitest integration.
4+
5+
[!WARNING]
6+
7+
Because Workers AI and Vectorize bindings do not have a local simulator, usage of these bindings will always access your Cloudflare account, and so will incur usage charges even in local development and testing. We recommend mocking any usage of these bindings in your tests as demonstrated in this fixture.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import childProcess from "node:child_process";
2+
3+
// Global setup runs inside Node.js, not `workerd`
4+
export default function () {
5+
const label = "Built ai-vectorize Worker";
6+
console.time(label);
7+
childProcess.execSync("wrangler build", { cwd: __dirname });
8+
console.timeEnd(label);
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Generated by Wrangler by running `wrangler types` (hash: 266f7400cc06616d9ccb541889857ea7)
2+
declare namespace Cloudflare {
3+
interface Env {
4+
VECTORIZE: VectorizeIndex;
5+
AI: Ai;
6+
}
7+
}
8+
interface Env extends Cloudflare.Env {}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
interface EmbeddingResponse {
2+
shape: number[];
3+
data: number[][];
4+
}
5+
6+
export default {
7+
async fetch(request, env, ctx): Promise<Response> {
8+
const path = new URL(request.url).pathname;
9+
10+
if (path === "/ai") {
11+
const stories = [
12+
"This is a story about an orange cloud",
13+
"This is a story about a llama",
14+
];
15+
const modelResp: EmbeddingResponse = await env.AI.run(
16+
"@cf/baai/bge-base-en-v1.5",
17+
{
18+
text: stories,
19+
}
20+
);
21+
return Response.json(modelResp);
22+
}
23+
24+
if (path === "/vectorize") {
25+
const vectors: VectorizeVector[] = [
26+
{ id: "123", values: [...Array(768).keys()] },
27+
{ id: "456", values: [...Array(768).keys()] },
28+
];
29+
30+
let inserted = await env.VECTORIZE.upsert(vectors);
31+
return Response.json(inserted);
32+
}
33+
34+
return new Response("Hello World!");
35+
},
36+
} satisfies ExportedHandler<Env>;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../tsconfig.workerd.json",
3+
"include": ["./**/*.ts"]
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module "cloudflare:test" {
2+
interface ProvidedEnv extends Env {}
3+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
createExecutionContext,
3+
env,
4+
SELF,
5+
waitOnExecutionContext,
6+
} from "cloudflare:test";
7+
import { describe, expect, it, vi } from "vitest";
8+
import worker from "../src/index";
9+
10+
// For now, you'll need to do something like this to get a correctly-typed
11+
// `Request` to pass to `worker.fetch()`.
12+
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
13+
14+
describe("Tests that do hit the AI binding", () => {
15+
describe("unit style", () => {
16+
it("lets you mock the ai binding", async () => {
17+
const request = new IncomingRequest("http://example.com/ai");
18+
19+
const ctx = createExecutionContext();
20+
21+
// mock the AI run function by directly modifying `env`
22+
env.AI.run = vi.fn().mockResolvedValue({
23+
shape: [1, 2],
24+
data: [[0, 0]],
25+
});
26+
const response = await worker.fetch(request, env, ctx);
27+
28+
await waitOnExecutionContext(ctx);
29+
expect(await response.text()).toMatchInlineSnapshot(
30+
`"{"shape":[1,2],"data":[[0,0]]}"`
31+
);
32+
});
33+
34+
it("lets you mock the vectorize binding", async () => {
35+
const request = new IncomingRequest("http://example.com/vectorize");
36+
const ctx = createExecutionContext();
37+
38+
// mock the vectorize upsert function by directly modifying `env`
39+
const mockVectorizeStore: VectorizeVector[] = [];
40+
env.VECTORIZE.upsert = vi.fn().mockImplementation(async (vectors) => {
41+
mockVectorizeStore.push(...vectors);
42+
return { mutationId: "123" };
43+
});
44+
45+
const response = await worker.fetch(request, env, ctx);
46+
47+
await waitOnExecutionContext(ctx);
48+
expect(await response.text()).toMatchInlineSnapshot(
49+
`"{"mutationId":"123"}"`
50+
);
51+
expect(mockVectorizeStore.map((v) => v.id)).toMatchInlineSnapshot(`
52+
[
53+
"123",
54+
"456",
55+
]
56+
`);
57+
});
58+
});
59+
});
60+
61+
describe("Tests that do not hit the AI binding", () => {
62+
it("responds with Hello World! (unit style)", async () => {
63+
const request = new IncomingRequest("http://example.com");
64+
// Create an empty context to pass to `worker.fetch()`.
65+
const ctx = createExecutionContext();
66+
const response = await worker.fetch(request, env, ctx);
67+
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
68+
await waitOnExecutionContext(ctx);
69+
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
70+
});
71+
72+
it("responds with Hello World! (integration style)", async () => {
73+
const response = await SELF.fetch("https://example.com");
74+
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
75+
});
76+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../tsconfig.workerd-test.json",
3+
"include": ["./**/*.ts", "../src/env.d.ts"]
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../tsconfig.node.json",
3+
"include": ["./*.ts"]
4+
}

0 commit comments

Comments
 (0)