import { describe, it, expect, beforeEach, vi } from "vitest"; import { World, defineComponent, type Entity } from "../src/index"; import { CommandQueue } from "../src/commands/command-queue"; // ── Components ────────────────────────────────────── const Health = defineComponent("health", { current: 100, max: 100 }); const DamageCmd = defineComponent("damageCmd", { amount: 0 }); const HealCmd = defineComponent("healCmd", { amount: 0 }); // ── Helpers ───────────────────────────────────────── async function settled() { await new Promise((r) => setTimeout(r, 0)); } // ── Basic command dispatch ────────────────────────── describe("CommandQueue", () => { let world: World; let queue: CommandQueue; beforeEach(() => { world = new World(); queue = new CommandQueue(world); }); it("dispatches commands to registered handlers", () => { const results: { entity: Entity; amount: number }[] = []; queue.handle(DamageCmd, (cmd, entity) => { results.push({ entity: entity!, amount: cmd.amount }); }); const e = world.spawn(); world.add(e, DamageCmd, { amount: 25 }); queue.execute(); expect(results).toHaveLength(1); expect(results[0].amount).toBe(25); }); it("removes the command component after dispatch", () => { queue.handle(DamageCmd, () => {}); const e = world.spawn(); world.add(e, DamageCmd, { amount: 10 }); expect(world.has(e, DamageCmd)).toBe(true); queue.execute(); expect(world.has(e, DamageCmd)).toBe(false); }); it("destroys an entity when it becomes empty after command removal", () => { queue.handle(DamageCmd, () => {}); const e = world.spawn(); world.add(e, DamageCmd, { amount: 10 }); queue.execute(); expect(world.isAlive(e)).toBe(false); }); it("does NOT destroy entities that still have other components", () => { queue.handle(DamageCmd, () => {}); const e = world.spawn(); world.add(e, Health); world.add(e, DamageCmd, { amount: 10 }); queue.execute(); expect(world.isAlive(e)).toBe(true); expect(world.has(e, Health)).toBe(true); expect(world.has(e, DamageCmd)).toBe(false); }); it("handles multiple entities with the same command type", () => { const hits: Entity[] = []; queue.handle(DamageCmd, (_, entity) => { hits.push(entity!); }); const a = world.spawn(); const b = world.spawn(); const c = world.spawn(); world.add(a, DamageCmd, { amount: 5 }); world.add(b, DamageCmd, { amount: 10 }); world.add(c, DamageCmd, { amount: 15 }); queue.execute(); expect(hits).toHaveLength(3); }); it("handles multiple registered command types", () => { const damages: number[] = []; const heals: number[] = []; queue .handle(DamageCmd, (cmd) => damages.push(cmd.amount)) .handle(HealCmd, (cmd) => heals.push(cmd.amount)); const a = world.spawn(); const b = world.spawn(); world.add(a, DamageCmd, { amount: 5 }); world.add(b, HealCmd, { amount: 20 }); queue.execute(); expect(damages).toEqual([5]); expect(heals).toEqual([20]); expect(world.has(a, DamageCmd)).toBe(false); expect(world.has(b, HealCmd)).toBe(false); }); it("is a no-op when no command entities exist", () => { queue.handle(DamageCmd, () => { throw new Error("should not be called"); }); // No entities with DamageCmd expect(() => queue.execute()).not.toThrow(); }); it("is a no-op when no handlers are registered", () => { const e = world.spawn(); world.add(e, DamageCmd, { amount: 10 }); expect(() => queue.execute()).not.toThrow(); expect(world.has(e, DamageCmd)).toBe(true); // not consumed }); }); // ── Interruption ──────────────────────────────────── describe("CommandQueue interruption", () => { let world: World; let queue: CommandQueue; beforeEach(() => { world = new World(); queue = new CommandQueue(world); }); it("skips processing when interrupted", () => { const handler = vi.fn(); queue.handle(DamageCmd, handler); const e = world.spawn(); world.add(e, DamageCmd, { amount: 10 }); // Interrupt with a never-resolving promise queue.interrupt(new Promise(() => {})); expect(queue.isInterrupted).toBe(true); queue.execute(); expect(handler).not.toHaveBeenCalled(); expect(world.has(e, DamageCmd)).toBe(true); }); it("resumes processing after interruption promise resolves", async () => { const handler = vi.fn(); queue.handle(DamageCmd, handler); let resolve!: () => void; const promise = new Promise((r) => { resolve = r; }); queue.interrupt(promise); expect(queue.isInterrupted).toBe(true); queue.execute(); expect(handler).not.toHaveBeenCalled(); resolve(); await settled(); expect(queue.isInterrupted).toBe(false); const e = world.spawn(); world.add(e, DamageCmd, { amount: 10 }); queue.execute(); expect(handler).toHaveBeenCalledTimes(1); }); it("resumes after all interruption promises settle (including rejections)", async () => { const handler = vi.fn(); queue.handle(DamageCmd, handler); queue.interrupt(Promise.reject(new Error("fail"))); await settled(); expect(queue.isInterrupted).toBe(false); const e = world.spawn(); world.add(e, DamageCmd, { amount: 10 }); queue.execute(); expect(handler).toHaveBeenCalledTimes(1); }); it("stays interrupted while at least one of several promises is pending", async () => { let resolveA!: () => void; const a = new Promise((r) => { resolveA = r; }); const b = Promise.resolve(); queue.interrupt(a); queue.interrupt(b); await settled(); // b resolves expect(queue.isInterrupted).toBe(true); // a still pending resolveA(); await settled(); expect(queue.isInterrupted).toBe(false); }); }); // ── Edge cases ────────────────────────────────────── describe("CommandQueue edge cases", () => { it("handler can safely add components to surviving entity", () => { const world = new World(); const queue = new CommandQueue(world); const Flag = defineComponent("flag", { set: false }); queue.handle(DamageCmd, (_cmd, entity) => { world.add(entity!, Flag, { set: true }); }); const e = world.spawn(); world.add(e, Health); world.add(e, DamageCmd, { amount: 10 }); queue.execute(); expect(world.has(e, Flag)).toBe(true); expect(world.isAlive(e)).toBe(true); }); it("handler can destroy a different entity", () => { const world = new World(); const queue = new CommandQueue(world); const other = world.spawn(); queue.handle(DamageCmd, () => { world.destroy(other); }); const e = world.spawn(); world.add(e, DamageCmd, { amount: 10 }); queue.execute(); expect(world.isAlive(other)).toBe(false); }); it("chainable .handle() calls", () => { const world = new World(); const queue = new CommandQueue(world); const a = vi.fn(); const b = vi.fn(); queue .handle(DamageCmd, (_cmd, _entity) => a()) .handle(HealCmd, (_cmd, _entity) => b()); const e1 = world.spawn(); world.add(e1, DamageCmd, { amount: 5 }); const e2 = world.spawn(); world.add(e2, HealCmd, { amount: 10 }); queue.execute(); expect(a).toHaveBeenCalled(); expect(b).toHaveBeenCalled(); }); });