import { afterEach, beforeEach, expect, test } from 'bun:test'; import { existsSync } from 'node:fs'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import type { Server } from 'bun'; import { layout, portsForSlot, readJson, saveConfig } from './config'; import { callWorker } from './control'; import { FakeDesktop } from './fakes'; import { runWorker, type WorkerState } from './worker'; const CHANNEL = '0b7f3c2a-1d2e-4f5a-8b9c-0d1e2f3a4b5c'; let home: string; let vash: Server; const received: Record[] = []; const posts: Record[] = []; let live = false; beforeEach(async () => { home = await mkdtemp(join(tmpdir(), 'vash-worker-')); process.env.VASH_BROADCAST_HOME = home; received.length = 0; posts.length = 0; live = false; vash = Bun.serve({ port: 0, async fetch(request, server) { const url = new URL(request.url); if (url.pathname === '/ws/agent') return server.upgrade(request) ? undefined : new Response('no'); if (request.headers.get('authorization') !== 'Bearer vash_test') return new Response('no', { status: 401 }); if (url.pathname === `/api/broadcast/${CHANNEL}/stream` && request.method === 'GET') return Response.json({ live, name: 'Main', width: 1920, height: 1080, fps: 60, bitrateKbps: 6000, overlayUrl: `${url.origin}/broadcast/${CHANNEL}/overlay`, record: false, destinations: [] }); if (url.pathname === `/api/broadcast/${CHANNEL}/stream`) { const body = (await request.json()) as Record; posts.push(body); return Response.json( body.event === 'start' ? { id: 'session-1', storagePrefix: null } : { ok: true } ); } return new Response('not found', { status: 404 }); }, websocket: { message(ws, raw) { const message = JSON.parse(String(raw)); received.push(message); if (message.type === 'hello') { ws.send(JSON.stringify({ type: 'welcome', channel: 'Main' })); ws.send( JSON.stringify({ type: 'follow', scene: 'idle', matchId: null, lobbyId: null, roomName: null, teamSize: null, title: null }) ); } } } }); await saveConfig({ version: 1, name: 'test', server: `http://127.0.0.1:${vash.port}`, channel: CHANNEL, token: 'vash_test', access: null, ports: portsForSlot(40 + Math.floor(Math.random() * 9)), obsPassword: 'pw', ingestKey: 'key', osuSource: null, tourneyHeight: 720, encoder: 'amd', autostart: false }); }); afterEach(async () => { vash.stop(true); delete process.env.VASH_BROADCAST_HOME; await rm(home, { recursive: true, force: true }); }); test('runs without its programs, reports why, follows Go live, and stops cleanly', async () => { const paths = layout('test'); const desktop = new FakeDesktop(); const done = runWorker('test', desktop); let status: WorkerState | null = null; for (let waited = 0; waited < 80 && !status?.vash.connected; waited++) { await Bun.sleep(100); status = await callWorker(paths.runtime, '/status'); } expect(status?.vash).toMatchObject({ connected: true, channel: 'Main' }); // OBS is not installed here, so the worker says it is (re)starting it rather than claiming success. expect(['starting', 'reconnecting']).toContain(status!.state); expect(status!.tourney.state).not.toBe('spectating'); expect(received.some((message) => message.type === 'status' && message.instance === 'test')).toBe( true ); expect(existsSync(paths.lock)).toBe(true); live = true; for (let waited = 0; waited < 80 && !posts.length; waited++) await Bun.sleep(100); expect(posts[0]).toEqual({ event: 'start' }); expect(await callWorker<{ ok: boolean }>(paths.runtime, '/stop', { method: 'POST' })).toEqual({ ok: true }); expect(await done).toBe(0); expect(posts.at(-1)).toMatchObject({ event: 'end', sessionId: 'session-1' }); expect(existsSync(paths.runtime)).toBe(false); expect(existsSync(paths.lock)).toBe(false); expect((await readJson(paths.state))?.state).toBe('stopped'); expect( received.some((message) => message.type === 'status' && message.state === 'stopping') ).toBe(true); }, 30_000);