Skip to content

Commit 0ce48c0

Browse files
authored
Cloudflare API mocks (#603)
1 parent 586d291 commit 0ce48c0

File tree

5 files changed

+1124
-2
lines changed

5 files changed

+1124
-2
lines changed

app/utils/cloudflare-ai-transcription.server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export async function transcribeMp3WithWorkersAi({
2525
mp3,
2626
model = getRequiredEnv('CLOUDFLARE_AI_TRANSCRIPTION_MODEL'),
2727
}: {
28+
// Accept Buffers and other Uint8Array views.
2829
mp3: Uint8Array
2930
/**
3031
* Recommended: `@cf/openai/whisper` because it supports raw binary audio via
@@ -49,7 +50,9 @@ export async function transcribeMp3WithWorkersAi({
4950
// Best-effort content-type; CF can infer in many cases, but be explicit.
5051
'Content-Type': 'audio/mpeg',
5152
},
52-
body: mp3,
53+
// Some fetch/undici TS typings are stricter than runtime and require
54+
// `Uint8Array<ArrayBuffer>` rather than `Uint8Array<ArrayBufferLike>`.
55+
body: mp3 as unknown as Uint8Array<ArrayBuffer>,
5356
})
5457

5558
if (!res.ok) {

mocks/__tests__/cloudflare.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { setupServer } from 'msw/node'
2+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest'
3+
import { cloudflareHandlers, resetCloudflareMockState } from '../cloudflare.ts'
4+
5+
const server = setupServer(...cloudflareHandlers)
6+
7+
beforeAll(() => {
8+
server.listen({ onUnhandledRequest: 'error' })
9+
})
10+
11+
beforeEach(() => {
12+
resetCloudflareMockState()
13+
})
14+
15+
afterAll(() => {
16+
server.close()
17+
})
18+
19+
describe('cloudflare MSW mocks', () => {
20+
test('Workers AI embeddings endpoint returns { result: { data } }', async () => {
21+
const res = await fetch(
22+
'https://api.cloudflare.com/client/v4/accounts/acc123/ai/run/@cf/google/embeddinggemma-300m',
23+
{
24+
method: 'POST',
25+
headers: {
26+
Authorization: 'Bearer test-token',
27+
'Content-Type': 'application/json',
28+
},
29+
body: JSON.stringify({ text: ['hello world'] }),
30+
},
31+
)
32+
33+
expect(res.ok).toBe(true)
34+
const json = (await res.json()) as any
35+
expect(json.success).toBe(true)
36+
expect(Array.isArray(json.result.data)).toBe(true)
37+
expect(Array.isArray(json.result.data[0])).toBe(true)
38+
expect(json.result.data[0].length).toBeGreaterThan(0)
39+
})
40+
41+
test('Workers AI transcription endpoint returns { result: { text } }', async () => {
42+
const res = await fetch(
43+
'https://api.cloudflare.com/client/v4/accounts/acc123/ai/run/%40cf%2Fopenai%2Fwhisper',
44+
{
45+
method: 'POST',
46+
headers: {
47+
Authorization: 'Bearer test-token',
48+
'Content-Type': 'audio/mpeg',
49+
},
50+
body: new Uint8Array([1, 2, 3, 4]),
51+
},
52+
)
53+
54+
expect(res.ok).toBe(true)
55+
const json = (await res.json()) as any
56+
expect(json.success).toBe(true)
57+
expect(typeof json.result.text).toBe('string')
58+
expect(json.result.text.toLowerCase()).toContain('mock transcription')
59+
})
60+
61+
test('Vectorize query uses match-sorter when embedding text is known', async () => {
62+
// Seed the expected doc so the test is independent of the filesystem.
63+
const seedNdjson = `${JSON.stringify({
64+
id: '/__mock__/about-mcp',
65+
values: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
66+
namespace: 'test',
67+
metadata: {
68+
type: 'page',
69+
title: 'About KCD MCP',
70+
url: '/__mock__/about-mcp',
71+
snippet: 'About KCD MCP',
72+
},
73+
})}\n`
74+
const seedRes = await fetch(
75+
'https://api.cloudflare.com/client/v4/accounts/acc123/vectorize/v2/indexes/semantic-index/upsert',
76+
{
77+
method: 'POST',
78+
headers: {
79+
Authorization: 'Bearer test-token',
80+
'Content-Type': 'application/x-ndjson',
81+
},
82+
body: seedNdjson,
83+
},
84+
)
85+
expect(seedRes.ok).toBe(true)
86+
87+
const embedRes = await fetch(
88+
'https://api.cloudflare.com/client/v4/accounts/acc123/ai/run/@cf/google/embeddinggemma-300m',
89+
{
90+
method: 'POST',
91+
headers: {
92+
Authorization: 'Bearer test-token',
93+
'Content-Type': 'application/json',
94+
},
95+
body: JSON.stringify({ text: ['About KCD MCP'] }),
96+
},
97+
)
98+
expect(embedRes.ok).toBe(true)
99+
const embedJson = (await embedRes.json()) as any
100+
const vector = embedJson.result.data[0]
101+
expect(Array.isArray(vector)).toBe(true)
102+
103+
const queryRes = await fetch(
104+
'https://api.cloudflare.com/client/v4/accounts/acc123/vectorize/v2/indexes/semantic-index/query',
105+
{
106+
method: 'POST',
107+
headers: {
108+
Authorization: 'Bearer test-token',
109+
'Content-Type': 'application/json',
110+
},
111+
body: JSON.stringify({
112+
vector,
113+
topK: 5,
114+
returnMetadata: 'all',
115+
namespace: 'test',
116+
}),
117+
},
118+
)
119+
expect(queryRes.ok).toBe(true)
120+
const queryJson = (await queryRes.json()) as any
121+
expect(queryJson.success).toBe(true)
122+
expect(queryJson.result.matches.length).toBeGreaterThan(0)
123+
expect(queryJson.result.matches[0].metadata.title).toBe('About KCD MCP')
124+
expect(String(queryJson.result.matches[0].metadata.url)).toContain('about-mcp')
125+
})
126+
127+
test('Vectorize query returns seeded matches with metadata', async () => {
128+
const res = await fetch(
129+
'https://api.cloudflare.com/client/v4/accounts/acc123/vectorize/v2/indexes/semantic-index/query',
130+
{
131+
method: 'POST',
132+
headers: {
133+
Authorization: 'Bearer test-token',
134+
'Content-Type': 'application/json',
135+
},
136+
body: JSON.stringify({
137+
vector: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
138+
topK: 3,
139+
returnMetadata: 'all',
140+
}),
141+
},
142+
)
143+
144+
expect(res.ok).toBe(true)
145+
const json = (await res.json()) as any
146+
expect(json.success).toBe(true)
147+
expect(Array.isArray(json.result.matches)).toBe(true)
148+
expect(json.result.matches.length).toBeGreaterThan(0)
149+
expect(typeof json.result.matches[0].id).toBe('string')
150+
expect(typeof json.result.matches[0].score).toBe('number')
151+
expect(typeof json.result.matches[0].metadata?.title).toBe('string')
152+
})
153+
154+
test('Vectorize upsert + query + delete_by_ids works', async () => {
155+
const accountId = 'acc999'
156+
const indexName = 'upsert-index'
157+
const id = '/mock/doc'
158+
const values = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
159+
160+
const ndjson = `${JSON.stringify({
161+
id,
162+
values,
163+
metadata: {
164+
type: 'doc',
165+
title: 'Mock Doc',
166+
url: 'https://kentcdodds.com/mock/doc',
167+
snippet: 'Inserted via Vectorize upsert mock.',
168+
},
169+
})}\n`
170+
171+
// Vitest runs in `jsdom` by default in this repo; the `FormData` impl from
172+
// jsdom is not always compatible with Node's `fetch` (undici). Use a tiny,
173+
// hand-crafted multipart body to exercise the mock parser reliably.
174+
const boundary = '----vitest-multipart-boundary'
175+
const multipartBody = [
176+
`--${boundary}\r\n`,
177+
'Content-Disposition: form-data; name="vectors"; filename="vectors.ndjson"\r\n',
178+
'Content-Type: application/x-ndjson\r\n',
179+
'\r\n',
180+
ndjson,
181+
`\r\n--${boundary}--\r\n`,
182+
].join('')
183+
184+
const upsertRes = await fetch(
185+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/vectorize/v2/indexes/${indexName}/upsert`,
186+
{
187+
method: 'POST',
188+
headers: {
189+
Authorization: 'Bearer test-token',
190+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
191+
},
192+
body: multipartBody,
193+
},
194+
)
195+
expect(upsertRes.ok).toBe(true)
196+
197+
const queryRes = await fetch(
198+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/vectorize/v2/indexes/${indexName}/query`,
199+
{
200+
method: 'POST',
201+
headers: {
202+
Authorization: 'Bearer test-token',
203+
'Content-Type': 'application/json',
204+
},
205+
body: JSON.stringify({ vector: values, topK: 5, returnMetadata: 'all' }),
206+
},
207+
)
208+
expect(queryRes.ok).toBe(true)
209+
const queryJson = (await queryRes.json()) as any
210+
const ids = queryJson.result.matches.map((m: any) => m.id)
211+
expect(ids).toContain(id)
212+
213+
const deleteRes = await fetch(
214+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/vectorize/v2/indexes/${indexName}/delete_by_ids`,
215+
{
216+
method: 'POST',
217+
headers: {
218+
Authorization: 'Bearer test-token',
219+
'Content-Type': 'application/json',
220+
},
221+
body: JSON.stringify({ ids: [id] }),
222+
},
223+
)
224+
expect(deleteRes.ok).toBe(true)
225+
const deleteJson = (await deleteRes.json()) as any
226+
expect(deleteJson.success).toBe(true)
227+
expect(deleteJson.result.deleted).toBeGreaterThan(0)
228+
})
229+
})
230+

0 commit comments

Comments
 (0)