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:
@@ -0,0 +1,175 @@
|
||||
// Regression test: full round must rotate through ALL active participants exactly once.
|
||||
// Audit of 100-round replay found 124 skips + 78 dupes (round 1 already missing Fighter
|
||||
// before any coverage action). nextTurn has core bug, not just coverage-path issue.
|
||||
//
|
||||
// This test is RED until nextTurn fixed.
|
||||
|
||||
const shared = require('@ttrpg/shared');
|
||||
const { startEncounter, nextTurn, makeParticipant } = shared;
|
||||
|
||||
function p(id, initiative, extra = {}) {
|
||||
return makeParticipant({
|
||||
id, name: id, type: 'monster',
|
||||
initiative, maxHp: 20, currentHp: 20,
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
function enc(ps) {
|
||||
return {
|
||||
name: 'T', participants: ps,
|
||||
isStarted: false, isPaused: false,
|
||||
round: 0, currentTurnParticipantId: null, turnOrderIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe('round rotation integrity', () => {
|
||||
test('3 participants: one full round visits each exactly once', () => {
|
||||
const ps = [p('a', 20), p('b', 15), p('c', 10)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
|
||||
const startOrder = e.turnOrderIds.slice();
|
||||
const visited = [e.currentTurnParticipantId];
|
||||
|
||||
// advance (len-1) turns: visits remaining participants, round NOT yet wrapped.
|
||||
for (let i = 0; i < startOrder.length - 1; i++) {
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
visited.push(e.currentTurnParticipantId);
|
||||
}
|
||||
|
||||
expect(e.round).toBe(1); // still round 1
|
||||
const uniq = new Set(visited);
|
||||
expect(uniq.size).toBe(startOrder.length); // each exactly once
|
||||
expect(visited.length).toBe(startOrder.length);
|
||||
});
|
||||
|
||||
test('8 participants (replay shape): one full round visits each exactly once', () => {
|
||||
const ps = [
|
||||
p('Goblin1', 12), p('Wolf', 13), p('Merchant', 8), p('OrcBoss', 11),
|
||||
p('Goblin2', 12), p('Fighter', 14), p('Rogue', 15), p('Cleric', 10),
|
||||
];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
|
||||
const startOrder = e.turnOrderIds.slice();
|
||||
const visited = [e.currentTurnParticipantId];
|
||||
for (let i = 0; i < startOrder.length - 1; i++) {
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
visited.push(e.currentTurnParticipantId);
|
||||
}
|
||||
|
||||
expect(e.round).toBe(1);
|
||||
const uniq = new Set(visited);
|
||||
expect(uniq.size).toBe(startOrder.length);
|
||||
expect(visited.length).toBe(startOrder.length);
|
||||
});
|
||||
|
||||
test('multiple rounds: each round visits each participant exactly once', () => {
|
||||
const ps = [p('a', 20), p('b', 15), p('c', 10), p('d', 5)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
|
||||
const startOrder = e.turnOrderIds.slice();
|
||||
const expectedRound = e.round;
|
||||
|
||||
// capture exactly one full round (current + len-1 advances), no wrap yet.
|
||||
const visited = [e.currentTurnParticipantId];
|
||||
for (let i = 0; i < startOrder.length - 1; i++) {
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
visited.push(e.currentTurnParticipantId);
|
||||
}
|
||||
const uniq = new Set(visited);
|
||||
expect(uniq.size).toBe(startOrder.length);
|
||||
expect(e.round).toBe(expectedRound);
|
||||
});
|
||||
});
|
||||
|
||||
describe('round rotation with mid-round state changes', () => {
|
||||
const { toggleParticipantActive, addParticipant, removeParticipant, reorderParticipants, applyHpChange } = shared;
|
||||
|
||||
test('toggle a participant inactive mid-round, others still each visited once', () => {
|
||||
const ps = [p('a', 20), p('b', 15), p('c', 10), p('d', 5)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
const startOrder = e.turnOrderIds.slice();
|
||||
|
||||
const visited = [e.currentTurnParticipantId];
|
||||
e = { ...e, ...nextTurn(e).patch }; visited.push(e.currentTurnParticipantId);
|
||||
// now mark 'a' inactive (already took its turn)
|
||||
e = { ...e, ...toggleParticipantActive(e, 'a').patch };
|
||||
e = { ...e, ...nextTurn(e).patch }; visited.push(e.currentTurnParticipantId);
|
||||
e = { ...e, ...nextTurn(e).patch }; visited.push(e.currentTurnParticipantId);
|
||||
// round should wrap, but 'a' inactive so only b,c,d visited
|
||||
const visitedActive = visited.filter(id => id !== 'a');
|
||||
const uniq = new Set(visitedActive);
|
||||
expect(uniq.size).toBe(startOrder.length - 1); // b,c,d each once
|
||||
});
|
||||
|
||||
test('reactivate inactive participant mid-round, it gets a turn this round', () => {
|
||||
const ps = [p('a', 20), p('b', 15), p('c', 10), p('d', 5)];
|
||||
let e = enc(ps);
|
||||
// start with 'c' inactive
|
||||
e.participants = e.participants.map(p => p.id === 'c' ? { ...p, isActive: false } : p);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
const startOrder = e.turnOrderIds.slice(); // should be a,b,d (c excluded)
|
||||
|
||||
expect(startOrder).not.toContain('c');
|
||||
|
||||
// advance one turn, then reactivate c
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
e = { ...e, ...toggleParticipantActive(e, 'c').patch };
|
||||
|
||||
// continue rotation - c should now be reachable
|
||||
const visited = [startOrder[0], e.currentTurnParticipantId];
|
||||
for (let i = 0; i < startOrder.length; i++) {
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
visited.push(e.currentTurnParticipantId);
|
||||
}
|
||||
expect(visited).toContain('c');
|
||||
});
|
||||
|
||||
test('addParticipant mid-round: new participant gets turn this round or next', () => {
|
||||
const ps = [p('a', 20), p('b', 15), p('c', 10)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
const startOrder = e.turnOrderIds.slice();
|
||||
|
||||
e = { ...e, ...nextTurn(e).patch }; // advance one
|
||||
// add new participant
|
||||
const newP = p('x', 25);
|
||||
e = { ...e, ...addParticipant(e, newP).patch };
|
||||
|
||||
// finish round - original 3 should still each get exactly one turn
|
||||
const visited = [startOrder[0], e.currentTurnParticipantId];
|
||||
while (e.round === 1) {
|
||||
const r = nextTurn(e);
|
||||
e = { ...e, ...r.patch };
|
||||
visited.push(e.currentTurnParticipantId);
|
||||
if (visited.length > 20) break; // safety
|
||||
}
|
||||
const originals = visited.filter(id => ['a','b','c'].includes(id));
|
||||
const uniq = new Set(originals);
|
||||
expect(uniq.size).toBe(3);
|
||||
});
|
||||
|
||||
test('reorderParticipants mid-round keeps rotation valid', () => {
|
||||
const ps = [p('a', 20), p('b', 15), p('c', 15), p('d', 5)]; // b,c same init (15)
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
const startOrder = e.turnOrderIds.slice();
|
||||
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
// reorder: swap b,c (same initiative)
|
||||
e = { ...e, ...reorderParticipants(e, 'b', 'c').patch };
|
||||
|
||||
const visited = [startOrder[0], e.currentTurnParticipantId];
|
||||
for (let i = 0; i < startOrder.length; i++) {
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
visited.push(e.currentTurnParticipantId);
|
||||
}
|
||||
const uniq = new Set(visited);
|
||||
expect(uniq.size).toBeGreaterThanOrEqual(startOrder.length);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user