Files
initiative/packages/domain/src/__tests__/advance-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

232 lines
6.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { advanceTurn, isDomainError } from "../advance-turn.js";
import type { DomainEvent } from "../events.js";
import {
type Combatant,
combatantId,
createEncounter,
type Encounter,
} 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 = advanceTurn(enc);
if (isDomainError(result)) {
throw new Error(`Expected success, got error: ${result.message}`);
}
return result;
}
// --- Acceptance Scenarios ---
describe("advanceTurn", () => {
describe("acceptance scenarios", () => {
it("scenario 1: advances from first to second combatant", () => {
const enc = encounter([A, B, C], 0, 1);
const { encounter: next, events } = successResult(enc);
expect(next.activeIndex).toBe(1);
expect(next.roundNumber).toBe(1);
expect(events).toEqual([
{
type: "TurnAdvanced",
previousCombatantId: combatantId("A"),
newCombatantId: combatantId("B"),
roundNumber: 1,
},
]);
});
it("scenario 2: advances from second to third combatant", () => {
const enc = encounter([A, B, C], 1, 1);
const { encounter: next, events } = successResult(enc);
expect(next.activeIndex).toBe(2);
expect(next.roundNumber).toBe(1);
expect(events).toEqual([
{
type: "TurnAdvanced",
previousCombatantId: combatantId("B"),
newCombatantId: combatantId("C"),
roundNumber: 1,
},
]);
});
it("scenario 3: wraps from last combatant to first, increments round", () => {
const enc = encounter([A, B, C], 2, 1);
const { encounter: next, events } = successResult(enc);
expect(next.activeIndex).toBe(0);
expect(next.roundNumber).toBe(2);
expect(events).toEqual([
{
type: "TurnAdvanced",
previousCombatantId: combatantId("C"),
newCombatantId: combatantId("A"),
roundNumber: 2,
},
{
type: "RoundAdvanced",
newRoundNumber: 2,
},
]);
});
it("scenario 4: wraps at round 5 to round 6 (not hardcoded)", () => {
const enc = encounter([A, B, C], 2, 5);
const { encounter: next, events } = successResult(enc);
expect(next.activeIndex).toBe(0);
expect(next.roundNumber).toBe(6);
expect(events[0]).toMatchObject({
type: "TurnAdvanced",
roundNumber: 6,
});
expect(events[1]).toEqual({
type: "RoundAdvanced",
newRoundNumber: 6,
});
});
it("scenario 5: single combatant always wraps", () => {
const enc = encounter([A], 0, 1);
const { encounter: next, events } = successResult(enc);
expect(next.activeIndex).toBe(0);
expect(next.roundNumber).toBe(2);
expect(events).toEqual([
{
type: "TurnAdvanced",
previousCombatantId: combatantId("A"),
newCombatantId: combatantId("A"),
roundNumber: 2,
},
{
type: "RoundAdvanced",
newRoundNumber: 2,
},
]);
});
it("scenario 6: two advances on a 2-combatant encounter completes a round", () => {
const enc = encounter([A, B], 0, 1);
const first = successResult(enc);
expect(first.encounter.activeIndex).toBe(1);
expect(first.encounter.roundNumber).toBe(1);
const second = successResult(first.encounter);
expect(second.encounter.activeIndex).toBe(0);
expect(second.encounter.roundNumber).toBe(2);
});
it("scenario 7: empty combatant list returns error", () => {
const enc: Encounter = {
combatants: [],
activeIndex: 0,
roundNumber: 1,
};
const result = advanceTurn(enc);
expectDomainError(result, "invalid-encounter");
});
it("scenario 8: three advances on [A,B,C] completes a full round cycle", () => {
let enc = encounter([A, B, C], 0, 1);
enc = successResult(enc).encounter;
enc = successResult(enc).encounter;
enc = successResult(enc).encounter;
expect(enc.activeIndex).toBe(0);
expect(enc.roundNumber).toBe(2);
});
});
describe("invariants", () => {
it("INV-1: createEncounter accepts empty combatant list", () => {
const result = createEncounter([]);
expect(isDomainError(result)).toBe(false);
});
it("INV-2: activeIndex always in bounds across all scenarios", () => {
const combatants = [A, B, C];
let enc = encounter(combatants, 0, 1);
for (let i = 0; i < 10; i++) {
const result = successResult(enc);
expect(result.encounter.activeIndex).toBeGreaterThanOrEqual(0);
expect(result.encounter.activeIndex).toBeLessThan(combatants.length);
enc = result.encounter;
}
});
it("INV-3: roundNumber never decreases", () => {
let enc = encounter([A, B, C], 0, 1);
let prevRound = enc.roundNumber;
for (let i = 0; i < 10; i++) {
const result = successResult(enc);
expect(result.encounter.roundNumber).toBeGreaterThanOrEqual(prevRound);
prevRound = result.encounter.roundNumber;
enc = result.encounter;
}
});
it("INV-4: determinism — same input produces same output", () => {
const enc = encounter([A, B, C], 1, 3);
const result1 = advanceTurn(enc);
const result2 = advanceTurn(enc);
expect(result1).toEqual(result2);
});
it("INV-5: every success emits at least TurnAdvanced", () => {
const scenarios: Encounter[] = [
encounter([A, B, C], 0, 1),
encounter([A, B, C], 2, 1),
encounter([A], 0, 1),
];
for (const enc of scenarios) {
const result = successResult(enc);
const hasTurnAdvanced = result.events.some(
(e: DomainEvent) => e.type === "TurnAdvanced",
);
expect(hasTurnAdvanced).toBe(true);
}
});
it("event ordering: on wrap, events are [TurnAdvanced, RoundAdvanced]", () => {
const enc = encounter([A, B, C], 2, 1);
const { events } = successResult(enc);
expect(events).toHaveLength(2);
expect(events[0].type).toBe("TurnAdvanced");
expect(events[1].type).toBe("RoundAdvanced");
});
});
});