// Characterization test: addParticipant + pause/resume corrupts turn rotation. // Audit found 56-77 violations/100 rounds starting round 20 in pure turn.js // simulation. Visible in live replay (round 10: 17 turns, 6 duped actors, // R-series stuck repeating forever). // // This test uses FRESH ids (crypto.randomUUID equivalent) — NOT the audit's // self-inflicted dup (loop spun while paused, re-added same `r${totalTurns}`). // Validates real bug reachable via normal UI flow (DM adds monster while paused, // resumes). const shared = require('@ttrpg/shared'); const { startEncounter, nextTurn, togglePause, addParticipant, makeParticipant } = shared; function p(id, initiative, extra = {}) { return makeParticipant({ id, name: id, type: 'monster', initiative, maxHp: 100, currentHp: 100, ...extra, }); } function enc(ps) { return { name: 'T', participants: ps, isStarted: false, isPaused: false, round: 0, currentTurnParticipantId: null, turnOrderIds: [], }; } describe('addParticipant + pause/resume rotation corruption', () => { test('add fresh participant while paused, resume, rotation completes full cycle', () => { const ps = [p('a', 20), p('b', 15), p('c', 10)]; let e = enc(ps); e = { ...e, ...startEncounter(e).patch }; const baseOrder = e.turnOrderIds.slice(); // [a,b,c] e = { ...e, ...nextTurn(e).patch }; // current=b e = { ...e, ...togglePause(e).patch }; // pause // add fresh participant x (initiative 25, would sort first) const x = p('x', 25); e = { ...e, ...addParticipant(e, x).patch }; e = { ...e, ...togglePause(e).patch }; // resume (rebuilds order) // after resume, complete one full round: visit each active participant once const visited = [e.currentTurnParticipantId]; for (let i = 0; i < e.turnOrderIds.length - 1; i++) { e = { ...e, ...nextTurn(e).patch }; visited.push(e.currentTurnParticipantId); } const uniq = new Set(visited); // EXPECT: 4 unique (a,b,c,x). BUG: rotation may not visit all. expect(uniq.size).toBe(e.turnOrderIds.length); }); test('multiple adds while paused, resume, rotation visits all', () => { const ps = [p('a', 20), p('b', 15), p('c', 10)]; let e = enc(ps); e = { ...e, ...startEncounter(e).patch }; e = { ...e, ...nextTurn(e).patch }; // current=b e = { ...e, ...togglePause(e).patch }; // pause // add 3 fresh participants for (const id of ['x', 'y', 'z']) { const np = p(id, 5 + Math.floor(Math.random() * 30)); e = { ...e, ...addParticipant(e, np).patch }; } e = { ...e, ...togglePause(e).patch }; // resume const visited = [e.currentTurnParticipantId]; for (let i = 0; i < e.turnOrderIds.length + 2; i++) { e = { ...e, ...nextTurn(e).patch }; visited.push(e.currentTurnParticipantId); } const uniq = new Set(visited); // EXPECT: all 6 participants reachable. BUG: some stuck/repeated. expect(uniq.size).toBe(e.turnOrderIds.length); }); test('add while running, then pause+resume, rotation stays valid', () => { const ps = [p('a', 20), p('b', 15), p('c', 10)]; let e = enc(ps); e = { ...e, ...startEncounter(e).patch }; e = { ...e, ...nextTurn(e).patch }; // current=b const x = p('x', 25); e = { ...e, ...addParticipant(e, x).patch }; // add while running e = { ...e, ...togglePause(e).patch }; // pause e = { ...e, ...togglePause(e).patch }; // resume const visited = [e.currentTurnParticipantId]; for (let i = 0; i < e.turnOrderIds.length + 2; i++) { e = { ...e, ...nextTurn(e).patch }; visited.push(e.currentTurnParticipantId); } const uniq = new Set(visited); expect(uniq.size).toBe(e.turnOrderIds.length); }); });