/** * Unit tests for tournament tools. * @module openfinclaw/tournament/tools.test */ import { DatabaseSync } from "node:sqlite"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { TournamentDb, ensureTournamentSchema } from "./db.js"; /** * Minimal tool executor for testing. * Collects registered tools and lets tests call them directly. */ class ToolCollector { tools = new Map< string, { execute: (id: string, params: Record) => Promise } >(); registerTool(factory: unknown, opts?: unknown) { const tool = typeof factory === "function" ? (factory as (ctx: unknown) => unknown)(null) : factory; const t = tool as { name: string; execute: (id: string, params: Record) => Promise; }; const names = (opts as { names?: string[] } | undefined)?.names ?? [t.name]; for (const name of names) { this.tools.set(name, t); } } async call( name: string, params: Record, ): Promise<{ content: Array<{ type: string; text: string }>; details: unknown }> { const tool = this.tools.get(name); if (!tool) throw new Error(`Tool not found: ${name}`); return tool.execute("test-call-id", params) as Promise<{ content: Array<{ type: string; text: string }>; details: unknown; }>; } } /** Extract parsed details from tool result. */ function details(result: { details: unknown }): Record { if (typeof result.details === "string") return JSON.parse(result.details); return result.details as Record; } describe("tournament tools", () => { let db: DatabaseSync; let tdb: TournamentDb; let collector: ToolCollector; beforeEach(async () => { db = new DatabaseSync(":memory:"); db.exec("PRAGMA journal_mode = WAL"); ensureTournamentSchema(db); tdb = new TournamentDb(db); collector = new ToolCollector(); const { registerTournamentTools } = await import("./tools.js"); registerTournamentTools(collector.registerTool.bind(collector), () => tdb); }); afterEach(() => { db.close(); }); describe("tournament_pick", () => { it("records valid pick", async () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.updateRoundStatus("round-20260331", "completed"); tdb.saveStrategy({ round_id: "round-20260331", agent_name: "bull", thesis: "test", confidence: 80, entry_price: null, exit_price: null, stop_loss: null, position_pct: null, sharpe: 1.5, max_drawdown: null, total_return: null, raw_result: null, }); const result = await collector.call("tournament_pick", { agent_name: "bull", user_id: "tg:123", session_key: "agent:main", }); const d = details(result); expect(d.agent_name).toBe("bull"); expect(d.message).toContain("BULL"); }); it("rejects invalid agent name", async () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.updateRoundStatus("round-20260331", "completed"); const result = await collector.call("tournament_pick", { agent_name: "invalid", user_id: "tg:123", session_key: "agent:main", }); const d = details(result); expect(d.error).toBe(true); }); it("returns error when no active round", async () => { const result = await collector.call("tournament_pick", { agent_name: "bull", user_id: "tg:123", session_key: "agent:main", }); const d = details(result); expect(d.error).toBe(true); expect(d.message).toContain("没有活跃"); }); it("handles duplicate pick idempotently", async () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.updateRoundStatus("round-20260331", "completed"); await collector.call("tournament_pick", { agent_name: "bull", user_id: "tg:123", session_key: "agent:main", }); const result = await collector.call("tournament_pick", { agent_name: "bear", user_id: "tg:123", session_key: "agent:main", }); const d = details(result); expect(d.message).toContain("已经"); }); it("allows different users to pick same round", async () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); tdb.updateRoundStatus("round-20260331", "completed"); const r1 = await collector.call("tournament_pick", { agent_name: "bull", user_id: "tg:111", session_key: "agent:main", }); const r2 = await collector.call("tournament_pick", { agent_name: "bear", user_id: "tg:222", session_key: "agent:main", }); expect(details(r1).agent_name).toBe("bull"); expect(details(r2).agent_name).toBe("bear"); }); }); describe("tournament_leaderboard", () => { it("returns formatted ranking with data", async () => { tdb.recordWin("bull", 2.0); tdb.recordWin("bear", 1.0); tdb.recordWin("contrarian", 3.0); const result = await collector.call("tournament_leaderboard", {}); const d = details(result); expect(d.message).toContain("排行榜"); expect((d.agents as Array<{ name: string }>)[0].name).toBe("contrarian"); }); it("returns empty message when no data", async () => { const result = await collector.call("tournament_leaderboard", {}); const d = details(result); expect(d.message).toContain("还没有"); }); }); describe("tournament_result", () => { it("stores strategy with all fields", async () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); const result = await collector.call("tournament_result", { round_id: "round-20260331", agent_name: "bull", thesis: "Strong momentum signals", entry_price: 150, exit_price: 165, stop_loss: 145, position_pct: 0.25, confidence: 85, sharpe: 1.5, max_drawdown: -0.08, total_return: 0.12, }); const d = details(result); expect(d.agent_name).toBe("bull"); const strategies = tdb.getStrategies("round-20260331"); expect(strategies).toHaveLength(1); expect(strategies[0].confidence).toBe(85); expect(strategies[0].sharpe).toBe(1.5); }); it("rejects invalid confidence range", async () => { tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" }); const result = await collector.call("tournament_result", { round_id: "round-20260331", agent_name: "bull", thesis: "test", confidence: 150, }); const d = details(result); expect(d.error).toBe(true); expect(d.message).toContain("0-100"); }); }); });