diff --git a/cli/test/integration/_gateway_helpers.ts b/cli/test/integration/_gateway_helpers.ts new file mode 100644 index 00000000..a21ffc41 --- /dev/null +++ b/cli/test/integration/_gateway_helpers.ts @@ -0,0 +1,198 @@ +import { join } from '@std/path' + +// Shared plumbing for the `slv gateway run` integration tests. Each +// test spawns the CLI as a subprocess with an isolated HOME + port so +// they don't pollute the developer's real ~/.slv/gateway/ nor collide +// with each other. Tests interact via HTTP /healthz, the JSON pidfile +// on disk, the WS endpoint, and exit codes. +// +// Deno's resource/op sanitizers flag subprocess streams as leaks even +// when we fully manage them, so each test opts out (sanitizeResources +// and sanitizeOps both false). We still cancel streams + await status +// in the shared cleanup helper, so nothing actually leaks at the OS +// level. + +export const CLI_ENTRY = new URL('../../src/index.ts', import.meta.url).pathname + +/** + * Return an OS-assigned free loopback port. Binding to port 0 lets the + * kernel pick a port that's actually free right now, then we release it + * immediately. This avoids the random-port collisions that made these + * tests flake (an in-use port → bind fail → polling a dead process). + */ +export const pickPort = (): number => { + const l = Deno.listen({ hostname: '127.0.0.1', port: 0 }) + const { port } = l.addr as Deno.NetAddr + l.close() + return port +} + +export type Proc = { + child: Deno.ChildProcess + home: string + port: number + stderr: Promise // eagerly-drained stderr text +} + +export type Gw = { + child: Deno.ChildProcess + home: string + port: number + token: string + stderr: Promise +} + +/** Fully drain a byte stream into a decoded string. */ +const drain = async (s: ReadableStream): Promise => { + const chunks: Uint8Array[] = [] + const reader = s.getReader() + while (true) { + const { value, done } = await reader.read() + if (done) break + if (value) chunks.push(value) + } + const total = chunks.reduce((n, c) => n + c.length, 0) + const out = new Uint8Array(total) + let o = 0 + for (const c of chunks) { + out.set(c, o) + o += c.length + } + return new TextDecoder().decode(out) +} + +/** + * Warm the Deno module cache for the CLI entry exactly once per test + * run. Each `deno run --no-check` subprocess otherwise re-downloads + + * compiles the whole CLI graph on a cold cache; under the parallel load + * of many spawns this can push a single cold start past the healthz + * deadline (the historical source of the flake). `deno cache` populates + * the shared on-disk cache so every subsequent spawn boots fast. + */ +let cacheWarm: Promise | undefined +const warmCache = (): Promise => { + if (!cacheWarm) { + cacheWarm = new Deno.Command(Deno.execPath(), { + args: ['cache', CLI_ENTRY], + stdin: 'null', + stdout: 'null', + stderr: 'null', + }).output().then(() => {}).catch(() => {}) + } + return cacheWarm +} + +export const spawnGateway = async ( + opts: { + port?: number + homeSeed?: (home: string) => void | Promise + envPort?: string // override SLV_GATEWAY_PORT literally (bad-port tests) + } = {}, +): Promise => { + await warmCache() + const home = await Deno.makeTempDir({ prefix: 'slv-gw-it-' }) + if (opts.homeSeed) await opts.homeSeed(home) + const port = opts.port ?? pickPort() + const env: Record = { + HOME: home, + PATH: Deno.env.get('PATH') ?? '/usr/bin:/bin', + SLV_GATEWAY_PORT: opts.envPort ?? String(port), + } + const child = new Deno.Command(Deno.execPath(), { + args: ['run', '-A', '--no-check', CLI_ENTRY, 'gateway', 'run'], + env, + stdin: 'null', + stdout: 'piped', + stderr: 'piped', + }).spawn() + // Eagerly drain stdout so the subprocess can't block on a full pipe + // during tests that don't care about it. The stderr future is + // returned for tests that assert on it. + drain(child.stdout).catch(() => {}) + const stderr = drain(child.stderr).catch(() => '') + return { child, home, port, stderr } +} + +/** + * Poll /healthz until the gateway answers, then return the parsed body. + * + * Fails fast: if the subprocess exits before /healthz comes up (e.g. a + * bind failure on a port that raced into use), we throw immediately with + * the drained stderr included, instead of polling the dead process until + * the deadline. Default timeout is generous (30s) so a cold subprocess + * start on a loaded CI runner doesn't flake. + */ +export const waitForHealthz = async ( + proc: Proc, + timeoutMs = 30_000, +): Promise<{ ok: boolean; pid: number; port: number; startedAt: string }> => { + const { port } = proc + let exited: { code: number; signal: Deno.Signal | null } | undefined + proc.child.status.then((s) => { + exited = s + }).catch(() => {}) + + const deadline = Date.now() + timeoutMs + let lastErr: unknown + while (Date.now() < deadline) { + if (exited) { + const err = await proc.stderr.catch(() => '') + throw new Error( + `gateway process exited before /healthz on :${port} ` + + `(code=${exited.code} signal=${exited.signal})\n--- stderr ---\n${err}`, + ) + } + try { + const res = await fetch(`http://127.0.0.1:${port}/healthz`) + if (res.ok) return await res.json() + await res.body?.cancel() + } catch (err) { + lastErr = err + } + await new Promise((r) => setTimeout(r, 100)) + } + throw new Error( + `gateway did not become healthy on :${port} within ${timeoutMs}ms${ + lastErr ? ` (last: ${lastErr})` : '' + }`, + ) +} + +export const cleanup = async (p: Proc): Promise => { + try { + p.child.kill('SIGTERM') + } catch { /* already dead */ } + await p.child.status.catch(() => {}) + await p.stderr // ensure stderr drain has completed + await Deno.remove(p.home, { recursive: true }).catch(() => {}) +} + +/** Alias kept for the ws/session/web_ui tests that say `stopGateway`. */ +export const stopGateway = cleanup + +/** + * Convenience for the WS/session/UI tests: spawn, wait for healthz, then + * read the token from the config file written by the first run. + */ +export const startGateway = async ( + opts: { + port?: number + } = {}, +): Promise => { + const proc = await spawnGateway(opts) + await waitForHealthz(proc) + const cfg = JSON.parse( + await Deno.readTextFile(join(proc.home, '.slv/gateway/gateway.json')), + ) as { token: string } + return { + child: proc.child, + home: proc.home, + port: proc.port, + token: cfg.token, + stderr: proc.stderr, + } +} + +// Shared options for every subprocess test: subprocess streams are +// owned by the helper + drained; Deno's sanitizer doesn't know that. +export const sub = { sanitizeResources: false, sanitizeOps: false } as const diff --git a/cli/test/integration/gateway_foreground.test.ts b/cli/test/integration/gateway_foreground.test.ts index 8f43fb06..dd96e442 100644 --- a/cli/test/integration/gateway_foreground.test.ts +++ b/cli/test/integration/gateway_foreground.test.ts @@ -1,110 +1,18 @@ import { assert, assertEquals, assertMatch } from '@std/assert' import { join } from '@std/path' +import { + cleanup, + pickPort, + spawnGateway, + sub, + waitForHealthz, +} from '/test/integration/_gateway_helpers.ts' // Integration tests for `slv gateway run`. Each test spawns the CLI as // a subprocess with an isolated HOME + port so they don't pollute the // developer's real ~/.slv/gateway/ nor collide with each other. Tests // interact via HTTP /healthz, the JSON pidfile on disk, and exit codes. -// -// Deno's resource/op sanitizers flag subprocess streams as leaks even -// when we fully manage them, so each test opts out (sanitizeResources -// and sanitizeOps both false). We still cancel streams + await status -// in the shared cleanup helper, so nothing actually leaks at the OS -// level. - -const CLI_ENTRY = new URL('../../src/index.ts', import.meta.url).pathname - -const pickPort = (): number => 30000 + Math.floor(Math.random() * 10000) - -type Proc = { - child: Deno.ChildProcess - home: string - port: number - stderr: Promise // eagerly-drained stderr text -} - -const spawnGateway = async ( - opts: { - port?: number - homeSeed?: (home: string) => void | Promise - envPort?: string // override SLV_GATEWAY_PORT literally (bad-port tests) - } = {}, -): Promise => { - const home = await Deno.makeTempDir({ prefix: 'slv-gw-it-' }) - if (opts.homeSeed) await opts.homeSeed(home) - const port = opts.port ?? pickPort() - const env: Record = { - HOME: home, - PATH: Deno.env.get('PATH') ?? '/usr/bin:/bin', - SLV_GATEWAY_PORT: opts.envPort ?? String(port), - } - const child = new Deno.Command(Deno.execPath(), { - args: ['run', '-A', '--no-check', CLI_ENTRY, 'gateway', 'run'], - env, - stdin: 'null', - stdout: 'piped', - stderr: 'piped', - }).spawn() - // Eagerly drain stdout so the subprocess can't block on a full pipe - // during tests that don't care about it. The stderr future is - // returned for tests that assert on it. - const drain = async (s: ReadableStream): Promise => { - const chunks: Uint8Array[] = [] - const reader = s.getReader() - while (true) { - const { value, done } = await reader.read() - if (done) break - if (value) chunks.push(value) - } - const total = chunks.reduce((n, c) => n + c.length, 0) - const out = new Uint8Array(total) - let o = 0 - for (const c of chunks) { - out.set(c, o) - o += c.length - } - return new TextDecoder().decode(out) - } - drain(child.stdout).catch(() => {}) - const stderr = drain(child.stderr).catch(() => '') - return { child, home, port, stderr } -} - -const waitForHealthz = async ( - port: number, - timeoutMs = 10_000, -): Promise<{ ok: boolean; pid: number; port: number; startedAt: string }> => { - const deadline = Date.now() + timeoutMs - let lastErr: unknown - while (Date.now() < deadline) { - try { - const res = await fetch(`http://127.0.0.1:${port}/healthz`) - if (res.ok) return await res.json() - await res.body?.cancel() - } catch (err) { - lastErr = err - } - await new Promise((r) => setTimeout(r, 100)) - } - throw new Error( - `gateway did not become healthy on :${port} within ${timeoutMs}ms${ - lastErr ? ` (last: ${lastErr})` : '' - }`, - ) -} - -const cleanup = async (p: Proc) => { - try { - p.child.kill('SIGTERM') - } catch { /* already dead */ } - await p.child.status.catch(() => {}) - await p.stderr // ensure stderr drain has completed - await Deno.remove(p.home, { recursive: true }).catch(() => {}) -} - -// Shared options for every subprocess test: subprocess streams are -// owned by the helper + drained; Deno's sanitizer doesn't know that. -const sub = { sanitizeResources: false, sanitizeOps: false } as const +// Shared spawn/health/port/cleanup plumbing lives in _gateway_helpers.ts. Deno.test( 'gateway run: fresh start writes config + pidfile, /healthz works, SIGTERM cleans up', @@ -112,7 +20,7 @@ Deno.test( async () => { const p = await spawnGateway() try { - const body = await waitForHealthz(p.port) + const body = await waitForHealthz(p) assertEquals(body.ok, true) assertEquals(body.port, p.port) assert(typeof body.pid === 'number' && body.pid > 0) @@ -188,7 +96,7 @@ Deno.test( async () => { const first = await spawnGateway() try { - await waitForHealthz(first.port) + await waitForHealthz(first) const firstPid = first.child.pid // Second run with a different HOME but sharing the first's @@ -249,7 +157,7 @@ Deno.test( }, }) try { - const body = await waitForHealthz(p.port) + const body = await waitForHealthz(p) assertEquals(body.ok, true) // Lock was replaced with our fresh pid const pid = JSON.parse( @@ -292,7 +200,7 @@ Deno.test( async () => { const p = await spawnGateway() try { - await waitForHealthz(p.port) + await waitForHealthz(p) const res = await fetch(`http://127.0.0.1:${p.port}/`) const body = await res.json() assertEquals(body.service, 'slv-gateway') diff --git a/cli/test/integration/gateway_session_abort.test.ts b/cli/test/integration/gateway_session_abort.test.ts index 60967e90..dc014050 100644 --- a/cli/test/integration/gateway_session_abort.test.ts +++ b/cli/test/integration/gateway_session_abort.test.ts @@ -1,82 +1,16 @@ import { assert, assertEquals } from '@std/assert' -import { join } from '@std/path' +import { + startGateway, + stopGateway, + sub, +} from '/test/integration/_gateway_helpers.ts' // End-to-end tests for Phase 2D-v3: one persistent WS that carries // both the chat and the mid-stream `session.abort`. Verifies the // abort sequence fires a single `aborted` terminal with NO trailing // `complete`, and that the same connection can be reused for a -// follow-up `session.echo` without re-authenticating. - -const CLI_ENTRY = new URL('../../src/index.ts', import.meta.url).pathname -const pickPort = (): number => 30000 + Math.floor(Math.random() * 10000) - -type Gw = { - child: Deno.ChildProcess - home: string - port: number - token: string - stderr: Promise -} - -const startGateway = async (): Promise => { - const home = await Deno.makeTempDir({ prefix: 'slv-gw-abort-' }) - const port = pickPort() - const child = new Deno.Command(Deno.execPath(), { - args: ['run', '-A', '--no-check', CLI_ENTRY, 'gateway', 'run'], - env: { - HOME: home, - PATH: Deno.env.get('PATH') ?? '/usr/bin:/bin', - SLV_GATEWAY_PORT: String(port), - }, - stdin: 'null', - stdout: 'piped', - stderr: 'piped', - }).spawn() - const drain = async (s: ReadableStream): Promise => { - const r = s.getReader() - const chunks: Uint8Array[] = [] - while (true) { - const { value, done } = await r.read() - if (done) break - if (value) chunks.push(value) - } - const total = chunks.reduce((n, c) => n + c.length, 0) - const buf = new Uint8Array(total) - let o = 0 - for (const c of chunks) { - buf.set(c, o) - o += c.length - } - return new TextDecoder().decode(buf) - } - drain(child.stdout).catch(() => {}) - const stderr = drain(child.stderr).catch(() => '') - const deadline = Date.now() + 10_000 - while (Date.now() < deadline) { - try { - const res = await fetch(`http://127.0.0.1:${port}/healthz`) - if (res.ok) { - await res.body?.cancel() - break - } - await res.body?.cancel() - } catch { /* retry */ } - await new Promise((r) => setTimeout(r, 100)) - } - const cfg = JSON.parse( - await Deno.readTextFile(join(home, '.slv/gateway/gateway.json')), - ) as { token: string } - return { child, home, port, token: cfg.token, stderr } -} - -const stopGateway = async (gw: Gw): Promise => { - try { - gw.child.kill('SIGTERM') - } catch { /* already dead */ } - await gw.child.status.catch(() => {}) - await gw.stderr - await Deno.remove(gw.home, { recursive: true }).catch(() => {}) -} +// follow-up `session.echo` without re-authenticating. Shared +// spawn/health/cleanup lives in _gateway_helpers.ts. type EventFrame = { kind: 'event' @@ -128,7 +62,10 @@ const openWs = (port: number): Promise<{ resW(found) return } - const timer = setTimeout(() => rejW(new Error('timeout')), timeoutMs) + const timer = setTimeout( + () => rejW(new Error('timeout')), + timeoutMs, + ) waiters.push({ check: pred, resolve: (e) => { @@ -160,8 +97,6 @@ const openWs = (port: number): Promise<{ ws.onerror = (e) => reject(e) }) -const sub = { sanitizeResources: false, sanitizeOps: false } as const - Deno.test( 'ws: session.abort mid-stream cancels echo with `aborted`, no `complete`', sub, @@ -186,9 +121,7 @@ Deno.test( await c.waitForEvent((e) => e.payload?.type === 'aborted') - const completes = c.events.filter((e) => - e.payload?.type === 'complete' - ) + const completes = c.events.filter((e) => e.payload?.type === 'complete') assertEquals( completes.length, 0, diff --git a/cli/test/integration/gateway_web_ui.test.ts b/cli/test/integration/gateway_web_ui.test.ts index c15ce10e..24454b6a 100644 --- a/cli/test/integration/gateway_web_ui.test.ts +++ b/cli/test/integration/gateway_web_ui.test.ts @@ -1,64 +1,13 @@ import { assert, assertEquals, assertStringIncludes } from '@std/assert' -import { join } from '@std/path' +import { + startGateway, + stopGateway, + sub, +} from '/test/integration/_gateway_helpers.ts' // Verifies the browser-UI endpoints serve the expected HTML with // the gateway's token inlined as a data-attribute. Loopback only. - -const CLI_ENTRY = new URL('../../src/index.ts', import.meta.url).pathname -const pickPort = (): number => 30000 + Math.floor(Math.random() * 10000) - -const startGateway = async () => { - const home = await Deno.makeTempDir({ prefix: 'slv-gw-ui-' }) - const port = pickPort() - const child = new Deno.Command(Deno.execPath(), { - args: ['run', '-A', '--no-check', CLI_ENTRY, 'gateway', 'run'], - env: { - HOME: home, - PATH: Deno.env.get('PATH') ?? '/usr/bin:/bin', - SLV_GATEWAY_PORT: String(port), - }, - stdin: 'null', - stdout: 'piped', - stderr: 'piped', - }).spawn() - const drain = async (s: ReadableStream) => { - const r = s.getReader() - while (true) { - const { done } = await r.read() - if (done) break - } - } - drain(child.stdout).catch(() => {}) - drain(child.stderr).catch(() => '') - const deadline = Date.now() + 10_000 - while (Date.now() < deadline) { - try { - const res = await fetch(`http://127.0.0.1:${port}/healthz`) - if (res.ok) { - await res.body?.cancel() - break - } - await res.body?.cancel() - } catch { /* retry */ } - await new Promise((r) => setTimeout(r, 100)) - } - const cfg = JSON.parse( - await Deno.readTextFile(join(home, '.slv/gateway/gateway.json')), - ) as { token: string } - return { child, home, port, token: cfg.token } -} - -const stopGateway = async ( - gw: { child: Deno.ChildProcess; home: string }, -) => { - try { - gw.child.kill('SIGTERM') - } catch { /* already dead */ } - await gw.child.status.catch(() => {}) - await Deno.remove(gw.home, { recursive: true }).catch(() => {}) -} - -const sub = { sanitizeResources: false, sanitizeOps: false } as const +// Shared spawn/health/cleanup lives in _gateway_helpers.ts. Deno.test('ui: GET /ui/ serves HTML with token inlined', sub, async () => { const gw = await startGateway() diff --git a/cli/test/integration/gateway_ws.test.ts b/cli/test/integration/gateway_ws.test.ts index dc556832..97ca7fdd 100644 --- a/cli/test/integration/gateway_ws.test.ts +++ b/cli/test/integration/gateway_ws.test.ts @@ -1,86 +1,16 @@ import { assert, assertEquals } from '@std/assert' -import { join } from '@std/path' +import { + startGateway, + stopGateway, + sub, +} from '/test/integration/_gateway_helpers.ts' // End-to-end WS protocol tests against a real gateway subprocess. -// Spawns `slv gateway run` with isolated HOME + random port, opens a +// Boots `slv gateway run` with an isolated HOME + free port, opens a // browser WebSocket to /v1/session/ws, walks the hello → auth → ping // handshake, and verifies invalid frames + unknown methods are -// rejected cleanly. - -const CLI_ENTRY = new URL('../../src/index.ts', import.meta.url).pathname -const pickPort = (): number => 30000 + Math.floor(Math.random() * 10000) - -type Gw = { - child: Deno.ChildProcess - home: string - port: number - token: string - stderr: Promise -} - -const startGateway = async (): Promise => { - const home = await Deno.makeTempDir({ prefix: 'slv-gw-ws-' }) - const port = pickPort() - const child = new Deno.Command(Deno.execPath(), { - args: ['run', '-A', '--no-check', CLI_ENTRY, 'gateway', 'run'], - env: { - HOME: home, - PATH: Deno.env.get('PATH') ?? '/usr/bin:/bin', - SLV_GATEWAY_PORT: String(port), - }, - stdin: 'null', - stdout: 'piped', - stderr: 'piped', - }).spawn() - const drain = async (s: ReadableStream): Promise => { - const r = s.getReader() - const chunks: Uint8Array[] = [] - while (true) { - const { value, done } = await r.read() - if (done) break - if (value) chunks.push(value) - } - const total = chunks.reduce((n, c) => n + c.length, 0) - const buf = new Uint8Array(total) - let o = 0 - for (const c of chunks) { - buf.set(c, o) - o += c.length - } - return new TextDecoder().decode(buf) - } - drain(child.stdout).catch(() => {}) - const stderr = drain(child.stderr).catch(() => '') - - // Poll /healthz until the server is up, then read the token from - // the config file written by the first run. - const deadline = Date.now() + 10_000 - while (Date.now() < deadline) { - try { - const res = await fetch(`http://127.0.0.1:${port}/healthz`) - if (res.ok) { - await res.body?.cancel() - break - } - await res.body?.cancel() - } catch { /* try again */ } - await new Promise((r) => setTimeout(r, 100)) - } - const cfg = JSON.parse( - await Deno.readTextFile(join(home, '.slv/gateway/gateway.json')), - ) as { token: string } - - return { child, home, port, token: cfg.token, stderr } -} - -const stopGateway = async (gw: Gw): Promise => { - try { - gw.child.kill('SIGTERM') - } catch { /* already dead */ } - await gw.child.status.catch(() => {}) - await gw.stderr - await Deno.remove(gw.home, { recursive: true }).catch(() => {}) -} +// rejected cleanly. Shared spawn/health/cleanup lives in +// _gateway_helpers.ts. /** Thin RPC helper over a WS: send a req, await the matching res. */ type WsClient = { @@ -131,8 +61,6 @@ const openWs = (port: number): Promise => ws.onerror = (e) => reject(e) }) -const sub = { sanitizeResources: false, sanitizeOps: false } as const - Deno.test('ws: hello → auth → ping handshake happy path', sub, async () => { const gw = await startGateway() try { diff --git a/cli/test/integration/gateway_ws_session.test.ts b/cli/test/integration/gateway_ws_session.test.ts index 2b34b5ff..c0d7c0eb 100644 --- a/cli/test/integration/gateway_ws_session.test.ts +++ b/cli/test/integration/gateway_ws_session.test.ts @@ -1,80 +1,14 @@ import { assert, assertEquals } from '@std/assert' -import { join } from '@std/path' +import { + startGateway, + stopGateway, + sub, +} from '/test/integration/_gateway_helpers.ts' // End-to-end WS tests for Phase 2B's `session.echo` + `session.abort` // methods. Boots a real gateway subprocess, authenticates, issues the -// session methods, and collects streaming event frames. - -const CLI_ENTRY = new URL('../../src/index.ts', import.meta.url).pathname -const pickPort = (): number => 30000 + Math.floor(Math.random() * 10000) - -type Gw = { - child: Deno.ChildProcess - home: string - port: number - token: string - stderr: Promise -} - -const startGateway = async (): Promise => { - const home = await Deno.makeTempDir({ prefix: 'slv-gw-sess-' }) - const port = pickPort() - const child = new Deno.Command(Deno.execPath(), { - args: ['run', '-A', '--no-check', CLI_ENTRY, 'gateway', 'run'], - env: { - HOME: home, - PATH: Deno.env.get('PATH') ?? '/usr/bin:/bin', - SLV_GATEWAY_PORT: String(port), - }, - stdin: 'null', - stdout: 'piped', - stderr: 'piped', - }).spawn() - const drain = async (s: ReadableStream): Promise => { - const r = s.getReader() - const chunks: Uint8Array[] = [] - while (true) { - const { value, done } = await r.read() - if (done) break - if (value) chunks.push(value) - } - const total = chunks.reduce((n, c) => n + c.length, 0) - const buf = new Uint8Array(total) - let o = 0 - for (const c of chunks) { - buf.set(c, o) - o += c.length - } - return new TextDecoder().decode(buf) - } - drain(child.stdout).catch(() => {}) - const stderr = drain(child.stderr).catch(() => '') - const deadline = Date.now() + 10_000 - while (Date.now() < deadline) { - try { - const res = await fetch(`http://127.0.0.1:${port}/healthz`) - if (res.ok) { - await res.body?.cancel() - break - } - await res.body?.cancel() - } catch { /* try again */ } - await new Promise((r) => setTimeout(r, 100)) - } - const cfg = JSON.parse( - await Deno.readTextFile(join(home, '.slv/gateway/gateway.json')), - ) as { token: string } - return { child, home, port, token: cfg.token, stderr } -} - -const stopGateway = async (gw: Gw): Promise => { - try { - gw.child.kill('SIGTERM') - } catch { /* already dead */ } - await gw.child.status.catch(() => {}) - await gw.stderr - await Deno.remove(gw.home, { recursive: true }).catch(() => {}) -} +// session methods, and collects streaming event frames. Shared +// spawn/health/cleanup lives in _gateway_helpers.ts. type Frame = | { kind: 'res'; id: string; ok: boolean; payload?: unknown; error?: string } @@ -126,14 +60,19 @@ const openWs = (port: number): Promise => .map((e) => e.payload), waitForEvent: (predicate, timeoutMs = 5000) => new Promise((resolve2, reject2) => { - const existing = events.find((e): e is Extract => + const existing = events.find(( + e, + ): e is Extract => e.kind === 'event' && predicate(e) ) if (existing) { resolve2(existing) return } - const timer = setTimeout(() => reject2(new Error('event wait timeout')), timeoutMs) + const timer = setTimeout( + () => reject2(new Error('event wait timeout')), + timeoutMs, + ) eventWaiters.push({ check: predicate, resolve: (f) => { @@ -166,8 +105,6 @@ const openWs = (port: number): Promise => ws.onerror = (e) => reject(e) }) -const sub = { sanitizeResources: false, sanitizeOps: false } as const - Deno.test( 'ws: session.echo streams text_delta events then complete', sub, @@ -196,10 +133,15 @@ Deno.test( // Seq is monotonically increasing const seqs = c.events - .filter((e): e is Extract => e.kind === 'event') + .filter((e): e is Extract => + e.kind === 'event' + ) .map((e) => e.seq) for (let i = 1; i < seqs.length; i++) { - assert(seqs[i] > seqs[i - 1], `seq must increase: ${seqs[i - 1]} → ${seqs[i]}`) + assert( + seqs[i] > seqs[i - 1], + `seq must increase: ${seqs[i - 1]} → ${seqs[i]}`, + ) } } finally { c.close()