/** * Unit tests for tournament DB operations. * @module openfinclaw/tournament/db.test */ import { DatabaseSync } from "node:sqlite"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { TournamentDb, ensureTournamentSchema } from "./db.js"; describe("TournamentDb", () => { let db: DatabaseSync; let tdb: TournamentDb; beforeEach(() => { db = new DatabaseSync(":memory:"); db.exec("PRAGMA journal_mode = WAL"); ensureTournamentSchema(db); tdb = new TournamentDb(db); }); afterEach(() => { db.close(); }); describe("schema", () => { it("creates tables idempotently", () => { ensureTournamentSchema(db); ensureTournamentSchema(db); const tables = db .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'tournament_%'") .all() as Array<{ name: string }>; const names = tables.map((t) => t.name).sort(); expect(names).toEqual([ "tournament_agents", "tournament_picks", "tournament_rounds", "tournament_strategies", ]); }); }); describe("rounds", () => { it("creates a round", () => { const created = tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); expect(created).toBe(true); const round = tdb.getRound("round-20260331"); expect(round).toBeDefined(); expect(round!.ticker).toBe("AAPL"); expect(round!.status).toBe("running"); }); it("returns false for duplicate round", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); const dup = tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "TSLA" }); expect(dup).toBe(false); }); it("updates round status to completed", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.updateRoundStatus("round-20260331", "completed"); const round = tdb.getRound("round-20260331")!; expect(round.status).toBe("completed"); expect(round.completed_at).toBeTruthy(); }); it("updates round status to skipped with reason", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.updateRoundStatus("round-20260331", "skipped", "Less than 2 agents succeeded"); const round = tdb.getRound("round-20260331")!; expect(round.status).toBe("skipped"); expect(round.skip_reason).toBe("Less than 2 agents succeeded"); }); it("counts consecutive skips", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "A" }); tdb.updateRoundStatus("round-20260331", "skipped"); tdb.createRound({ id: "round-20260330", date: "2026-03-30", ticker: "B" }); tdb.updateRoundStatus("round-20260330", "skipped"); tdb.createRound({ id: "round-20260329", date: "2026-03-29", ticker: "C" }); tdb.updateRoundStatus("round-20260329", "completed"); expect(tdb.countConsecutiveSkips()).toBe(2); }); }); describe("strategies", () => { it("saves and retrieves strategies", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.saveStrategy({ round_id: "round-20260331", agent_name: "bull", thesis: "EMA crossover bullish", entry_price: 150.0, exit_price: 165.0, stop_loss: 145.0, position_pct: 0.25, confidence: 85, sharpe: 1.5, max_drawdown: -0.08, total_return: 0.12, raw_result: '{"detail":"full analysis"}', }); const strategies = tdb.getStrategies("round-20260331"); expect(strategies).toHaveLength(1); expect(strategies[0].agent_name).toBe("bull"); expect(strategies[0].confidence).toBe(85); expect(strategies[0].sharpe).toBe(1.5); }); it("upserts strategy on conflict", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.saveStrategy({ round_id: "round-20260331", agent_name: "bull", thesis: "First", confidence: 50, entry_price: null, exit_price: null, stop_loss: null, position_pct: null, sharpe: null, max_drawdown: null, total_return: null, raw_result: null, }); tdb.saveStrategy({ round_id: "round-20260331", agent_name: "bull", thesis: "Updated", confidence: 90, entry_price: 100, exit_price: 110, stop_loss: 95, position_pct: 0.3, sharpe: 2.0, max_drawdown: -0.05, total_return: 0.2, raw_result: null, }); const strategies = tdb.getStrategies("round-20260331"); expect(strategies).toHaveLength(1); expect(strategies[0].thesis).toBe("Updated"); expect(strategies[0].confidence).toBe(90); }); }); describe("agents", () => { it("records win and updates avg sharpe", () => { tdb.recordWin("bull", 1.5); tdb.recordWin("bull", 2.5); const agent = tdb.getAgent("bull")!; expect(agent.wins).toBe(2); expect(agent.rounds_played).toBe(2); expect(agent.avg_sharpe).toBe(2.0); }); it("records loss", () => { tdb.recordLoss("bear", 0.5); const agent = tdb.getAgent("bear")!; expect(agent.losses).toBe(1); expect(agent.rounds_played).toBe(1); }); it("returns leaderboard sorted by avg_sharpe", () => { tdb.recordWin("bull", 2.0); tdb.recordWin("bear", 1.0); tdb.recordWin("contrarian", 3.0); const lb = tdb.getLeaderboard(); expect(lb[0].name).toBe("contrarian"); expect(lb[1].name).toBe("bull"); expect(lb[2].name).toBe("bear"); }); }); describe("picks", () => { it("records a pick", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); const recorded = tdb.recordPick({ round_id: "round-20260331", user_id: "telegram:12345", session_key: "agent:main:telegram", agent_name: "bull", }); expect(recorded).toBe(true); }); it("returns false for duplicate pick by same user", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.recordPick({ round_id: "round-20260331", user_id: "telegram:12345", session_key: "agent:main:telegram", agent_name: "bull", }); const dup = tdb.recordPick({ round_id: "round-20260331", user_id: "telegram:12345", session_key: "agent:main:telegram", agent_name: "bear", }); expect(dup).toBe(false); }); it("allows different users to pick same round", () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); const pick1 = tdb.recordPick({ round_id: "round-20260331", user_id: "telegram:12345", session_key: "agent:main:telegram", agent_name: "bull", }); const pick2 = tdb.recordPick({ round_id: "round-20260331", user_id: "telegram:67890", session_key: "agent:main:telegram", agent_name: "bear", }); expect(pick1).toBe(true); expect(pick2).toBe(true); }); }); });