import { describe, it, expect } from "vitest"; import { computeWinner, computeScoreboard, categoryFromTask, customCriterion, criterionById, ratingKeyFor, shuffledOrder, normalizeOrder, blindLabel, CRITERION_RE, type ResponseLike } from "@/lib/arena/scoring"; import { liveMetrics, formatDelta } from "@/lib/arena/metrics"; import { buildArenaMarkdown, buildArenaJson, buildArenaShareSnapshot, generationParameters, arenaExportFilename } from "@/lib/arena/export"; const r = (id: string, modelKey: string, over: Partial = {}): ResponseLike => ({ id, modelKey, status: "complete", ttftMs: 500, latencyMs: 3000, costUsd: 0.01, usage: { inputTokens: 100, outputTokens: 400 }, ...over }); describe("computeWinner", () => { const responses = [r("a", "openai/gpt", { ttftMs: 800, costUsd: 0.02, usage: { inputTokens: 100, outputTokens: 500 } }), r("b", "anthropic/claude", { ttftMs: 400, costUsd: 0.01, usage: { inputTokens: 100, outputTokens: 300 } }), r("c", "gemini/flash", { ttftMs: 300, costUsd: 0.002, usage: { inputTokens: 100, outputTokens: 200 } })]; it("returns null without votes", () => { expect(computeWinner(responses, [])).toBeNull(); expect(computeWinner([], [{ criterion: "best", responseId: "a", modelKey: "openai/gpt" }])).toBeNull(); }); it("picks the response with the most criteria", () => { const w = computeWinner(responses, [ { criterion: "best", responseId: "a", modelKey: "openai/gpt" }, { criterion: "writing", responseId: "a", modelKey: "openai/gpt" }, { criterion: "fastest", responseId: "c", modelKey: "gemini/flash" }, ]); expect(w?.modelKey).toBe("openai/gpt"); expect(w?.criteriaWon.sort()).toEqual(["best", "writing"]); expect(w?.tieBreak).toBeNull(); }); it("breaks ties by time to first token", () => { const w = computeWinner(responses, [ { criterion: "best", responseId: "a", modelKey: "openai/gpt" }, { criterion: "coding", responseId: "b", modelKey: "anthropic/claude" }, ]); expect(w?.modelKey).toBe("anthropic/claude"); expect(w?.tieBreak).toBe("fastest"); }); it("computes deltas against the mean of the others", () => { const w = computeWinner(responses, [{ criterion: "best", responseId: "c", modelKey: "gemini/flash" }])!; expect(w.deltas.others).toBe(2); expect(w.deltas.costUsd).toBeCloseTo(0.002 - 0.015, 6); expect(w.deltas.ttftMs).toBeCloseTo(300 - 600); expect(w.deltas.outputTokens).toBeCloseTo(200 - 400); }); it("ignores votes pointing at unknown responses and dedupes criteria", () => { const w = computeWinner(responses, [ { criterion: "best", responseId: "zzz", modelKey: "nope/none" }, { criterion: "value", responseId: "b", modelKey: "anthropic/claude" }, { criterion: "value", responseId: "b", modelKey: "anthropic/claude" }, ]); expect(w?.modelKey).toBe("anthropic/claude"); expect(w?.votes).toBe(1); }); }); describe("computeScoreboard", () => { const sessions = [ { id: "s1", modelKeys: ["openai/gpt", "anthropic/claude"], category: "coding" as const }, { id: "s2", modelKeys: ["openai/gpt", "anthropic/claude"], category: "writing" as const }, { id: "s3", modelKeys: ["openai/gpt", "gemini/flash"], category: "coding" as const }, ]; const responses = [ { ...r("a1", "openai/gpt", { costUsd: 0.02, ttftMs: 800 }), sessionId: "s1" }, { ...r("b1", "anthropic/claude", { costUsd: 0.01, ttftMs: 400 }), sessionId: "s1" }, { ...r("a2", "openai/gpt", { costUsd: 0.04, ttftMs: 600 }), sessionId: "s2" }, { ...r("b2", "anthropic/claude", { costUsd: 0.01, ttftMs: 500 }), sessionId: "s2" }, { ...r("a3", "openai/gpt", { costUsd: 0.03, ttftMs: 700 }), sessionId: "s3" }, { ...r("c3", "gemini/flash", { costUsd: 0.001, ttftMs: 200 }), sessionId: "s3" }, ]; const votes = [ { sessionId: "s1", criterion: "best", responseId: "b1", modelKey: "anthropic/claude" }, { sessionId: "s1", criterion: "coding", responseId: "b1", modelKey: "anthropic/claude" }, { sessionId: "s2", criterion: "best", responseId: "a2", modelKey: "openai/gpt" }, { sessionId: "s2", criterion: "value", responseId: "b2", modelKey: "anthropic/claude" }, // s3 undecided ]; it("ranks by win rate over decided sessions", () => { const rows = computeScoreboard({ sessions, responses, votes }); const claude = rows.find((x) => x.modelKey === "anthropic/claude")!; const gpt = rows.find((x) => x.modelKey === "openai/gpt")!; const flash = rows.find((x) => x.modelKey === "gemini/flash")!; expect(claude.sessions).toBe(2); expect(claude.decided).toBe(2); expect(claude.wins).toBe(2); // s1 (2 criteria) + s2 (1–1 tie → Claude's 500 ms TTFT beats 600 ms) expect(claude.winRate).toBe(1); expect(gpt.sessions).toBe(3); expect(gpt.decided).toBe(2); expect(gpt.wins).toBe(0); expect(flash.decided).toBe(0); expect(flash.winRate).toBe(0); expect(rows[0].modelKey).toBe("anthropic/claude"); expect(claude.votes).toBe(3); expect(claude.avgCostUsd).toBeCloseTo(0.01); expect(gpt.avgTtftMs).toBeCloseTo(700); }); it("s2 tie goes to the faster model", () => { const rows = computeScoreboard({ sessions: [sessions[1]], responses, votes }); // a2 ttft 600 vs b2 ttft 500 → claude wins the tie expect(rows.find((x) => x.modelKey === "anthropic/claude")!.wins).toBe(1); expect(rows.find((x) => x.modelKey === "openai/gpt")!.wins).toBe(0); }); it("filters by category", () => { const rows = computeScoreboard({ sessions, responses, votes }, { category: "coding" }); expect(rows.find((x) => x.modelKey === "anthropic/claude")!.sessions).toBe(1); expect(rows.find((x) => x.modelKey === "openai/gpt")!.sessions).toBe(2); expect(rows.find((x) => x.modelKey === "openai/gpt")!.decided).toBe(1); }); it("filters by criterion (cost efficiency = value)", () => { const rows = computeScoreboard({ sessions, responses, votes }, { criterion: "value" }); const claude = rows.find((x) => x.modelKey === "anthropic/claude")!; expect(claude.wins).toBe(1); expect(claude.decided).toBe(1); expect(claude.winRate).toBe(1); expect(rows.find((x) => x.modelKey === "openai/gpt")!.wins).toBe(0); }); it("returns an empty list without sessions", () => { expect(computeScoreboard({ sessions: [], responses: [], votes: [] })).toEqual([]); }); }); describe("criteria & categories", () => { it("maps router tasks to categories", () => { expect(categoryFromTask("coding")).toBe("coding"); expect(categoryFromTask("analysis")).toBe("reasoning"); expect(categoryFromTask("translation")).toBe("writing"); expect(categoryFromTask("chat")).toBe("general"); expect(categoryFromTask(undefined)).toBe("general"); }); it("builds custom criteria with stable ids", () => { const c = customCriterion(" Most Concise! ")!; expect(c.id).toBe("custom:most-concise"); expect(c.label).toBe("Most Concise!"); expect(CRITERION_RE.test(c.id)).toBe(true); expect(customCriterion("!!!")).toBeNull(); expect(criterionById("custom:most-concise").label).toBe("Most concise"); }); it("keeps legacy rating keys", () => { expect(ratingKeyFor("best")).toBe("best"); expect(ratingKeyFor("fastest")).toBe("bestSpeed"); expect(ratingKeyFor("coding")).toBe("bestCoding"); expect(ratingKeyFor("custom:tone").startsWith("bestCustom_")).toBe(true); }); }); describe("blind helpers", () => { it("shuffles into a valid permutation", () => { const o = shuffledOrder(4, () => 0.42); expect([...o].sort()).toEqual([0, 1, 2, 3]); expect(normalizeOrder(o, 4)).toEqual(o); expect(normalizeOrder([0, 0, 1], 3)).toEqual([0, 1, 2]); expect(normalizeOrder("nope", 2)).toEqual([0, 1]); expect(blindLabel(2)).toBe("Model C"); }); }); describe("liveMetrics", () => { const model = { key: "openai/gpt", id: "gpt", provider: "openai" as const, displayName: "GPT", capabilities: { text: true, vision: false, audioInput: false, audioOutput: false, imageGeneration: false, video: false, reasoning: false, tools: false, structuredOutput: false, streaming: true, files: false, webSearch: false }, limits: {}, parameters: {}, status: "active" as const, pricing: { inputPerMillion: 1, outputPerMillion: 10 } }; it("estimates while streaming and switches to exact when done", () => { const t0 = 1_000; const live = liveMetrics({ status: "streaming", text: "x".repeat(400), startedAt: t0, firstTokenAt: t0 + 500, response: null }, model, 200, t0 + 2500); expect(live.exact).toBe(false); expect(live.ttftMs).toBe(500); expect(live.outputTokens).toBe(100); expect(live.tokensPerSecond).toBe(50); expect(live.costUsd).toBeCloseTo(200 / 1e6 + 100 * 10 / 1e6, 9); const done = liveMetrics({ status: "done", text: "x", startedAt: t0, firstTokenAt: t0 + 500, response: { ttftMs: 480, latencyMs: 2480, costUsd: 0.0012, usage: { inputTokens: 210, outputTokens: 120 } } }, model, 200, t0 + 9999); expect(done.exact).toBe(true); expect(done.ttftMs).toBe(480); expect(done.outputTokens).toBe(120); expect(done.tokensPerSecond).toBe(60); expect(done.costUsd).toBe(0.0012); }); it("formats signed deltas", () => { expect(formatDelta(-0.0031, "usd")).toBe("−$0.0031"); expect(formatDelta(120, "ms")).toBe("+120 ms"); expect(formatDelta(-340, "tokens")).toBe("−340 tok"); expect(formatDelta(null, "ms")).toBe("—"); }); }); describe("exports", () => { const input = { session: { id: "arn_1", prompt: "Explain monads simply", systemPrompt: "Be brief", modelKeys: ["openai/gpt", "anthropic/claude"], settings: { temperature: 0.3, attachmentIds: ["att_1"], blind: true, blindOrder: [1, 0] }, createdAt: "2026-09-11T12:00:00.000Z" }, responses: [ { ...r("a", "openai/gpt"), provider: "openai", content: "A monad is…", reasoning: null, error: null, ratings: { best: false }, createdAt: "2026-09-11T12:00:01.000Z" }, { ...r("b", "anthropic/claude", { ttftMs: 300 }), provider: "anthropic", content: "Think of a box…", reasoning: "hmm", error: null, ratings: { best: true }, createdAt: "2026-09-11T12:00:01.000Z" }, ], votes: [{ criterion: "best", responseId: "b", modelKey: "anthropic/claude", category: "general", createdAt: "2026-09-11T12:00:05.000Z" }], models: { "openai/gpt": { displayName: "GPT", provider: "openai" }, "anthropic/claude": { displayName: "Claude", provider: "anthropic" } }, }; it("strips Arena flags from parameters", () => { expect(generationParameters(input.session.settings)).toEqual({ temperature: 0.3 }); expect(arenaExportFilename("Explain monads simply", "md")).toBe("arena-explain-monads-simply.md"); }); it("builds markdown with prompt, metrics, votes, winner and responses", () => { const md = buildArenaMarkdown(input); expect(md).toContain("# Arena comparison — 2 models"); expect(md).toContain("Blind Arena"); expect(md).toContain("Explain monads simply"); expect(md).toContain("| Claude | complete | 300 ms |"); expect(md).toContain("**Best answer** → Claude"); expect(md).toContain("**Arena Winner: Claude**"); expect(md).toContain("1 attachment (not included"); expect(md).toContain("### Claude (anthropic)"); expect(md).toContain("
Reasoning"); }); it("builds JSON and a share snapshot without attachment data", () => { const json = JSON.parse(buildArenaJson(input)); expect(json.kind).toBe("arena-session"); expect(json.session.parameters).toEqual({ temperature: 0.3 }); expect(json.session.attachmentCount).toBe(1); expect(json.winner.displayName).toBe("Claude"); expect(json.responses[1].criteriaWon).toEqual(["best"]); const snap = buildArenaShareSnapshot(input); expect(snap.version).toBe(1); expect(snap.blind).toBe(true); expect(snap.models.map((m) => m.displayName)).toEqual(["GPT", "Claude"]); expect(snap.winner?.modelKey).toBe("anthropic/claude"); expect(JSON.stringify(snap)).not.toContain("att_1"); expect(JSON.stringify(snap)).not.toContain("dataBase64"); }); });