Watch party: guests stream instead of waiting on a device copy, one load per song, host seat and guest seats can't be taken over, host reload restores the song, real taps only detach, reconnect after background, keep invite code and chat draft

This commit is contained in:
Jonathan Sykes
2026-10-15 19:12:10 +08:00
parent 63cea89b68
commit 32d1f6891b
5 changed files with 127 additions and 23 deletions

View File

@@ -126,3 +126,45 @@ test('DJ suggestions and votes are broadcast, host consumes approved songs', asy
h.send({ type: 'dj', action: 'take' }); expect((await h.next('dj-play')).video.id).toBe('0gfX0dFLaBc'); expect((await g.next('dj')).entries).toEqual([]);
h.ws.close(); g.ws.close();
});
describe('watch party seat rules', () => {
test('unknown code closes with 4004 (no reconnect loop)', async () => {
const bad = client({ role: 'guest', pid: 'guestpid7777', name: 'X', code: 'YYYYYY' });
expect(await bad.closed).toBe(4004);
});
test('a guest cannot take the host seat or another guest seat; the same device can', async () => {
const host = client({ role: 'host', pid: 'hostpid0002', name: 'H' });
const { code } = await host.next('hello');
const fake = client({ role: 'guest', pid: 'hostpid0002', name: 'Evil', code });
expect(await fake.closed).toBe(4003);
const g = client({ role: 'guest', pid: 'guestpid0003', key: 'k'.repeat(36), name: 'G', code });
await g.next('hello');
const thief = client({ role: 'guest', pid: 'guestpid0003', key: 'x'.repeat(36), name: 'T', code });
expect(await thief.closed).toBe(4005);
const again = client({ role: 'guest', pid: 'guestpid0003', key: 'k'.repeat(36), name: 'G', code });
await again.next('hello');
expect(await g.closed).toBe(4000);
host.send({ type: 'end' }); await host.closed;
});
test('a host resuming a room the server lost is told so', async () => {
const host = client({ role: 'host', pid: 'hostpid0004', name: 'H', code: 'QQQQQQ', secret: 'whatever-secret' });
const hello = await host.next('hello');
expect(hello.lost).toBe(true);
expect(hello.code).not.toBe('QQQQQQ');
host.send({ type: 'end' }); await host.closed;
});
test('voice presence floods are dropped', async () => {
const host = client({ role: 'host', pid: 'hostpid0005', name: 'H' });
const { code } = await host.next('hello');
const g = client({ role: 'guest', pid: 'guestpid0006', key: 'k'.repeat(36), name: 'G', code });
await g.next('hello'); host.drain('members');
for (let i = 0; i < 400; i++) g.send({ type: 'voice', on: i % 2 === 0 });
await Bun.sleep(300);
let n = 0; try { for (;;) { await host.next('members', 50); n++; } } catch { /* drained */ }
expect(n).toBeLessThanOrEqual(41);
host.send({ type: 'end' }); await host.closed;
});
});