mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
77 lines
3.0 KiB
TypeScript
77 lines
3.0 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { read } from '../../lib/api/read'
|
|
|
|
/**
|
|
* A READ WAITS 15 S FOR ITS REPLY, AND THEN AS LONG AS ITS BODY TAKES.
|
|
*
|
|
* `lib/api/read` gives up on a read whose reply has not started in 15 s, so an
|
|
* answer that never comes is unreachable; a body that has started is read to
|
|
* the end however slowly it arrives, so a slow line is never cut off. The
|
|
* fetch here is a stand-in whose body arrives in timed parts; it honours the
|
|
* read's abort the way a browser does.
|
|
*/
|
|
|
|
const SECOND = 1000
|
|
/** The document a read accepts: any object. */
|
|
const object = (b: unknown) => (b && typeof b === 'object' ? (b as Record<string, unknown>) : null)
|
|
|
|
/** Wait `ms`, or fail when `signal` aborts first; `Infinity` waits for the abort alone. */
|
|
const sleep = (ms: number, signal?: AbortSignal | null) =>
|
|
new Promise<void>((done, fail) => {
|
|
const t = Number.isFinite(ms) ? setTimeout(done, ms) : undefined
|
|
signal?.addEventListener('abort', () => (clearTimeout(t), fail(signal.reason)), { once: true })
|
|
})
|
|
|
|
/** A fetch whose body is `parts`, the first at once and each later one `gap` after the last; `close` ends it, else it stalls. */
|
|
const parts =
|
|
(sent: string[], gap: number, close: boolean): typeof fetch =>
|
|
async (_url, init) => {
|
|
const signal = init?.signal
|
|
const bytes = new TextEncoder()
|
|
let i = 0
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
async pull(c) {
|
|
try {
|
|
if (i < sent.length) {
|
|
await sleep(i ? gap : 0, signal)
|
|
c.enqueue(bytes.encode(sent[i++]))
|
|
} else if (close) c.close()
|
|
else await sleep(Infinity, signal)
|
|
} catch (e) {
|
|
c.error(e)
|
|
}
|
|
},
|
|
})
|
|
return new Response(stream, { status: 200, headers: { 'content-type': 'application/json' } })
|
|
}
|
|
|
|
/** Run `act` with `stub` as the page's fetch, and the time it took. */
|
|
async function timed<T>(stub: typeof fetch, act: () => Promise<T>): Promise<{ took: number; got: T | Error }> {
|
|
const real = globalThis.fetch
|
|
globalThis.fetch = stub
|
|
const t0 = Date.now()
|
|
try {
|
|
const got = await act().catch((e: Error) => e)
|
|
return { took: Date.now() - t0, got }
|
|
} finally {
|
|
globalThis.fetch = real
|
|
}
|
|
}
|
|
|
|
test.describe.configure({ timeout: 60 * SECOND })
|
|
|
|
test('a body that keeps arriving is read to the end, however long it takes', async () => {
|
|
const { took, got } = await timed(parts(['{"a"', ':', '1}'], 10 * SECOND, true), () => read('/v1/patience/slow', object))
|
|
expect(got).toEqual({ a: 1 })
|
|
expect(took, 'longer than the 15 s a read waits for its reply').toBeGreaterThan(19 * SECOND)
|
|
})
|
|
|
|
test('a reply that never starts is given up after 15 s', async () => {
|
|
const never: typeof fetch = (_url, init) => sleep(Infinity, init?.signal).then(() => new Response(null))
|
|
const { took, got } = await timed(never, () => read('/v1/patience/silent', object))
|
|
expect(got).toBeInstanceOf(Error)
|
|
expect((got as Error).message).toContain('no reply in 15 s')
|
|
expect(took).toBeGreaterThanOrEqual(15 * SECOND - 50)
|
|
expect(took).toBeLessThan(18 * SECOND)
|
|
})
|