Files
initiative/packages/domain/src/__tests__/retreat-turn.test.ts
Lukas 36768d3aa1 Upgrade Biome to 2.4.7 and enable 54 additional lint rules
Add rules covering bug prevention (noLeakedRender, noFloatingPromises,
noImportCycles, noReactForwardRef), security (noScriptUrl, noAlert),
performance (noAwaitInLoops, useTopLevelRegex), and code style
(noNestedTernary, useGlobalThis, useNullishCoalescing, useSortedClasses,
plus ~40 more). Fix all violations: extract top-level regex constants,
guard React && renders with boolean coercion, refactor nested ternaries,
replace window with globalThis, sort Tailwind classes, and introduce
expectDomainError test helper to eliminate conditional expects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 14:25:09 +01:00

180 lines
4.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { DomainEvent } from "../events.js";
import { retreatTurn } from "../retreat-turn.js";
import {
type Combatant,
combatantId,
createEncounter,
type Encounter,
isDomainError,
} from "../types.js";
import { expectDomainError } from "./test-helpers.js";
// --- Helpers ---
function makeCombatant(name: string): Combatant {
return { id: combatantId(name), name };
}
const A = makeCombatant("A");
const B = makeCombatant("B");
const C = makeCombatant("C");
function encounter(
combatants: Combatant[],
activeIndex: number,
roundNumber: number,
): Encounter {
const result = createEncounter(combatants, activeIndex, roundNumber);
if (isDomainError(result)) {
throw new Error(`Test setup failed: ${result.message}`);
}
return result;
}
function successResult(enc: Encounter) {
const result = retreatTurn(enc);
if (isDomainError(result)) {
throw new Error(`Expected success, got error: ${result.message}`);
}
return result;
}
// --- Acceptance Scenarios ---
describe("retreatTurn", () => {
describe("acceptance scenarios", () => {
it("scenario 1: mid-round retreat — retreats from second to first combatant", () => {
const enc = encounter([A, B, C], 1, 1);
const { encounter: next, events } = successResult(enc);
expect(next.activeIndex).toBe(0);
expect(next.roundNumber).toBe(1);
expect(events).toEqual([
{
type: "TurnRetreated",
previousCombatantId: combatantId("B"),
newCombatantId: combatantId("A"),
roundNumber: 1,
},
]);
});
it("scenario 2: round-boundary retreat — wraps from first combatant to last, decrements round", () => {
const enc = encounter([A, B, C], 0, 2);
const { encounter: next, events } = successResult(enc);
expect(next.activeIndex).toBe(2);
expect(next.roundNumber).toBe(1);
expect(events).toEqual([
{
type: "TurnRetreated",
previousCombatantId: combatantId("A"),
newCombatantId: combatantId("C"),
roundNumber: 1,
},
{
type: "RoundRetreated",
newRoundNumber: 1,
},
]);
});
it("scenario 3: start-of-encounter error — cannot retreat at round 1 index 0", () => {
const enc = encounter([A, B, C], 0, 1);
const result = retreatTurn(enc);
expectDomainError(result, "no-previous-turn");
});
it("scenario 4: single-combatant retreat — wraps to same combatant, decrements round", () => {
const enc = encounter([A], 0, 2);
const { encounter: next, events } = successResult(enc);
expect(next.activeIndex).toBe(0);
expect(next.roundNumber).toBe(1);
expect(events).toEqual([
{
type: "TurnRetreated",
previousCombatantId: combatantId("A"),
newCombatantId: combatantId("A"),
roundNumber: 1,
},
{
type: "RoundRetreated",
newRoundNumber: 1,
},
]);
});
it("scenario 5: empty-encounter error", () => {
const enc: Encounter = {
combatants: [],
activeIndex: 0,
roundNumber: 1,
};
const result = retreatTurn(enc);
expectDomainError(result, "invalid-encounter");
});
});
describe("invariants", () => {
it("determinism — same input produces same output", () => {
const enc = encounter([A, B, C], 1, 3);
const result1 = retreatTurn(enc);
const result2 = retreatTurn(enc);
expect(result1).toEqual(result2);
});
it("activeIndex always in bounds after retreat", () => {
const combatants = [A, B, C];
// Start at round 4 so we can retreat many times
let enc = encounter(combatants, 2, 4);
for (let i = 0; i < 9; i++) {
const result = successResult(enc);
expect(result.encounter.activeIndex).toBeGreaterThanOrEqual(0);
expect(result.encounter.activeIndex).toBeLessThan(combatants.length);
enc = result.encounter;
}
});
it("roundNumber never goes below 1", () => {
let enc = encounter([A, B, C], 2, 2);
// Retreat through rounds — should stop at round 1 index 0
while (!(enc.roundNumber === 1 && enc.activeIndex === 0)) {
const result = successResult(enc);
expect(result.encounter.roundNumber).toBeGreaterThanOrEqual(1);
enc = result.encounter;
}
});
it("every success emits at least TurnRetreated", () => {
const scenarios: Encounter[] = [
encounter([A, B, C], 1, 1),
encounter([A, B, C], 0, 2),
encounter([A], 0, 2),
];
for (const enc of scenarios) {
const result = successResult(enc);
const hasTurnRetreated = result.events.some(
(e: DomainEvent) => e.type === "TurnRetreated",
);
expect(hasTurnRetreated).toBe(true);
}
});
it("event ordering: on wrap, events are [TurnRetreated, RoundRetreated]", () => {
const enc = encounter([A, B, C], 0, 2);
const { events } = successResult(enc);
expect(events).toHaveLength(2);
expect(events[0].type).toBe("TurnRetreated");
expect(events[1].type).toBe("RoundRetreated");
});
});
});