import { describe, expect, it } from "vitest"; import { removeCombatant } from "../remove-combatant.js"; import type { Combatant, Encounter } from "../types.js"; import { combatantId, isDomainError } from "../types.js"; // --- Helpers --- function makeCombatant(name: string): Combatant { return { id: combatantId(name), name }; } const A = makeCombatant("A"); const B = makeCombatant("B"); const C = makeCombatant("C"); const D = makeCombatant("D"); function enc( combatants: Combatant[], activeIndex = 0, roundNumber = 1, ): Encounter { return { combatants, activeIndex, roundNumber }; } function successResult(encounter: Encounter, id: string) { const result = removeCombatant(encounter, combatantId(id)); if (isDomainError(result)) { throw new Error(`Expected success, got error: ${result.message}`); } return result; } // --- Acceptance Scenarios --- describe("removeCombatant", () => { describe("acceptance scenarios", () => { it("AS-1: remove combatant after active — activeIndex unchanged", () => { // [A*, B, C] remove C → [A*, B], activeIndex stays 0 const e = enc([A, B, C], 0, 2); const { encounter, events } = successResult(e, "C"); expect(encounter.combatants).toEqual([A, B]); expect(encounter.activeIndex).toBe(0); expect(encounter.roundNumber).toBe(2); expect(events).toEqual([ { type: "CombatantRemoved", combatantId: combatantId("C"), name: "C", }, ]); }); it("AS-2: remove combatant before active — activeIndex decrements", () => { // [A, B, C*] remove A → [B, C*], activeIndex 2→1 const e = enc([A, B, C], 2, 3); const { encounter } = successResult(e, "A"); expect(encounter.combatants).toEqual([B, C]); expect(encounter.activeIndex).toBe(1); expect(encounter.roundNumber).toBe(3); }); it("AS-3: remove active combatant mid-list — next slides in", () => { // [A, B*, C, D] remove B → [A, C*, D], activeIndex stays 1 const e = enc([A, B, C, D], 1, 1); const { encounter } = successResult(e, "B"); expect(encounter.combatants).toEqual([A, C, D]); expect(encounter.activeIndex).toBe(1); }); it("AS-4: remove active combatant at end — wraps to 0", () => { // [A, B, C*] remove C → [A, B], activeIndex wraps to 0 const e = enc([A, B, C], 2, 1); const { encounter } = successResult(e, "C"); expect(encounter.combatants).toEqual([A, B]); expect(encounter.activeIndex).toBe(0); }); it("AS-5: remove only combatant — empty list, activeIndex 0", () => { const e = enc([A], 0, 5); const { encounter } = successResult(e, "A"); expect(encounter.combatants).toEqual([]); expect(encounter.activeIndex).toBe(0); expect(encounter.roundNumber).toBe(5); }); it("AS-6: ID not found — returns DomainError", () => { const e = enc([A, B], 0, 1); const result = removeCombatant(e, combatantId("nonexistent")); expect(isDomainError(result)).toBe(true); if (isDomainError(result)) { expect(result.code).toBe("combatant-not-found"); } }); }); describe("invariants", () => { it("event shape includes combatantId and name", () => { const e = enc([A, B], 0, 1); const { events } = successResult(e, "B"); expect(events).toHaveLength(1); expect(events[0]).toEqual({ type: "CombatantRemoved", combatantId: combatantId("B"), name: "B", }); }); it("roundNumber never changes on removal", () => { const e = enc([A, B, C], 1, 7); const { encounter } = successResult(e, "A"); expect(encounter.roundNumber).toBe(7); }); it("determinism — same input produces same output", () => { const e = enc([A, B, C], 1, 3); const result1 = removeCombatant(e, combatantId("B")); const result2 = removeCombatant(e, combatantId("B")); expect(result1).toEqual(result2); }); it("every success emits exactly one CombatantRemoved event", () => { const scenarios: [Encounter, string][] = [ [enc([A]), "A"], [enc([A, B], 1), "A"], [enc([A, B, C], 2, 5), "C"], ]; for (const [e, id] of scenarios) { const { events } = successResult(e, id); expect(events).toHaveLength(1); expect(events[0].type).toBe("CombatantRemoved"); } }); }); });