Files
hanzo.ai/e2e/unit/patience.spec.ts

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)
})