Files
dotcom-arcade/streaming/session-manager/test/sessions.test.ts
T
branderson 9144395201 feat(streaming): log session lifecycles and why streamers fail
The session manager logs each session's creation, readiness time and
removal with its reason. A streamer that exits during startup now fails
the start at once instead of after 60 seconds, and the log carries its
exit code and last output; containers are no longer auto-removed, so
that output survives until teardown. A failed start reaches the app as
a 502.
2026-10-14 00:52:36 -07:00

85 lines
3.7 KiB
TypeScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { SessionError, SessionManager } from '../src/sessions.ts';
import { catalog, FakeDriver, fastTimings, MemoryLog, sleep } from './fakes.ts';
test('rejects unstreamable games and enforces the session cap', async () => {
const sessions = new SessionManager(new FakeDriver(), catalog, 1, fastTimings);
await assert.rejects(sessions.create('flash'), (e: SessionError) => e.status === 422);
const { id, size } = await sessions.create('java');
assert.deepEqual(size, { width: 472, height: 354 });
await assert.rejects(sessions.create('java'), (e: SessionError) => e.status === 503);
await sessions.teardown(id);
await sessions.create('java');
await sessions.teardownAll();
});
test('removes a session that is never connected to', async () => {
const driver = new FakeDriver();
const sessions = new SessionManager(driver, catalog, 4, fastTimings);
const { id } = await sessions.create('java');
await sessions.start(id, {});
await sleep(400);
assert.equal(sessions.has(id), false);
assert.deepEqual(driver.removed, [id]);
});
test('keeps a session while connected and survives a quick reconnect', async () => {
const driver = new FakeDriver();
const sessions = new SessionManager(driver, catalog, 4, fastTimings);
const { id } = await sessions.create('java');
await sessions.start(id, {});
sessions.socketOpened(id);
await sleep(400);
assert.equal(sessions.has(id), true);
sessions.socketClosed(id);
await sleep(50);
sessions.socketOpened(id);
await sleep(200);
assert.equal(sessions.has(id), true);
sessions.socketClosed(id);
await sleep(200);
assert.equal(sessions.has(id), false);
});
test('starts with the given saves, pushes changes, and ends with a final push', async () => {
const driver = new FakeDriver();
const sessions = new SessionManager(driver, catalog, 4, fastTimings);
const { id } = await sessions.create('java');
await sessions.start(id, { 'cookies.txt': 'YQ==' });
await assert.rejects(sessions.start(id, {}), (e: SessionError) => e.status === 409);
assert.deepEqual(driver.containers.get(id)!.saves, { 'cookies.txt': 'YQ==' });
const seen: unknown[] = [];
sessions.watchSaves(id, (files) => seen.push(files));
driver.containers.get(id)!.saves['cookies.txt'] = 'Yg==';
await sleep(60);
driver.containers.get(id)!.saves['cookies.txt'] = 'Yw==';
await sessions.teardown(id);
assert.deepEqual(seen, [{ 'cookies.txt': 'YQ==' }, { 'cookies.txt': 'Yg==' }, { 'cookies.txt': 'Yw==' }, null]);
});
test('logs a session from creation to removal', async () => {
const log = new MemoryLog();
const sessions = new SessionManager(new FakeDriver(), catalog, 4, fastTimings, log);
const { id } = await sessions.create('java');
await sessions.start(id, {});
await sessions.teardown(id, 'player left');
assert.equal(log.lines.length, 3);
assert.match(log.lines[0], new RegExp(`Session ${id}: created for game java at 472x354`));
assert.match(log.lines[1], /ready in \d+\.\ds/);
assert.match(log.lines[2], /removing \(player left\)/);
});
test('reports a streamer that fails to start, and removes it', async () => {
const driver = new FakeDriver();
driver.failStart = new Error('streamer exited with code 1; last output:\nno display');
const log = new MemoryLog();
const sessions = new SessionManager(driver, catalog, 4, fastTimings, log);
const { id } = await sessions.create('java');
await assert.rejects(sessions.start(id, {}), (e: SessionError) => e.status === 502);
assert.equal(sessions.has(id), false);
assert.deepEqual(driver.removed, [id]);
assert.ok(log.lines.some((line) => line.includes('failed to start: streamer exited with code 1') && line.includes('no display')));
});