import { describe, expect, test } from "bun:test"; import { renderReport, resultsJson, type BenchRun } from "../../src/bench/report.ts"; import type { SongScore } from "../../src/eval/score.ts"; const mr = (f1: number | null, oct = 0) => ({ tp: 0, fp: 0, fn: 0, precision: f1, recall: f1, f1, octaveErrors: oct, matches: [] as [number, number][] }); function score(gf1: number, vocals: number | null, flags = { oracleStems: false, oracleTempo: false, oracleNotes: false }): SongScore { return { flags, guitarMerged: { ...mr(gf1, 1), pass: gf1 >= 0.85 }, partSplit: null, perRole: { guitar: mr(gf1), vocals: mr(vocals), bass: mr(0.5) }, drumsPerClass: null, rhythm: { factor: 1, beatOffset: 0, onBeat: 0.9, durationOk: null }, stringAgreement: null, spuriousRate: 0.1, gpPitch: { ...mr(gf1), tp: 0, fp: 0, fn: 1 }, barAgreement: { refBars: 8, outBars: 8, timeSigMatch: true }, }; } const modes = [ { name: "full", flags: { oracleStems: false, oracleTempo: false, oracleNotes: false } }, { name: "oracle-notes", flags: { oracleStems: true, oracleTempo: true, oracleNotes: true } }, ]; const mk = (g: number): BenchRun => ({ runId: "fx", date: "2026-10-30", modes, results: [ { tier: "t1a", seed: 1, mode: "full", score: score(g, null), seconds: 1 }, { tier: "t1a", seed: 2, mode: "full", score: score(g, 0.4), seconds: 2 }, { tier: "t1a", seed: 1, mode: "oracle-notes", score: score(1, 1, modes[1]!.flags), seconds: 1 }, ], }); describe("renderReport", () => { test("column order, n/a, caveat", () => { const md = renderReport(mk(0.9)); const header = md.split("\n").find((l) => l.startsWith("| Mode"))!; expect(header.indexOf("Guitar merged F1")).toBeLessThan(header.indexOf("Vocals F1")); expect(header.indexOf("Vocals F1")).toBeLessThan(header.indexOf("Bass F1")); expect(md).toContain("n/a"); expect(md).toContain("Voice Oohs"); expect(md).toContain("fx"); }); test("tier pass line", () => { expect(renderReport(mk(0.9))).toContain("PASS"); const bad = renderReport(mk(0.5)); expect(bad).toContain("FAIL"); expect(bad).not.toContain("Tier t1a: PASS"); }); test("per-mode tick uses the median rule with a k/n pass count", () => { const gs = [0.9, 0.9, 0.9, 0.9, 0.3]; const run: BenchRun = { runId: "fx", date: "d", modes: [modes[0]!], results: gs.map((g, i) => ({ tier: "t1a" as const, seed: i, mode: "full", score: score(g, null), seconds: 1 })) }; const row = renderReport(run).split("\n").find((l) => l.startsWith("| full"))!; expect(row).toContain("✓ 4/5"); expect(row).not.toContain("✗"); const bad = renderReport({ ...run, results: gs.map((g, i) => ({ tier: "t1a" as const, seed: i, mode: "full", score: score(g === 0.9 ? 0.3 : 0.9, null), seconds: 1 })) }); expect(bad.split("\n").find((l) => l.startsWith("| full"))).toContain("✗ 1/5"); }); test("secondary metric columns: GP-file F1, bar agreement, techniques n/a", () => { const md = renderReport(mk(0.9)); const header = md.split("\n").find((l) => l.startsWith("| Mode"))!; expect(header).toContain("GP-file guitar F1"); expect(header).toContain("Bar count match"); expect(header).toContain("Techniques"); expect(md).toContain("n/a (tier 1 has no technique marks)"); }); test("role with empty truth renders n/a with spurious count", () => { const run = mk(0.9); const s = run.results[0]!.score; s.perRole.bass = { ...mr(0), tp: 0, fp: 3, fn: 0 }; const md = renderReport({ ...run, results: [run.results[0]!] }); expect(md).toContain("n/a (3 spurious)"); expect(md).not.toContain("0.00 (0.00"); }); }); test("results JSON omits per-note matches", () => { const run = mk(0.9); run.results[0]!.score.guitarMerged.matches = [[0, 1]]; const json = resultsJson(run); expect(json).not.toContain('"matches"'); expect(JSON.parse(json).results).toHaveLength(3); expect(JSON.stringify(run)).toContain('"matches"'); // in-memory run (and score.json) keep them });