import { describe, expect, it } from "@suaveplan/testing/runner"; import { Aggregate } from "../src/core/aggregate/aggregate.js"; import { createAggregateRepository } from "../src/core/aggregate-repository/aggregate-repository.js"; import { createMemoryEventStore } from "../src/core/event-store/memory-event-store.js"; import { createProjection } from "../src/projection/builder/builder.js"; import type { DomainEvent } from "../src/types/event-sourcing.types.js"; interface AccountState { balance: number; open: boolean; } class BankAccount extends Aggregate { public constructor(id: string) { super(id, { balance: 0, open: true }); } public deposit(amount: number): void { this.raise("Deposited", { amount }); } public withdraw(amount: number): void { if (amount > this.state.balance) throw new Error("Insufficient funds"); this.raise("Withdrawn", { amount }); } public close(): void { this.raise("AccountClosed", {}); } protected applyEvent(event: DomainEvent): void { const p = event.payload as { amount: number }; switch (event.type) { case "Deposited": this.state = { ...this.state, balance: this.state.balance + p.amount, }; break; case "Withdrawn": this.state = { ...this.state, balance: this.state.balance - p.amount, }; break; case "AccountClosed": this.state = { ...this.state, open: false }; break; } } public get balance(): number { return this.state.balance; } public get isOpen(): boolean { return this.state.open; } } describe("event-sourcing integration", () => { it("full lifecycle: create, save, reload", { timeout: 30000 }, async () => { const store = createMemoryEventStore(); const repo = createAggregateRepository({ store, factory: (id) => new BankAccount(id), }); const account = new BankAccount("acc-1"); account.deposit(1000); account.withdraw(250); await repo.save(account); const loaded = await repo.load("acc-1"); expect(loaded.balance).toBe(750); expect(loaded.getVersion()).toBe(2); }); it("project events to read model", { timeout: 30000 }, async () => { const store = createMemoryEventStore(); const repo = createAggregateRepository({ store, factory: (id) => new BankAccount(id), }); const account = new BankAccount("acc-2"); account.deposit(500); account.deposit(200); account.withdraw(100); await repo.save(account); const totalDeposits = createProjection({ initialState: 0, handlers: { Deposited: (s, e) => s + (e.payload as { amount: number }).amount, }, }); const events = await store.load("acc-2"); expect(totalDeposits.project(events)).toBe(700); }); it("snapshot reduces replay events", { timeout: 30000 }, async () => { const store = createMemoryEventStore(); const repo = createAggregateRepository({ store, factory: (id) => new BankAccount(id), snapshotThreshold: 3, }); const account = new BankAccount("acc-3"); account.deposit(100); account.deposit(200); account.deposit(300); await repo.save(account); // should snapshot at version 3 const snap = await store.loadSnapshot("acc-3"); expect(snap).toBeDefined(); expect(snap?.version).toBe(3); const loaded = await repo.load("acc-3"); expect(loaded.balance).toBe(600); }); it("concurrent save raises concurrency conflict", { timeout: 30000, }, async () => { const store = createMemoryEventStore(); const repo = createAggregateRepository({ store, factory: (id) => new BankAccount(id), }); // First save const a1 = new BankAccount("acc-4"); a1.deposit(100); await repo.save(a1); // Load same aggregate twice const b1 = await repo.load("acc-4"); const b2 = await repo.load("acc-4"); b1.deposit(50); b2.withdraw(50); await repo.save(b1); // b2 has stale version — should conflict await expect(repo.save(b2)).rejects.toThrow(); }); });