Files
esports/streamer/windows/worker.test.ts
Stan Runge 54444a77f0 feat: add native Windows broadcaster and live score modules
- streamer/windows: vash-broadcast.exe (setup/login/start/stop/status/doctor)
  with per-instance OBS, tosu and FFmpeg, a supervised worker, OCR room
  selection and OBS scene reconciliation; built and tested on Linux only
- streamer/relay.ts shared with Vash TV; Cloudflare Access proxy for dev/staging
- worker status validation and live score helpers for the site
- STREAMING_HANDOFF.md and streamer/ALPHA_TEST.md record what is unverified

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:02:25 +00:00

132 lines
4.2 KiB
TypeScript

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<undefined>;
const received: Record<string, unknown>[] = [];
const posts: Record<string, unknown>[] = [];
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<string, unknown>;
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<WorkerState>(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<WorkerState>(paths.state))?.state).toBe('stopped');
expect(
received.some((message) => message.type === 'status' && message.state === 'stopping')
).toBe(true);
}, 30_000);