Files
gitea-axi/bench/aggregate.test.ts
alexion 7216d3cc31
All checks were successful
CI / test (pull_request) Successful in 51s
CI / test (push) Successful in 50s
feat: add benchmark aggregator and reporting (task 0030)
Add bench/aggregate.ts: the pure aggregator seam that rolls the
accumulated sample store into a readable comparison. aggregate produces
a headline table (one row per arm, cost-equivalent tokens as the
headline plus raw tokens, turns, duration, success rate, coverage, and
imputed cost as a de-emphasized secondary column), per-tier and
per-token-component breakdowns, and the separate bonus table; renderReport
renders it as stable text. Cost-equivalent tokens are weighted at render
time from the four retained components per ADR 0014, and incomplete
coverage is annotated rather than hidden. Unit-tested against synthetic
append-only sample stores.
2026-07-16 12:46:03 -04:00

554 lines
22 KiB
TypeScript

import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
aggregate,
costEquivalentTokens,
readAllSamples,
renderReport,
} from "./aggregate.js";
import type { ResultRecord, TokenComponents } from "./result.js";
import { createSampleStore } from "./store.js";
/** A minimal ResultRecord for the gitea-axi arm, overridable per sample. */
function record(overrides: Partial<ResultRecord> = {}): ResultRecord {
return {
arm: "gitea-axi",
taskId: "t1",
tier: "read",
trial: 1,
timestamp: "2026-07-16T00:00:00Z",
tokens: { freshInput: 0, cacheCreation: 0, cacheRead: 0, output: 0 },
turns: 0,
durationMs: 0,
imputedCostUsd: 0,
outcome: { pass: true },
...overrides,
};
}
describe("costEquivalentTokens", () => {
// Behavior: cost-equivalent tokens weight a run's four retained token
// components by Anthropic's published API pricing ratios — fresh input 1x,
// cache-write 1.25x, cache-read 0.1x, output 5x (ADR 0014). The expected
// total is derived BY HAND from those ratios, not recomputed the way the
// code would, so it independently pins the metric.
//
// Distinct components are chosen so each of the four weighted products is a
// different number and no two raw components share a value; a wrong weight on
// any single component therefore cannot be masked by another:
// freshInput 400 * 1 = 400
// cacheCreation 800 * 1.25 = 1000
// cacheRead 2000 * 0.1 = 200
// output 600 * 5 = 3000
// total = 4600
it("weights the four retained token components by the documented pricing ratios", () => {
const tokens: TokenComponents = {
freshInput: 400,
cacheCreation: 800,
cacheRead: 2000,
output: 600,
};
expect(costEquivalentTokens(tokens)).toBe(4600);
});
});
describe("aggregate", () => {
// Behavior: the headline view yields one row per arm whose per-run metrics —
// cost-equivalent tokens (the headline), raw tokens, turns, duration, and
// imputed cost — are the MEANS across that arm's samples, and whose success
// rate is the fraction of its runs that passed. Here the gitea-axi arm has
// two samples on task t1 (one pass, one fail). Every expected number below is
// derived BY HAND from the two samples and the ADR 0014 pricing ratios
// (fresh input 1x, cache-write 1.25x, cache-read 0.1x, output 5x), so it
// pins the metric independently of how aggregate computes it:
// cost-equiv A = 100*1 + 20*5 = 200; B = 200*1 + 40*5 = 400; mean = 300
// raw tokens A = 120; B = 240; mean = 180
// turns (5 + 7) / 2 = 6
// durationMs (1000 + 3000) / 2 = 2000
// successRate 1 pass of 2 = 0.5
// imputedCost (0.02 + 0.06) / 2 = 0.04
it("yields one headline row per arm with per-run means and the passing fraction as success rate", () => {
const sampleA = record({
trial: 1,
tokens: { freshInput: 100, cacheCreation: 0, cacheRead: 0, output: 20 },
turns: 5,
durationMs: 1000,
imputedCostUsd: 0.02,
outcome: { pass: true },
});
const sampleB = record({
trial: 2,
tokens: { freshInput: 200, cacheCreation: 0, cacheRead: 0, output: 40 },
turns: 7,
durationMs: 3000,
imputedCostUsd: 0.06,
outcome: { pass: false, failure: "incorrect" },
});
const report = aggregate({
records: [sampleA, sampleB],
suite: [{ id: "t1", tier: "read" }],
bonus: [],
});
const row = report.headline.find((r) => r.arm === "gitea-axi");
if (!row) throw new Error("expected a gitea-axi headline row");
expect(row.samples).toBe(2);
expect(row.costEquivalentTokens).toBeCloseTo(300);
expect(row.rawTokens).toBeCloseTo(180);
expect(row.turns).toBeCloseTo(6);
expect(row.durationMs).toBeCloseTo(2000);
expect(row.successRate).toBeCloseTo(0.5);
expect(row.imputedCostUsd).toBeCloseTo(0.04);
});
// Behavior: the per-tier breakdown is rendered from the same records — grouped
// by tier, then by arm — reporting each tier-arm's cost-equivalent-token mean
// and passing fraction. Three gitea-axi samples span two tiers: two on a read
// task r1 (one pass, one fail) and one on a single-mutation task m1 (pass).
// Every expected number is derived BY HAND from those samples and the ADR 0014
// weights (fresh input 1x, output 5x; no cache here), independent of how
// aggregate computes them:
// read gitea-axi: cost A = 100 + 20*5 = 200; B = 200 + 40*5 = 400;
// mean = 300; success 1 of 2 = 0.5; samples 2
// single-mutation gitea-axi: cost = 50 + 10*5 = 100; success 1 of 1 = 1;
// samples 1
// The tier order is the fixed reporting order, an independent literal.
it("breaks records down per tier and per arm with cost-equivalent-token means and success rates", () => {
const r1a = record({
taskId: "r1",
tier: "read",
trial: 1,
tokens: { freshInput: 100, cacheCreation: 0, cacheRead: 0, output: 20 },
outcome: { pass: true },
});
const r1b = record({
taskId: "r1",
tier: "read",
trial: 2,
tokens: { freshInput: 200, cacheCreation: 0, cacheRead: 0, output: 40 },
outcome: { pass: false, failure: "incorrect" },
});
const m1c = record({
taskId: "m1",
tier: "single-mutation",
trial: 1,
tokens: { freshInput: 50, cacheCreation: 0, cacheRead: 0, output: 10 },
outcome: { pass: true },
});
const report = aggregate({
records: [r1a, r1b, m1c],
suite: [
{ id: "r1", tier: "read" },
{ id: "m1", tier: "single-mutation" },
],
bonus: [],
});
// One breakdown per tier, in the fixed reporting order.
expect(report.tiers.map((t) => t.tier)).toEqual([
"read",
"single-mutation",
"find-then-act",
"multi-step",
]);
const readAxi = report.tiers
.find((t) => t.tier === "read")
?.arms.find((a) => a.arm === "gitea-axi");
if (!readAxi) throw new Error("expected a read/gitea-axi tier-arm");
expect(readAxi.samples).toBe(2);
expect(readAxi.costEquivalentTokens).toBeCloseTo(300);
expect(readAxi.successRate).toBeCloseTo(0.5);
const mutAxi = report.tiers
.find((t) => t.tier === "single-mutation")
?.arms.find((a) => a.arm === "gitea-axi");
if (!mutAxi) throw new Error("expected a single-mutation/gitea-axi tier-arm");
expect(mutAxi.samples).toBe(1);
expect(mutAxi.costEquivalentTokens).toBeCloseTo(100);
expect(mutAxi.successRate).toBeCloseTo(1);
});
// Behavior: the per-token-component breakdown is rendered from the same
// records — per arm, the per-run MEAN of each of the four raw token
// components — so a reader can see what drives an arm's cost. The gitea-axi
// arm has two samples; an arm with no samples reports null for every
// component. Each mean is derived BY HAND from the two samples, independent
// of how aggregate computes it:
// freshInput (100 + 200) / 2 = 150
// cacheCreation (40 + 60) / 2 = 50
// cacheRead (1000 + 3000)/2 = 2000
// output (20 + 40) / 2 = 30
it("breaks records down per arm into the per-run mean of each token component", () => {
const sampleA = record({
trial: 1,
tokens: { freshInput: 100, cacheCreation: 40, cacheRead: 1000, output: 20 },
});
const sampleB = record({
trial: 2,
tokens: { freshInput: 200, cacheCreation: 60, cacheRead: 3000, output: 40 },
});
const report = aggregate({
records: [sampleA, sampleB],
suite: [{ id: "t1", tier: "read" }],
bonus: [],
});
const axi = report.components.find((c) => c.arm === "gitea-axi");
if (!axi) throw new Error("expected a gitea-axi component breakdown");
expect(axi.freshInput).toBeCloseTo(150);
expect(axi.cacheCreation).toBeCloseTo(50);
expect(axi.cacheRead).toBeCloseTo(2000);
expect(axi.output).toBeCloseTo(30);
// An arm with no samples reports null for every component.
const tea = report.components.find((c) => c.arm === "tea");
if (!tea) throw new Error("expected a tea component breakdown");
expect(tea.freshInput).toBeNull();
expect(tea.cacheCreation).toBeNull();
expect(tea.cacheRead).toBeNull();
expect(tea.output).toBeNull();
});
// Behavior: the separate bonus table is rendered from the same records. It
// emits one row per supplied bonus definition, in the given order, carrying
// that operation's capability metadata (operation text, direction, note, and
// gitea-axi's own applicability), plus — for any arm that actually has
// samples for that bonus cell — the per-arm run metrics. Bonus cells are
// usually unrun, so a cell's arms list is empty unless records exist for it.
// Here bonus-x has one tea sample and bonus-y has none. The one derived
// number is by hand from that sample and the ADR 0014 weights (fresh input
// 1x, output 5x): cost = 100 + 20*5 = 200.
it("emits one bonus row per definition with its capability metadata and per-arm samples only where run", () => {
const teaSample = record({
arm: "tea",
taskId: "bonus-x",
tier: "read",
trial: 1,
tokens: { freshInput: 100, cacheCreation: 0, cacheRead: 0, output: 20 },
outcome: { pass: true },
});
const report = aggregate({
records: [teaSample],
suite: [],
bonus: [
{
id: "bonus-x",
operation: "Do the X thing",
direction: "gitea-axi-advantage",
note: "only gitea-axi does X ergonomically",
giteaAxi: "applicable",
},
{
id: "bonus-y",
operation: "Do the Y thing",
direction: "gitea-axi-not-applicable",
note: "Y is outside gitea-axi's surface",
giteaAxi: "not-applicable",
},
],
});
// One row per definition, in the given order.
expect(report.bonus.map((b) => b.id)).toEqual(["bonus-x", "bonus-y"]);
// bonus-x carries its capability metadata and the tea cell's metrics.
const bx = report.bonus.find((b) => b.id === "bonus-x");
if (!bx) throw new Error("expected a bonus-x row");
expect(bx.operation).toBe("Do the X thing");
expect(bx.direction).toBe("gitea-axi-advantage");
expect(bx.note).toBe("only gitea-axi does X ergonomically");
expect(bx.giteaAxi).toBe("applicable");
const bxTea = bx.arms.find((a) => a.arm === "tea");
if (!bxTea) throw new Error("expected a tea entry on bonus-x");
expect(bx.arms).toHaveLength(1);
expect(bxTea.samples).toBe(1);
expect(bxTea.costEquivalentTokens).toBeCloseTo(200);
expect(bxTea.successRate).toBeCloseTo(1);
// bonus-y was never run, so it carries metadata but no arm metrics.
const by = report.bonus.find((b) => b.id === "bonus-y");
if (!by) throw new Error("expected a bonus-y row");
expect(by.giteaAxi).toBe("not-applicable");
expect(by.direction).toBe("gitea-axi-not-applicable");
expect(by.arms).toEqual([]);
});
});
describe("renderReport", () => {
// Behavior: the rendered headline table shows one row per arm, presents
// cost-equivalent tokens as the headline metric column and imputed cost as a
// de-emphasized secondary column, and marks an arm with no samples with the
// "—" placeholder. Two gitea-axi samples on t1 give a cost-equivalent mean of
// (200 + 400)/2 = 300 and an imputed-cost mean of (0.02 + 0.06)/2 = 0.04,
// both derived BY HAND, independent of how the report is rendered. Assertions
// target the gitea-axi row's own line and the presence of labels/values —
// never fixed column widths — so they survive a layout refactor.
it("renders one row per arm with cost-equivalent tokens as the headline and imputed cost as a secondary column", () => {
const sampleA = record({
trial: 1,
tokens: { freshInput: 100, cacheCreation: 0, cacheRead: 0, output: 20 },
turns: 5,
durationMs: 1000,
imputedCostUsd: 0.02,
outcome: { pass: true },
});
const sampleB = record({
trial: 2,
tokens: { freshInput: 200, cacheCreation: 0, cacheRead: 0, output: 40 },
turns: 7,
durationMs: 3000,
imputedCostUsd: 0.06,
outcome: { pass: false, failure: "incorrect" },
});
const output = renderReport(
aggregate({
records: [sampleA, sampleB],
suite: [{ id: "t1", tier: "read" }],
bonus: [],
}),
);
// Every arm gets a row.
expect(output).toContain("gitea-axi");
expect(output).toContain("tea");
expect(output).toContain("gitea-mcp");
expect(output).toContain("raw-api");
// The headline metric and the secondary column are labelled.
expect(output.toLowerCase()).toContain("cost-equivalent");
expect(output.toLowerCase()).toContain("imputed");
// The gitea-axi row carries its cost-equivalent mean (300) and imputed
// mean (0.04) on its own line.
const axiLine = output.split("\n").find((l) => l.includes("gitea-axi"));
if (!axiLine) throw new Error("expected a gitea-axi row line");
expect(axiLine).toContain("300");
expect(axiLine).toContain("0.04");
// An arm with no samples shows the em-dash placeholder on its row. The
// "tea" arm token is matched at a word boundary so the substring inside
// "gitea-axi"/"gitea-mcp" cannot be mistaken for the tea row.
const teaLine = output
.split("\n")
.find((l) => /(^|[^a-z-])tea([^a-z-]|$)/.test(l));
if (!teaLine) throw new Error("expected a tea row line");
expect(teaLine).toContain("—");
});
// Behavior: a partial matrix renders without error and incomplete coverage is
// annotated rather than hidden or treated as complete. With the default
// reporting floor of 3, a cell is covered at >=3 samples, partial at 1-2, and
// missing at 0. gitea-axi has one covered / one partial / one missing cell,
// so it is incomplete; tea has all three cells covered, so it is complete.
// The coverage counts (1 partial, 1 missing for gitea-axi) are derived BY
// HAND from the sample layout, independent of how the report is rendered.
// Lines are located by content, not index, so the assertions survive a
// layout refactor.
it("renders a partial matrix without throwing and annotates incomplete coverage rather than hiding it", () => {
const read = (taskId: string, trial: number) =>
record({ taskId, tier: "read", trial, outcome: { pass: true } });
const records: ResultRecord[] = [
// gitea-axi: t1 covered (3), t2 partial (2), t3 missing (0).
read("t1", 1),
read("t1", 2),
read("t1", 3),
read("t2", 1),
read("t2", 2),
// tea: all three tasks covered (3 each).
{ ...read("t1", 1), arm: "tea" },
{ ...read("t1", 2), arm: "tea" },
{ ...read("t1", 3), arm: "tea" },
{ ...read("t2", 1), arm: "tea" },
{ ...read("t2", 2), arm: "tea" },
{ ...read("t2", 3), arm: "tea" },
{ ...read("t3", 1), arm: "tea" },
{ ...read("t3", 2), arm: "tea" },
{ ...read("t3", 3), arm: "tea" },
];
const input = {
records,
suite: [
{ id: "t1", tier: "read" as const },
{ id: "t2", tier: "read" as const },
{ id: "t3", tier: "read" as const },
],
bonus: [],
};
// A partial matrix renders without error.
expect(() => renderReport(aggregate(input))).not.toThrow();
const output = renderReport(aggregate(input));
// The coverage annotation names the reporting floor it is measured against.
expect(output).toContain("reporting floor of 3");
// gitea-axi is incomplete: 1 partial, 1 missing, flagged incomplete.
const axiLine = output
.split("\n")
.find((l) => /\bgitea-axi\b/.test(l) && l.includes("partial"));
if (!axiLine) throw new Error("expected a gitea-axi coverage annotation");
expect(axiLine).toContain("1 partial");
expect(axiLine).toContain("1 missing");
expect(axiLine).toContain("incomplete");
// tea is complete and is not labelled incomplete.
const teaLine = output
.split("\n")
.find((l) => /(^|[^a-z-])tea([^a-z-]|$)/.test(l) && l.includes("complete"));
if (!teaLine) throw new Error("expected a tea coverage annotation");
expect(teaLine).toContain("complete");
expect(teaLine).not.toContain("incomplete");
});
// Behavior: the per-tier breakdown, the per-token-component breakdown, and the
// separate bonus table are all rendered from the same records — each as its
// own section surfacing its content. A single gitea-axi sample on a read task
// fixes the component means (one sample, so each mean equals its raw value):
// fresh input 300, cache write 80, cache read 1000, output 60 — derived BY
// HAND, independent of how the report renders. Assertions are presence-based
// (section headers and content strings), not column layout, so they survive a
// rendering refactor.
it("renders the per-tier, per-token-component, and bonus sections from the same records", () => {
const output = renderReport(
aggregate({
records: [
record({
taskId: "r1",
tier: "read",
trial: 1,
tokens: {
freshInput: 300,
cacheCreation: 80,
cacheRead: 1000,
output: 60,
},
outcome: { pass: true },
}),
],
suite: [
{ id: "r1", tier: "read" },
{ id: "m1", tier: "single-mutation" },
],
bonus: [
{
id: "bonus-x",
operation: "Do the X thing",
direction: "gitea-axi-advantage",
note: "only gitea-axi does X",
giteaAxi: "applicable",
},
],
}),
);
const lower = output.toLowerCase();
// Per-tier section: a header naming tiers, and every tier surfaced.
expect(lower).toContain("tier");
expect(output).toContain("read");
expect(output).toContain("single-mutation");
expect(output).toContain("find-then-act");
expect(output).toContain("multi-step");
// Per-token-component section: a header, the four component labels, and
// gitea-axi's component means.
expect(lower).toContain("component");
expect(lower).toContain("fresh input");
expect(lower).toContain("cache write");
expect(lower).toContain("cache read");
expect(lower).toContain("output");
expect(output).toContain("300");
expect(output).toContain("80");
expect(output).toContain("1000");
expect(output).toContain("60");
// Bonus section: a header, the operation text, and gitea-axi's applicability.
expect(lower).toContain("bonus");
expect(output).toContain("Do the X thing");
expect(output).toContain("applicable");
});
});
describe("renderReport over an append-only sample store", () => {
let rootA: string;
let rootB: string;
beforeEach(() => {
rootA = mkdtempSync(join(tmpdir(), "bench-aggregate-"));
rootB = mkdtempSync(join(tmpdir(), "bench-aggregate-"));
});
afterEach(() => {
rmSync(rootA, { recursive: true, force: true });
rmSync(rootB, { recursive: true, force: true });
});
// Behavior: rendering is stable — the rendered report is a function of the
// accumulated set of samples only, so the order in which samples were
// appended to the store must not change the output. This is a stability
// property a correct implementation already satisfies, exercised end to end
// through the real append-only store. The same records are appended to two
// independent stores in genuinely different orders (one reversed and with
// cells interleaved differently); the two rendered reports must be identical.
it("renders identically regardless of the order samples were appended", () => {
// A set spanning two arms and two tasks; the gitea-axi/t1 cell reaches the
// reporting floor of three so the headline and coverage sections are
// non-trivial rather than all placeholders.
const records: ResultRecord[] = [
record({ arm: "gitea-axi", taskId: "t1", tier: "read", trial: 1 }),
record({ arm: "gitea-axi", taskId: "t1", tier: "read", trial: 2 }),
record({ arm: "gitea-axi", taskId: "t1", tier: "read", trial: 3 }),
record({ arm: "gitea-axi", taskId: "t2", tier: "single-mutation", trial: 1 }),
record({ arm: "tea", taskId: "t1", tier: "read", trial: 1 }),
record({ arm: "tea", taskId: "t2", tier: "single-mutation", trial: 1 }),
record({ arm: "tea", taskId: "t2", tier: "single-mutation", trial: 2 }),
];
// Two genuinely different append orders over the same set.
const orderA = records;
const orderB = [...records].reverse();
// The two orders truly differ, so the stability claim is not vacuous.
expect(orderB).not.toEqual(orderA);
const storeA = createSampleStore(rootA);
for (const r of orderA) storeA.append(r);
const storeB = createSampleStore(rootB);
for (const r of orderB) storeB.append(r);
const suite = [
{ id: "t1", tier: "read" as const },
{ id: "t2", tier: "single-mutation" as const },
];
const a = renderReport(
aggregate({ records: readAllSamples(storeA), suite, bonus: [] }),
);
const b = renderReport(
aggregate({ records: readAllSamples(storeB), suite, bonus: [] }),
);
// Append order does not affect the rendered report.
expect(a).toBe(b);
// Non-vacuity: the report actually rendered real headline content.
expect(a.length).toBeGreaterThan(0);
expect(a).toContain("gitea-axi");
expect(a.toLowerCase()).toContain("cost-equivalent");
});
});