/** * Unit tests for the Evaluation center's Score-only chart helpers: Score extraction, * dynamic y-axis range, gap segmentation across runtime series, and runtime grouping. */ import { describe, expect, it } from "vitest"; import { lineSegments, modelSeries, scoreScale, scoreValues, seriesValues, } from "../src/features/benchmark/benchmark-metrics"; const evaluations = [{ score: 60 }, { score: 75.25 }, { score: 85.5 }]; describe("scoreValues", () => { it("extracts stored Scores and treats non-finite input as a gap", () => { expect(scoreValues(evaluations)).toEqual([60, 75.25, 85.5]); expect(scoreValues([{ score: Number.NaN }, { score: Infinity }])).toEqual([null, null]); }); }); describe("scoreScale (dynamic padded Score axis)", () => { it("pads observed scores, clamps to 0..100, and rounds outward to friendly ticks", () => { expect(scoreScale([71, 83.67, 88.33])).toEqual({ min: 60, max: 100, ticks: [60, 70, 80, 90, 100], }); }); it("keeps a dynamic range for a single or repeated score", () => { expect(scoreScale([88])).toEqual({ min: 75, max: 100, ticks: [75, 80, 85, 90, 95, 100], }); expect(scoreScale([50, 50])).toEqual({ min: 40, max: 60, ticks: [40, 45, 50, 55, 60], }); }); it("clamps boundary scores and falls back safely when every value is missing", () => { expect(scoreScale([100])).toEqual({ min: 90, max: 100, ticks: [90, 92, 94, 96, 98, 100], }); expect(scoreScale([0])).toEqual({ min: 0, max: 10, ticks: [0, 2, 4, 6, 8, 10], }); expect(scoreScale([null, null])).toEqual({ min: 0, max: 100, ticks: [0, 20, 40, 60, 80, 100], }); }); }); describe("lineSegments (gap segmentation)", () => { it("no gaps: one segment with everything (consecutive indexes)", () => { expect(lineSegments([60, 75.25, 85.5])).toEqual([ [ { index: 0, value: 60 }, { index: 1, value: 75.25 }, { index: 2, value: 85.5 }, ], ]); }); it("a middle gap breaks into two segments (a lone point still forms a segment: point drawn, no line)", () => { expect(lineSegments([0.12, null, 0.2])).toEqual([ [{ index: 0, value: 0.12 }], [{ index: 2, value: 0.2 }], ]); expect(lineSegments([null, 1, 2, null, 3])).toEqual([ [ { index: 1, value: 1 }, { index: 2, value: 2 }, ], [{ index: 4, value: 3 }], ]); }); it("all missing / empty list: no segments", () => { expect(lineSegments([null, null])).toEqual([]); expect(lineSegments([])).toEqual([]); }); }); describe("modelSeries / seriesValues (curves split by model ID and thinking level)", () => { const mixed = [ { score: 6, provider: "deepseek", modelId: "deepseek-v4-flash", thinkingLevel: "medium", }, { score: 7 }, // Defensive untagged input -> trailing gray series. { score: 7.5, provider: "deepseek", modelId: "deepseek-v4-pro", thinkingLevel: "xhigh", }, { score: 8.5, provider: "deepseek", modelId: "deepseek-v4-pro", thinkingLevel: "xhigh", }, ]; it("groups by (modelId, thinkingLevel) in first-appearance order; untagged records go to a trailing unnamed series", () => { const series = modelSeries(mixed); expect(series.map((s) => s.modelId)).toEqual([ "deepseek-v4-flash", "deepseek-v4-pro", undefined, ]); expect(series.map((s) => s.thinkingLevel)).toEqual(["medium", "xhigh", undefined]); expect(series.map((s) => s.indices)).toEqual([[0], [2, 3], [1]]); expect(series[2]!.key).toBe(""); }); it("the same model ID and thinking level across providers stays in one series", () => { const sameRuntime = [ { score: 1, provider: "moonshot", modelId: "kimi-k2.6", thinkingLevel: "medium", }, { score: 2, provider: "siliconflow", modelId: "kimi-k2.6", thinkingLevel: "medium", }, ]; const series = modelSeries(sameRuntime); expect(series).toHaveLength(1); expect(series[0]!.indices).toEqual([0, 1]); }); it("the same model ID at different thinking levels forms separate series", () => { const levels = [ { score: 1, modelId: "deepseek-v4-pro", thinkingLevel: "medium" }, { score: 2, modelId: "deepseek-v4-pro", thinkingLevel: "xhigh" }, ]; const series = modelSeries(levels); expect(series).toHaveLength(2); expect(series.map((s) => s.thinkingLevel)).toEqual(["medium", "xhigh"]); }); it("seriesValues: indexes outside the series are null (skipped points), keeping the global time axis", () => { const series = modelSeries(mixed); expect(seriesValues(mixed, series[1]!)).toEqual([null, null, 7.5, 8.5]); expect(seriesValues(mixed, series[2]!)).toEqual([null, 7, null, null]); }); it("all untagged defensive input forms one unnamed series", () => { const series = modelSeries([{}, {}]); expect(series).toHaveLength(1); expect(series[0]!.key).toBe(""); expect(series[0]!.indices).toEqual([0, 1]); }); });