tests: consolidate into tests/ dirs, fix import paths

Move all test files out of source dirs into per-workspace tests/:
- shared/tests/   (3 unit test files)
- server/tests/   (1 integration test)
- src/tests/      (8 characterization + scenario tests + testHelpers)

Fix all relative import paths (App, storage, __mocks__, testHelpers).
Fix jest.config testMatch globs in shared/ and server/ (rootDir +
<rootDir>/tests pattern).

Delete scripts/repro-pause-bug.js (debug scratch, superseded by
turn.pause-add.test.js).

Keep scripts/replay-combat.js + scripts/audit-rotation.js as manual
demo/exploratory tools (NOT unit tests, not deterministic).

No logic changes. All green: shared 49 + 1 validated RED, server 23,
FE 62. Scenario test unchanged (240s timeout, pre-existing slow).
This commit is contained in:
david raistrick
2026-06-29 16:02:22 -04:00
parent 33e0e52789
commit f81308a0df
16 changed files with 26 additions and 100 deletions
+100
View File
@@ -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);
});
});