tests: turn.combat.test.js (deterministic RED for BUG-5), deprecate audits

REAL test audit should have been. jest, seeded RNG, mirrors replay-combat.js
op sequence exactly. Asserts per-round invariants: rotation-dupe, turnOrder
dup-id, currentTurn valid+active, HP bounds.

Result: 13 rotation-dupes / 100 rounds. First at round 4 (Cleric twice).
Deterministic, reproducible every run. BUG-5 locked.

Deprecate tests/audit/*.js: random sim gave false 0-violations while
this exact test reproduces bug. Commented early-return. Kept for reference,
delete later when log analyzer + unit tests cover ground.

TODO: BUG-5 added (mid-round addParticipant/revive corrupts rotation).

Root cause hypothesis: computeTurnOrderAfterAddition appends id to
turnOrderIds end. Round wrap re-sorts by initiative. currentTurn pointer
stale after sort → drifts → nextTurn revisits.

Test RED by design (documents live bug). Pre-push will block on push.
This commit is contained in:
david raistrick
2026-06-30 12:33:56 -04:00
parent d48ecf1460
commit 08c6146cf7
4 changed files with 316 additions and 2 deletions
+28 -1
View File
@@ -1,4 +1,19 @@
// scripts/audit-state.js
// DEPRECATED — DO NOT USE.
// Random simulation gave false 0-violations while replay (exact ops)
// reproduced real bugs. Replay-mirror approach = duplicate work.
// Kept for now in case parts reusable. Will delete once log analyzer
// (scratch/) + unit tests cover the ground.
//
// Prefer: replay-combat.js dumps turnOrderIds per turn, log analyzer
// finds dupes/skips from real run. Unit tests lock confirmed bugs.
//
// To revive: delete this early-return block below.
if (require.main === module) {
console.error('audit-state.js DEPRECATED. See header comment.');
process.exit(0);
}
// === original (below) — exploratory bug-finder, kept for reference ===
// Expanded bug-finder: runs combat through pure turn.js, audits invariant
// checks per round across multiple bug classes (not just rotation).
// NOT a unit test (Math.random, exploratory). Unit tests lock known bugs.
@@ -106,6 +121,18 @@ for (let roundN = 1; roundN <= ROUNDS; roundN++) {
const dead = e.participants.find(p => p.currentHp <= 0);
if (dead) { try { e = { ...e, ...removeParticipant(e, dead.id).patch }; } catch (err) {} }
}
// mid-round revive: DM reactivates a downed participant's turn (mirrors
// replay-combat.js + real play). Triggers same path as revive-between-rounds
// but INSIDE rotation — where BUG-5 lives.
if (totalTurns % 7 === 0 && totalTurns > 0) {
const down = e.participants.find(p => p.currentHp <= 0 || p.isActive === false);
if (down) {
try {
if (down.isActive === false) e = { ...e, ...toggleParticipantActive(e, down.id).patch };
e = { ...e, ...applyHpChange(e, down.id, 'heal', down.maxHp).patch };
} catch (err) {}
}
}
if (totalTurns % 10 === 0 && totalTurns > 0) {
const np = makeParticipant({ id: `r${totalTurns}`, name:`R${totalTurns}`, type:'monster', initiative:9, maxHp:100, currentHp:100 });
try { e = { ...e, ...addParticipant(e, np).patch }; } catch (err) {}