P14a: global session sidebar + roster + private-channel backend
- Session sidebar (header "☰ Session" toggle, mobile drawer) visible to GM AND players: which session you're in, who's here (GM + players), and the live roll feed. The GM's own public rolls now mirror into the local feed too. - Server roster broadcast to everyone (sendRoster on join/seatGrant/disconnect/ host-resume); includes the GM. Players now see the roster, not just the GM. - Targeted private channel (backend, wired to UI next): privateState (GM→server→ one player only) + privateHandout; image sent INLINE so non-recipients never get it. roster/privateHandout schemas + stores + wsSync handlers + senders. - Adversarial-review fixes: GM included in roster; roster refreshes on GM disconnect; privateState returns a forbidden error on auth failure. +3 server tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -127,4 +127,34 @@ describe('RoomHub', () => {
|
||||
hub.playerPatch(player, 'someone-else', { hp: { current: 0, max: 24, temp: 0 } });
|
||||
expect(gm.msgs.filter((m) => m.t === 'playerPatched').length).toBe(before);
|
||||
});
|
||||
|
||||
it('broadcasts a roster (GM + players) to everyone', () => {
|
||||
const hub = new RoomHub();
|
||||
const { gm, player } = hostAndJoin(hub);
|
||||
const roster = lastOf(gm, 'roster');
|
||||
expect(roster?.players.map((p) => p.name)).toContain('GM');
|
||||
expect(roster?.players).toHaveLength(2); // GM + 1 player
|
||||
expect(lastOf(player, 'roster')?.players).toHaveLength(2); // players see the roster too
|
||||
});
|
||||
|
||||
it('routes a private handout to the target player ONLY', () => {
|
||||
const hub = new RoomHub();
|
||||
const gm = fake(); hub.host(gm);
|
||||
const hosted = lastOf(gm, 'hosted') as Extract<ServerMessage, { t: 'hosted' }>;
|
||||
const p1 = fake(); hub.join(p1, hosted.joinCode);
|
||||
const p2 = fake(); hub.join(p2, hosted.joinCode);
|
||||
const roster = lastOf(gm, 'roster')!;
|
||||
const targetId = roster.players.find((p) => p.playerId !== 'gm')!.playerId; // first player (insertion order)
|
||||
|
||||
hub.privateState(gm, hosted.gmSecret, targetId, { title: 'Secret', body: 'just for you' });
|
||||
expect(lastOf(p1, 'privateHandout')).toMatchObject({ handout: { title: 'Secret' } });
|
||||
expect(lastOf(p2, 'privateHandout')).toBeUndefined(); // a non-recipient never receives it
|
||||
});
|
||||
|
||||
it('rejects privateState from a non-GM', () => {
|
||||
const hub = new RoomHub();
|
||||
const { player } = hostAndJoin(hub);
|
||||
hub.privateState(player, 'not-the-secret', 'gm', { title: 'x', body: 'y' });
|
||||
expect(lastOf(player, 'error')?.code).toBe('forbidden');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user