mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 08:02:38 +00:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
import type { FetchLike, StorageLike } from '../src/types'
|
|
|
|
export interface RecordedCall {
|
|
method: string
|
|
url: URL
|
|
headers: Record<string, string>
|
|
body: unknown
|
|
/** A body sent as is rather than as JSON (an upload). */
|
|
raw?: Blob
|
|
signal?: AbortSignal
|
|
}
|
|
|
|
export type Route = (call: RecordedCall) => Response | Promise<Response> | undefined
|
|
|
|
/** A fetch whose responses come from the first route that answers; every call is recorded. */
|
|
export function fetchMock(...routes: Route[]): { fetch: FetchLike; calls: RecordedCall[] } {
|
|
const calls: RecordedCall[] = []
|
|
const fetch: FetchLike = async (input, init) => {
|
|
const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url)
|
|
const call: RecordedCall = {
|
|
method: init?.method ?? 'GET',
|
|
url,
|
|
headers: Object.fromEntries(
|
|
Object.entries((init?.headers as Record<string, string>) ?? {}).map(([k, v]) => [k.toLowerCase(), v])
|
|
),
|
|
body: typeof init?.body === 'string' ? JSON.parse(init.body) : undefined,
|
|
raw: init?.body instanceof Blob ? init.body : undefined,
|
|
signal: init?.signal ?? undefined
|
|
}
|
|
calls.push(call)
|
|
for (const route of routes) {
|
|
const res = await route(call)
|
|
if (res) return res
|
|
}
|
|
return new Response(`no route for ${call.method} ${url.pathname}`, { status: 404 })
|
|
}
|
|
return { fetch, calls }
|
|
}
|
|
|
|
export function json(value: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(value), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' }
|
|
})
|
|
}
|
|
|
|
export function text(value: string, status = 200): Response {
|
|
return new Response(value, { status })
|
|
}
|
|
|
|
/** A `text/event-stream` body carrying one `data:` frame per event. */
|
|
export function sse(events: object[]): Response {
|
|
return new Response(events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join(''), {
|
|
status: 200,
|
|
headers: { 'content-type': 'text/event-stream' }
|
|
})
|
|
}
|
|
|
|
/** Like `sse`, but a number in the list pauses that many milliseconds before the next frame. */
|
|
export function sseTimed(events: (object | number)[]): Response {
|
|
const encoder = new TextEncoder()
|
|
const body = new ReadableStream<Uint8Array>({
|
|
async start(controller) {
|
|
for (const e of events) {
|
|
if (typeof e === 'number') await new Promise((r) => setTimeout(r, e))
|
|
else controller.enqueue(encoder.encode(`data: ${JSON.stringify(e)}\n\n`))
|
|
}
|
|
controller.close()
|
|
}
|
|
})
|
|
return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream' } })
|
|
}
|
|
|
|
export function ndjson(...events: object[]): string {
|
|
return events.map((e) => JSON.stringify(e)).join('\n') + '\n'
|
|
}
|
|
|
|
export function memoryStorage(): StorageLike & { data: Map<string, string> } {
|
|
const data = new Map<string, string>()
|
|
return {
|
|
data,
|
|
getItem: (k) => data.get(k) ?? null,
|
|
setItem: (k, v) => void data.set(k, v),
|
|
removeItem: (k) => void data.delete(k)
|
|
}
|
|
}
|
|
|
|
export function messageRow(
|
|
seq: number,
|
|
type: 'user' | 'assistant' | 'tool',
|
|
content: string,
|
|
extra: Record<string, unknown> = {}
|
|
) {
|
|
return {
|
|
id: `row-${seq}`,
|
|
conversation_id: 'conv',
|
|
message_type: type,
|
|
content,
|
|
job_id: null,
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
created_seq: seq,
|
|
step_name: null,
|
|
success: true,
|
|
...extra
|
|
}
|
|
}
|