tests: pause-add rotation corruption + dup-id, log bugs to TODO
- turn.pause-add.test.js: 3 tests isolating addParticipant+pause/resume interaction. Clean minimal repro passes (bug needs more state than single add+pause). Audit authoritative repro. - turn.characterization.test.js: RED 'addParticipant rejects duplicate id'. Validates current allow-dup behavior. - TODO.md: BUG-1 (add+pause rotation corruption, 32/100 audit violations), BUG-2 (dup id allow). Both confirmed real, NOT fixed. Audit bisect: dmg+heal+cond+toggle+remove+add+pause = 32 violations. add+pause alone = 0. Combo needs full state. No feature code changed.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user