niceeval 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs-site/zh/concepts/adapter.mdx +6 -6
- package/docs-site/zh/concepts/experiment.mdx +7 -7
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +63 -20
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +243 -0
- package/docs-site/zh/guides/experiments.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +252 -0
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +224 -0
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/viewing-results.mdx +147 -77
- package/docs-site/zh/guides/write-experiment.mdx +9 -9
- package/docs-site/zh/guides/write-send.mdx +6 -6
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/introduction.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +10 -2
- package/docs-site/zh/reference/define-agent.mdx +5 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/package.json +3 -2
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/ai-sdk.ts +2 -2
- package/src/agents/bub.ts +14 -4
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +4 -4
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +104 -14
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +3 -3
- package/src/context/session.ts +2 -2
- package/src/context/types.ts +2 -2
- package/src/i18n/en.ts +29 -7
- package/src/i18n/zh-CN.ts +24 -2
- package/src/report/aggregate.ts +79 -50
- package/src/report/components.tsx +190 -0
- package/src/report/compute.ts +149 -84
- package/src/report/default-report.tsx +222 -0
- package/src/report/dual-face.test.tsx +527 -0
- package/src/report/format.ts +29 -0
- package/src/report/index.ts +79 -24
- package/src/report/load.ts +67 -0
- package/src/report/metrics.ts +2 -1
- package/src/report/param.ts +18 -0
- package/src/report/primitives.tsx +117 -0
- package/src/report/react/CaseList.tsx +2 -2
- package/src/report/react/DeltaTable.tsx +2 -2
- package/src/report/react/MetricBars.tsx +109 -0
- package/src/report/react/MetricLine.tsx +200 -0
- package/src/report/react/MetricMatrix.tsx +1 -1
- package/src/report/react/MetricScatter.tsx +1 -1
- package/src/report/react/MetricTable.tsx +1 -1
- package/src/report/react/RunOverview.tsx +4 -4
- package/src/report/react/Scoreboard.tsx +3 -3
- package/src/report/react/cell.tsx +1 -1
- package/src/report/react/fixtures.ts +98 -36
- package/src/report/react/format.ts +3 -28
- package/src/report/react/index.tsx +41 -24
- package/src/report/react/render.test.tsx +8 -8
- package/src/report/react/styles.css +77 -0
- package/src/report/report.test.ts +248 -128
- package/src/report/report.ts +69 -0
- package/src/report/text/faces.ts +356 -0
- package/src/report/text/layout.ts +125 -0
- package/src/report/text/plot.ts +127 -0
- package/src/report/tree.ts +220 -0
- package/src/report/types.ts +75 -27
- package/src/report/web.ts +40 -0
- package/src/results/copy.ts +95 -41
- package/src/results/format.ts +12 -11
- package/src/results/index.ts +34 -17
- package/src/results/open.ts +201 -81
- package/src/results/results.test.ts +595 -191
- package/src/results/select.ts +107 -55
- package/src/results/types.ts +137 -45
- package/src/results/writer.ts +258 -0
- package/src/runner/attempt.ts +3 -3
- package/src/runner/fingerprint.ts +1 -1
- package/src/runner/reporters/artifacts.ts +38 -78
- package/src/runner/reporters/braintrust.test.ts +2 -2
- package/src/runner/reporters/braintrust.ts +3 -3
- package/src/runner/run.ts +1 -1
- package/src/runner/types.ts +18 -12
- package/src/show/compose.ts +215 -0
- package/src/show/index.ts +263 -0
- package/src/show/render.ts +456 -0
- package/src/show/show.test.ts +505 -0
- package/src/view/app/App.tsx +85 -24
- package/src/view/app/components/CostScoreChart.tsx +29 -10
- package/src/view/app/components/ExperimentTable.tsx +27 -9
- package/src/view/app/components/GroupSelector.tsx +1 -1
- package/src/view/app/i18n.ts +18 -3
- package/src/view/app/lib/attempt-route.test.ts +9 -9
- package/src/view/app/lib/attempt-route.ts +5 -5
- package/src/view/app/lib/outcome.ts +1 -3
- package/src/view/app/lib/rows.ts +95 -15
- package/src/view/app/main.tsx +18 -6
- package/src/view/app/pages/RunsPage.tsx +4 -7
- package/src/view/app/pages/TracesPage.tsx +3 -8
- package/src/view/app/types.ts +50 -6
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.test.ts +254 -0
- package/src/view/data.ts +336 -0
- package/src/view/index.ts +64 -10
- package/src/view/server.ts +31 -9
- package/src/view/shared/types.ts +47 -41
- package/src/view/styles.css +5 -0
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +308 -0
- package/docs-site/.mintignore +0 -13
- package/docs-site/AGENTS.md +0 -46
- package/docs-site/concepts/adapter.mdx +0 -287
- package/docs-site/concepts/assert.mdx +0 -243
- package/docs-site/concepts/drive.mdx +0 -118
- package/docs-site/concepts/evals.mdx +0 -108
- package/docs-site/concepts/experiment.mdx +0 -37
- package/docs-site/concepts/hitl.mdx +0 -83
- package/docs-site/concepts/judge.mdx +0 -129
- package/docs-site/concepts/overview.mdx +0 -98
- package/docs-site/concepts/tier.mdx +0 -42
- package/docs-site/docs-ref/00-index.md +0 -23
- package/docs-site/docs-ref/01-page-types.md +0 -43
- package/docs-site/docs-ref/03-style-rules.md +0 -45
- package/docs-site/docs-ref/04-code-examples.md +0 -37
- package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
- package/docs-site/docs-ref/06-checklists.md +0 -53
- package/docs-site/docs.json +0 -256
- package/docs-site/example/ai-agent-application.mdx +0 -148
- package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
- package/docs-site/example/claude-code-codex-skill.mdx +0 -147
- package/docs-site/example/showcase.mdx +0 -39
- package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
- package/docs-site/example/tier1-claude-sdk.mdx +0 -119
- package/docs-site/example/tier1-codex-sdk.mdx +0 -111
- package/docs-site/example/tier1-langgraph.mdx +0 -119
- package/docs-site/example/tier1-pi-sdk.mdx +0 -119
- package/docs-site/favicon.svg +0 -5
- package/docs-site/github-diff.css +0 -135
- package/docs-site/github-diff.js +0 -11
- package/docs-site/guides/authoring.mdx +0 -129
- package/docs-site/guides/ci-integration.mdx +0 -97
- package/docs-site/guides/connect-otel.mdx +0 -209
- package/docs-site/guides/connect-your-agent.mdx +0 -221
- package/docs-site/guides/dataset-fanout.mdx +0 -98
- package/docs-site/guides/experiments.mdx +0 -71
- package/docs-site/guides/fixtures.mdx +0 -147
- package/docs-site/guides/reporters.mdx +0 -113
- package/docs-site/guides/runner.mdx +0 -79
- package/docs-site/guides/sandbox-agent.mdx +0 -138
- package/docs-site/guides/sandbox-backends.mdx +0 -64
- package/docs-site/guides/scoring-guide.mdx +0 -92
- package/docs-site/guides/viewing-results.mdx +0 -195
- package/docs-site/guides/write-experiment.mdx +0 -81
- package/docs-site/guides/write-send.mdx +0 -347
- package/docs-site/index.mdx +0 -181
- package/docs-site/introduction.mdx +0 -141
- package/docs-site/quickstart.mdx +0 -136
- package/docs-site/reference/builtin-agents.mdx +0 -161
- package/docs-site/reference/capabilities.mdx +0 -76
- package/docs-site/reference/cli.mdx +0 -120
- package/docs-site/reference/define-agent.mdx +0 -168
- package/docs-site/reference/define-config.mdx +0 -41
- package/docs-site/reference/define-eval.mdx +0 -160
- package/docs-site/reference/events.mdx +0 -131
- package/docs-site/reference/expect.mdx +0 -112
- package/docs-site/tracker.js +0 -8
- package/src/report/react/data.ts +0 -17
- package/src/view/aggregate.ts +0 -103
- package/src/view/loader.test.ts +0 -61
- package/src/view/loader.ts +0 -257
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
// niceeval/report
|
|
2
|
-
//
|
|
3
|
-
// examScore 空真、skipped 稀释、scoreboard
|
|
4
|
-
//
|
|
1
|
+
// niceeval/report 计算层的单元测试:全部用内存 fake(Snapshot / AttemptHandle 按
|
|
2
|
+
// niceeval/results 的读取契约手工构造),专门覆盖 docs/reports.md 点名的坑 ——
|
|
3
|
+
// 两级聚合 vs 平铺、pass@k、examScore 空真、skipped 稀释、scoreboard 固定分母与
|
|
4
|
+
// 最长前缀、scatter/delta 的 null 语义、快照键对比、param 维度与轴、
|
|
5
|
+
// cases 的 redact/truncated、身份键去重、Selection warnings 随行。
|
|
5
6
|
|
|
6
7
|
import { describe, expect, it } from "vitest";
|
|
7
8
|
|
|
8
9
|
import type { AssertionResult, EvalResult, ResultOutcome, RunSummary } from "../types.ts";
|
|
9
|
-
import type { AttemptHandle,
|
|
10
|
-
import type { Dimension } from "./types.ts";
|
|
10
|
+
import type { AttemptHandle, RunDir, Selection, SelectionWarning, Snapshot } from "../results/index.ts";
|
|
11
|
+
import type { Dimension, MetricCell } from "./types.ts";
|
|
11
12
|
import { costUSD, defineMetric, durationMs, examScore, passRate, tokens } from "./metrics.ts";
|
|
12
|
-
import {
|
|
13
|
+
import { param } from "./param.ts";
|
|
13
14
|
import { formatMetricValue } from "./format.ts";
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
import {
|
|
16
|
+
CaseList,
|
|
17
|
+
DeltaTable,
|
|
18
|
+
MetricBars,
|
|
19
|
+
MetricLine,
|
|
20
|
+
MetricMatrix,
|
|
21
|
+
MetricScatter,
|
|
22
|
+
MetricTable,
|
|
23
|
+
RunOverview,
|
|
24
|
+
Scoreboard,
|
|
25
|
+
} from "./components.tsx";
|
|
26
|
+
|
|
27
|
+
// ───────────────────────── fake 数据(按 results 读取契约造)─────────────────────────
|
|
16
28
|
|
|
17
29
|
let seq = 0;
|
|
18
30
|
|
|
@@ -35,31 +47,19 @@ function softAssertion(name: string, score: number, extra: Partial<AssertionResu
|
|
|
35
47
|
return { name, severity: "soft", score, passed: true, ...extra };
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
function wrap(run: RunHandle, result: EvalResult, index: number): AttemptHandle {
|
|
39
|
-
return {
|
|
40
|
-
run,
|
|
41
|
-
ref: { run: run.dir.split("/").pop()!, result: index },
|
|
42
|
-
result,
|
|
43
|
-
events: async () => null,
|
|
44
|
-
trace: async () => null,
|
|
45
|
-
o11y: async () => null,
|
|
46
|
-
diff: async () => null,
|
|
47
|
-
sources: async () => null,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
50
|
interface SnapSpec {
|
|
52
51
|
experimentId: string;
|
|
53
52
|
results: EvalResult[];
|
|
54
53
|
agent?: string;
|
|
55
54
|
model?: string;
|
|
56
55
|
runStartedAt?: string;
|
|
56
|
+
knownEvalIds?: string[];
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
let runSeq = 0;
|
|
60
60
|
|
|
61
61
|
/** 最小构造:一个 run 装一个快照。runStartedAt 决定去重时谁是「最新 run」。 */
|
|
62
|
-
function snap(spec: SnapSpec):
|
|
62
|
+
function snap(spec: SnapSpec): Snapshot {
|
|
63
63
|
runSeq += 1;
|
|
64
64
|
const startedAt = spec.runStartedAt ?? `2026-06-01T00:00:00.${String(runSeq).padStart(3, "0")}Z`;
|
|
65
65
|
const summary: RunSummary = {
|
|
@@ -73,17 +73,52 @@ function snap(spec: SnapSpec): SnapshotHandle {
|
|
|
73
73
|
durationMs: 0,
|
|
74
74
|
results: spec.results,
|
|
75
75
|
};
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
const runDir: RunDir = { dir: `/results/run-${runSeq}`, summary, attempts: [] };
|
|
77
|
+
const attempts: AttemptHandle[] = spec.results.map((r, i) => ({
|
|
78
|
+
evalId: r.id,
|
|
79
|
+
experimentId: r.experimentId ?? spec.experimentId,
|
|
80
|
+
result: r,
|
|
81
|
+
ref: { run: `run-${runSeq}`, result: i },
|
|
82
|
+
runDir,
|
|
83
|
+
events: async () => null,
|
|
84
|
+
trace: async () => null,
|
|
85
|
+
o11y: async () => null,
|
|
86
|
+
diff: async () => null,
|
|
87
|
+
sources: async () => null,
|
|
88
|
+
}));
|
|
89
|
+
runDir.attempts = attempts;
|
|
90
|
+
const evals = new Map<string, AttemptHandle[]>();
|
|
91
|
+
for (const attempt of attempts) {
|
|
92
|
+
const list = evals.get(attempt.evalId);
|
|
93
|
+
if (list) list.push(attempt);
|
|
94
|
+
else evals.set(attempt.evalId, [attempt]);
|
|
95
|
+
}
|
|
79
96
|
return {
|
|
80
97
|
experimentId: spec.experimentId,
|
|
81
|
-
run,
|
|
82
98
|
startedAt,
|
|
83
99
|
agent: spec.agent ?? "agent-x",
|
|
84
100
|
model: spec.model,
|
|
101
|
+
schemaVersion: 1,
|
|
102
|
+
evals: [...evals.entries()].map(([id, list]) => ({ id, attempts: list })),
|
|
85
103
|
attempts,
|
|
86
|
-
|
|
104
|
+
runDir,
|
|
105
|
+
knownEvalIds: spec.knownEvalIds,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 手工造一个选集(warnings 随行的形状,filter 语义与 results 一致)。 */
|
|
110
|
+
function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selection {
|
|
111
|
+
return {
|
|
112
|
+
snapshots,
|
|
113
|
+
warnings,
|
|
114
|
+
filter(predicate) {
|
|
115
|
+
const kept = snapshots.filter(predicate);
|
|
116
|
+
const survivors = new Set(kept.map((s) => s.experimentId));
|
|
117
|
+
return selection(
|
|
118
|
+
kept,
|
|
119
|
+
warnings.filter((w) => typeof w.experimentId !== "string" || survivors.has(w.experimentId)),
|
|
120
|
+
);
|
|
121
|
+
},
|
|
87
122
|
};
|
|
88
123
|
}
|
|
89
124
|
|
|
@@ -93,14 +128,9 @@ describe("两级聚合引擎", () => {
|
|
|
93
128
|
it("题内先折再跨题平均:A=[1]、B=[0,0,0] → 0.5,不是平铺的 0.25", async () => {
|
|
94
129
|
const s = snap({
|
|
95
130
|
experimentId: "exp/x",
|
|
96
|
-
results: [
|
|
97
|
-
res("A", "passed"),
|
|
98
|
-
res("B", "failed"),
|
|
99
|
-
res("B", "failed"),
|
|
100
|
-
res("B", "failed"),
|
|
101
|
-
],
|
|
131
|
+
results: [res("A", "passed"), res("B", "failed"), res("B", "failed"), res("B", "failed")],
|
|
102
132
|
});
|
|
103
|
-
const data = await
|
|
133
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
|
|
104
134
|
expect(data.dimension).toBe("agent");
|
|
105
135
|
expect(data.rows).toHaveLength(1);
|
|
106
136
|
const cell = data.rows[0].cells["pass-rate"];
|
|
@@ -116,20 +146,14 @@ describe("两级聚合引擎", () => {
|
|
|
116
146
|
name: "pass@k",
|
|
117
147
|
better: "higher",
|
|
118
148
|
unit: "%",
|
|
119
|
-
value: (a) =>
|
|
120
|
-
a.result.outcome === "skipped" ? null : a.result.outcome === "passed" ? 1 : 0,
|
|
149
|
+
value: (a) => (a.result.outcome === "skipped" ? null : a.result.outcome === "passed" ? 1 : 0),
|
|
121
150
|
aggregate: { perEval: "max", across: "mean" },
|
|
122
151
|
});
|
|
123
152
|
const s = snap({
|
|
124
153
|
experimentId: "exp/x",
|
|
125
|
-
results: [
|
|
126
|
-
res("A", "failed"),
|
|
127
|
-
res("A", "failed"),
|
|
128
|
-
res("B", "failed"),
|
|
129
|
-
res("B", "passed"),
|
|
130
|
-
],
|
|
154
|
+
results: [res("A", "failed"), res("A", "failed"), res("B", "failed"), res("B", "passed")],
|
|
131
155
|
});
|
|
132
|
-
const data = await
|
|
156
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, passRate] });
|
|
133
157
|
// A: max(0,0)=0;B: max(0,1)=1 → (0+1)/2
|
|
134
158
|
expect(data.rows[0].cells["pass@k"].value).toBe(0.5);
|
|
135
159
|
// 对照:默认 mean/mean 的 passRate = (0 + 0.5)/2
|
|
@@ -137,11 +161,8 @@ describe("两级聚合引擎", () => {
|
|
|
137
161
|
});
|
|
138
162
|
|
|
139
163
|
it("skipped 是 null:不稀释均值,但计入 total(覆盖率如实)", async () => {
|
|
140
|
-
const s = snap({
|
|
141
|
-
|
|
142
|
-
results: [res("A", "passed"), res("B", "skipped")],
|
|
143
|
-
});
|
|
144
|
-
const data = await table([s], { rows: "agent", columns: [passRate] });
|
|
164
|
+
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "skipped")] });
|
|
165
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
|
|
145
166
|
const cell = data.rows[0].cells["pass-rate"];
|
|
146
167
|
expect(cell.value).toBe(1); // B 整桶为 null,不参与 across,不是 0.5
|
|
147
168
|
expect(cell.samples).toBe(1);
|
|
@@ -149,14 +170,15 @@ describe("两级聚合引擎", () => {
|
|
|
149
170
|
expect(cell.refs).toHaveLength(1);
|
|
150
171
|
});
|
|
151
172
|
|
|
152
|
-
it("全组 null → value null、display 兜底,不编 0", async () => {
|
|
173
|
+
it("全组 null → value null、display 兜底,不编 0;refs 必填(空数组)", async () => {
|
|
153
174
|
const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
|
|
154
|
-
const data = await
|
|
175
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
|
|
155
176
|
const cell = data.rows[0].cells["pass-rate"];
|
|
156
177
|
expect(cell.value).toBeNull();
|
|
157
178
|
expect(cell.display).toBe("—");
|
|
158
179
|
expect(cell.samples).toBe(0);
|
|
159
180
|
expect(cell.total).toBe(1);
|
|
181
|
+
expect(cell.refs).toEqual([]);
|
|
160
182
|
});
|
|
161
183
|
|
|
162
184
|
it("where 不满足 → null,不进聚合", async () => {
|
|
@@ -166,7 +188,7 @@ describe("两级聚合引擎", () => {
|
|
|
166
188
|
value: () => 5,
|
|
167
189
|
});
|
|
168
190
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
|
|
169
|
-
const data = await
|
|
191
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [onlyPassed] });
|
|
170
192
|
const cell = data.rows[0].cells["only-passed"];
|
|
171
193
|
expect(cell.value).toBe(5);
|
|
172
194
|
expect(cell.samples).toBe(1);
|
|
@@ -186,45 +208,41 @@ describe("两级聚合引擎", () => {
|
|
|
186
208
|
res("A", "passed", { attempt: 2 }),
|
|
187
209
|
],
|
|
188
210
|
});
|
|
189
|
-
const data = await
|
|
211
|
+
const data = await MetricTable.data([s], { rows: byParity, columns: [passRate] });
|
|
190
212
|
expect(data.dimension).toBe("parity");
|
|
191
213
|
const byKey = Object.fromEntries(data.rows.map((r) => [r.key, r.cells["pass-rate"].value]));
|
|
192
214
|
// 同一道题的 attempt 分进两组:even 组内 [1,1] 折成 1,odd 组内 [0] 折成 0
|
|
193
|
-
// (若第一级在分组前全局做,两组都会是 2/3)
|
|
194
215
|
expect(byKey).toEqual({ even: 1, odd: 0 });
|
|
195
216
|
});
|
|
196
217
|
|
|
197
218
|
it("同一次计算里指标重名是错误", async () => {
|
|
198
219
|
const dup = defineMetric({ name: "pass-rate", value: () => 1 });
|
|
199
220
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
200
|
-
await expect(
|
|
221
|
+
await expect(MetricTable.data([s], { rows: "agent", columns: [passRate, dup] })).rejects.toThrow(
|
|
201
222
|
/Duplicate metric name "pass-rate"/,
|
|
202
223
|
);
|
|
203
224
|
});
|
|
204
225
|
|
|
205
226
|
it("sort 方向随 better,缺数据行沉底", async () => {
|
|
206
|
-
const good = snap({
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
});
|
|
211
|
-
const bad = snap({
|
|
212
|
-
experimentId: "exp/bad",
|
|
213
|
-
agent: "bad",
|
|
214
|
-
results: [res("A", "failed", { agent: "bad" })],
|
|
215
|
-
});
|
|
216
|
-
const none = snap({
|
|
217
|
-
experimentId: "exp/none",
|
|
218
|
-
agent: "none",
|
|
219
|
-
results: [res("A", "skipped", { agent: "none" })],
|
|
220
|
-
});
|
|
221
|
-
const data = await table([none, bad, good], {
|
|
227
|
+
const good = snap({ experimentId: "exp/good", agent: "good", results: [res("A", "passed", { agent: "good" })] });
|
|
228
|
+
const bad = snap({ experimentId: "exp/bad", agent: "bad", results: [res("A", "failed", { agent: "bad" })] });
|
|
229
|
+
const none = snap({ experimentId: "exp/none", agent: "none", results: [res("A", "skipped", { agent: "none" })] });
|
|
230
|
+
const data = await MetricTable.data([none, bad, good], {
|
|
222
231
|
rows: "agent",
|
|
223
232
|
columns: [passRate],
|
|
224
233
|
sort: passRate,
|
|
225
234
|
});
|
|
226
235
|
expect(data.rows.map((r) => r.key)).toEqual(["good", "bad", "none"]);
|
|
227
236
|
});
|
|
237
|
+
|
|
238
|
+
it("列键是字面量联合:拼错列名编译不过", async () => {
|
|
239
|
+
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
240
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [passRate, costUSD] });
|
|
241
|
+
const cell: MetricCell = data.rows[0].cells[passRate.name]; // 键锚在指标对象上
|
|
242
|
+
expect(cell.value).toBe(1);
|
|
243
|
+
// @ts-expect-error 列里没有这个键 —— 编译期挡住,不是运行时 undefined
|
|
244
|
+
data.rows[0].cells["pass-rat"];
|
|
245
|
+
});
|
|
228
246
|
});
|
|
229
247
|
|
|
230
248
|
// ───────────────────────── examScore ─────────────────────────
|
|
@@ -235,7 +253,7 @@ describe("examScore", () => {
|
|
|
235
253
|
experimentId: "exp/x",
|
|
236
254
|
results: [res("A", "errored", { assertions: [], error: "adapter crashed" })],
|
|
237
255
|
});
|
|
238
|
-
const data = await
|
|
256
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
|
|
239
257
|
const cell = data.rows[0].cells["exam-score"];
|
|
240
258
|
expect(cell.value).toBe(0); // 交白卷是 0 分,不是缺数据,更不是满分
|
|
241
259
|
expect(cell.samples).toBe(1);
|
|
@@ -244,11 +262,9 @@ describe("examScore", () => {
|
|
|
244
262
|
it("failed 得 0,哪怕 soft 分不低(报告不重新判卷)", async () => {
|
|
245
263
|
const s = snap({
|
|
246
264
|
experimentId: "exp/x",
|
|
247
|
-
results: [
|
|
248
|
-
res("A", "failed", { assertions: [softAssertion("judge", 0.9)] }),
|
|
249
|
-
],
|
|
265
|
+
results: [res("A", "failed", { assertions: [softAssertion("judge", 0.9)] })],
|
|
250
266
|
});
|
|
251
|
-
const data = await
|
|
267
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
|
|
252
268
|
expect(data.rows[0].cells["exam-score"].value).toBe(0);
|
|
253
269
|
});
|
|
254
270
|
|
|
@@ -273,15 +289,15 @@ describe("examScore", () => {
|
|
|
273
289
|
}),
|
|
274
290
|
],
|
|
275
291
|
});
|
|
276
|
-
const a = await
|
|
292
|
+
const a = await MetricTable.data([withSoft], { rows: "agent", columns: [examScore] });
|
|
277
293
|
expect(a.rows[0].cells["exam-score"].value).toBe(0.75);
|
|
278
|
-
const b = await
|
|
294
|
+
const b = await MetricTable.data([noSoft], { rows: "agent", columns: [examScore] });
|
|
279
295
|
expect(b.rows[0].cells["exam-score"].value).toBe(1);
|
|
280
296
|
});
|
|
281
297
|
|
|
282
298
|
it("skipped → null,不进聚合", async () => {
|
|
283
299
|
const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
|
|
284
|
-
const data = await
|
|
300
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
|
|
285
301
|
expect(data.rows[0].cells["exam-score"].value).toBeNull();
|
|
286
302
|
});
|
|
287
303
|
});
|
|
@@ -299,7 +315,7 @@ describe("内置指标", () => {
|
|
|
299
315
|
res("B", "failed"), // 无 usage → null,不稀释
|
|
300
316
|
],
|
|
301
317
|
});
|
|
302
|
-
const data = await
|
|
318
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [tokens] });
|
|
303
319
|
const cell = data.rows[0].cells["tokens"];
|
|
304
320
|
expect(cell.value).toBe(1200);
|
|
305
321
|
expect(cell.display).toBe("1.2k tokens");
|
|
@@ -318,16 +334,16 @@ describe("内置指标", () => {
|
|
|
318
334
|
res("B", "errored", { durationMs: 3000 }),
|
|
319
335
|
],
|
|
320
336
|
});
|
|
321
|
-
const cost = await
|
|
337
|
+
const cost = await MetricTable.data([s], { rows: "agent", columns: [costUSD] });
|
|
322
338
|
expect(cost.rows[0].cells["cost"].value).toBe(0.5);
|
|
323
|
-
const dur = await
|
|
339
|
+
const dur = await MetricTable.data([s], { rows: "agent", columns: [durationMs] });
|
|
324
340
|
expect(dur.rows[0].cells["duration"].value).toBe(2000); // (1000 + 3000)/2,errored 实测照算
|
|
325
341
|
});
|
|
326
342
|
});
|
|
327
343
|
|
|
328
|
-
// ─────────────────────────
|
|
344
|
+
// ───────────────────────── Scoreboard.data ─────────────────────────
|
|
329
345
|
|
|
330
|
-
describe("
|
|
346
|
+
describe("Scoreboard.data", () => {
|
|
331
347
|
it("固定分母 + missing 如实 + 权重最长前缀生效", async () => {
|
|
332
348
|
const alpha = snap({
|
|
333
349
|
experimentId: "exp/alpha",
|
|
@@ -343,13 +359,13 @@ describe("scoreboard", () => {
|
|
|
343
359
|
agent: "beta",
|
|
344
360
|
results: [res("algebra/x", "passed", { agent: "beta" })],
|
|
345
361
|
});
|
|
346
|
-
const board = await
|
|
347
|
-
|
|
362
|
+
const board = await Scoreboard.data([alpha, beta], {
|
|
363
|
+
rows: "agent",
|
|
348
364
|
subjects: "evalGroup",
|
|
349
365
|
weights: { "algebra/": 3, "algebra/hard/": 9 },
|
|
350
366
|
fullMarks: 100,
|
|
351
367
|
});
|
|
352
|
-
expect(board.
|
|
368
|
+
expect(board.dimension).toBe("agent");
|
|
353
369
|
// 生效权重表可审计:最长前缀在前(匹配顺序)
|
|
354
370
|
expect(board.weights).toEqual([
|
|
355
371
|
{ prefix: "algebra/hard/", weight: 9 },
|
|
@@ -390,32 +406,43 @@ describe("scoreboard", () => {
|
|
|
390
406
|
res("algebra/y", "skipped", { agent: "solo" }), // 无有效样本 → missing(按 0 计但如实标注)
|
|
391
407
|
],
|
|
392
408
|
});
|
|
393
|
-
const board = await
|
|
409
|
+
const board = await Scoreboard.data([solo], { rows: "agent" });
|
|
394
410
|
const row = board.rows[0];
|
|
395
411
|
// 两题各 1 分:0.5 + 0(missing)→ 100 × 0.5/2 = 25
|
|
396
412
|
expect(row.total.value).toBeCloseTo(25);
|
|
397
413
|
expect(row.subjects[0].missing).toBe(1);
|
|
398
414
|
expect(row.subjects[0].evals).toBe(2);
|
|
399
415
|
});
|
|
416
|
+
|
|
417
|
+
it("快照携带的 knownEvalIds 进固定分母(发布目录上的残缺不消失)", async () => {
|
|
418
|
+
const s = snap({
|
|
419
|
+
experimentId: "exp/x",
|
|
420
|
+
agent: "solo",
|
|
421
|
+
results: [res("algebra/x", "passed", { agent: "solo" })],
|
|
422
|
+
knownEvalIds: ["algebra/x", "algebra/y"],
|
|
423
|
+
});
|
|
424
|
+
const board = await Scoreboard.data([s], { rows: "agent" });
|
|
425
|
+
expect(board.rows[0].subjects[0].evals).toBe(2);
|
|
426
|
+
expect(board.rows[0].subjects[0].missing).toBe(1);
|
|
427
|
+
expect(board.rows[0].total.value).toBeCloseTo(50);
|
|
428
|
+
});
|
|
400
429
|
});
|
|
401
430
|
|
|
402
|
-
// ─────────────────────────
|
|
431
|
+
// ───────────────────────── MetricScatter.data ─────────────────────────
|
|
403
432
|
|
|
404
|
-
describe("
|
|
433
|
+
describe("MetricScatter.data", () => {
|
|
405
434
|
it("任一轴 null 的点仍在 rows 里、可数;series 随组解析", async () => {
|
|
406
435
|
const withCost = snap({
|
|
407
436
|
experimentId: "exp/a",
|
|
408
437
|
agent: "a1",
|
|
409
|
-
results: [
|
|
410
|
-
res("A", "passed", { agent: "a1", usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.5 } }),
|
|
411
|
-
],
|
|
438
|
+
results: [res("A", "passed", { agent: "a1", usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.5 } })],
|
|
412
439
|
});
|
|
413
440
|
const noCost = snap({
|
|
414
441
|
experimentId: "exp/b",
|
|
415
442
|
agent: "b1",
|
|
416
443
|
results: [res("A", "passed", { agent: "b1" })],
|
|
417
444
|
});
|
|
418
|
-
const data = await
|
|
445
|
+
const data = await MetricScatter.data([withCost, noCost], {
|
|
419
446
|
points: "experiment",
|
|
420
447
|
series: "agent",
|
|
421
448
|
x: costUSD,
|
|
@@ -438,12 +465,59 @@ describe("scatter", () => {
|
|
|
438
465
|
});
|
|
439
466
|
});
|
|
440
467
|
|
|
441
|
-
// ─────────────────────────
|
|
468
|
+
// ───────────────────────── param():维度与轴 ─────────────────────────
|
|
442
469
|
|
|
443
|
-
describe("
|
|
444
|
-
|
|
470
|
+
describe("param()", () => {
|
|
471
|
+
const withParams = (id: string, params: Record<string, unknown> | undefined, outcome: ResultOutcome) =>
|
|
472
|
+
snap({
|
|
473
|
+
experimentId: id,
|
|
474
|
+
results: [res("A", outcome, { experimentId: id, experiment: { id, params } })],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("MetricLine.data:x 收 param、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
|
|
478
|
+
const s1 = withParams("ultra/lat-100", { latencyMs: 100, agents: 1 }, "passed");
|
|
479
|
+
const s2 = withParams("ultra/lat-300", { latencyMs: 300, agents: 1 }, "failed");
|
|
480
|
+
const legacy = withParams("ultra/legacy", undefined, "passed");
|
|
481
|
+
const data = await MetricLine.data([s1, s2, legacy], {
|
|
482
|
+
x: param("latencyMs", { label: "Simulated latency", unit: "ms" }),
|
|
483
|
+
series: param("agents", { label: (v) => `${v} agents` }),
|
|
484
|
+
y: passRate,
|
|
485
|
+
});
|
|
486
|
+
expect(data.x).toEqual({ key: "latencyMs", label: "Simulated latency", unit: "ms" });
|
|
487
|
+
expect(data.series).toBe("agents");
|
|
488
|
+
expect(data.rows).toHaveLength(3);
|
|
489
|
+
|
|
490
|
+
const p100 = data.rows.find((r) => r.key === "ultra/lat-100")!;
|
|
491
|
+
expect(p100.x).toBe(100);
|
|
492
|
+
expect(p100.xDisplay).toBe("100ms");
|
|
493
|
+
expect(p100.series).toBe("1 agents");
|
|
494
|
+
expect(p100.y.value).toBe(1);
|
|
495
|
+
|
|
496
|
+
// 未声明 param 的 experiment 不猜:作轴 x=null(组件不画、注脚报数),分组归 (unset)
|
|
497
|
+
const legacyRow = data.rows.find((r) => r.key === "ultra/legacy")!;
|
|
498
|
+
expect(legacyRow.x).toBeNull();
|
|
499
|
+
expect(legacyRow.xDisplay).toBe("");
|
|
500
|
+
expect(legacyRow.series).toBe("(unset)");
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("param 当维度用:按声明值分组,label 函数折组名", async () => {
|
|
504
|
+
const s1 = withParams("exp/a", { agents: 1 }, "passed");
|
|
505
|
+
const s2 = withParams("exp/b", { agents: 16 }, "failed");
|
|
506
|
+
const data = await MetricTable.data([s1, s2], {
|
|
507
|
+
rows: param("agents", { label: (v) => `${v} agents` }),
|
|
508
|
+
columns: [passRate],
|
|
509
|
+
});
|
|
510
|
+
expect(data.dimension).toBe("agents");
|
|
511
|
+
expect(data.rows.map((r) => r.key)).toEqual(["1 agents", "16 agents"]);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ───────────────────────── RunOverview.data ─────────────────────────
|
|
516
|
+
|
|
517
|
+
describe("RunOverview.data", () => {
|
|
518
|
+
it("costUSD 全缺为 null 不编 0;有实测/估算则求和;Snapshot[] 输入无警告", async () => {
|
|
445
519
|
const bare = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
|
|
446
|
-
const bareOverview = await
|
|
520
|
+
const bareOverview = await RunOverview.data([bare]);
|
|
447
521
|
expect(bareOverview.totals.costUSD).toBeNull();
|
|
448
522
|
expect(bareOverview.totals.attempts).toBe(2);
|
|
449
523
|
expect(bareOverview.totals.passed).toBe(1);
|
|
@@ -457,32 +531,41 @@ describe("overview", () => {
|
|
|
457
531
|
res("B", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } }),
|
|
458
532
|
],
|
|
459
533
|
});
|
|
460
|
-
const data = await
|
|
534
|
+
const data = await RunOverview.data([priced]);
|
|
461
535
|
expect(data.totals.costUSD).toBeCloseTo(0.3);
|
|
462
536
|
expect(data.totals.evals).toBe(2);
|
|
463
|
-
expect(data.warnings).toEqual(["snapshot covers 1 of 50 evals"]);
|
|
464
537
|
expect(data.snapshots).toEqual([
|
|
465
538
|
{ experimentId: "exp/y", agent: "agent-x", model: undefined, startedAt: priced.startedAt },
|
|
466
539
|
]);
|
|
467
540
|
});
|
|
541
|
+
|
|
542
|
+
it("收 Selection 时 warnings 随行进数据 —— 诚实不靠使用者记得透传", async () => {
|
|
543
|
+
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
544
|
+
const warning: SelectionWarning = {
|
|
545
|
+
kind: "partial-coverage",
|
|
546
|
+
experimentId: "exp/x",
|
|
547
|
+
covered: 1,
|
|
548
|
+
total: 50,
|
|
549
|
+
message: "snapshot covers 1 of 50 evals seen in history",
|
|
550
|
+
};
|
|
551
|
+
const data = await RunOverview.data(selection([s], [warning]));
|
|
552
|
+
expect(data.warnings).toEqual([warning]);
|
|
553
|
+
});
|
|
468
554
|
});
|
|
469
555
|
|
|
470
|
-
// ─────────────────────────
|
|
556
|
+
// ───────────────────────── DeltaTable.data ─────────────────────────
|
|
471
557
|
|
|
472
|
-
describe("
|
|
473
|
-
it("任一侧 null → delta null 不硬算;双侧有值给带符号 display", async () => {
|
|
558
|
+
describe("DeltaTable.data", () => {
|
|
559
|
+
it("任一侧 null → delta null 不硬算;双侧有值给带符号 display;Δ=0 是 ±0", async () => {
|
|
474
560
|
const base = snap({
|
|
475
561
|
experimentId: "exp/base",
|
|
476
|
-
results: [
|
|
477
|
-
res("A", "failed"),
|
|
478
|
-
res("B", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } }),
|
|
479
|
-
],
|
|
562
|
+
results: [res("A", "failed"), res("B", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } })],
|
|
480
563
|
});
|
|
481
564
|
const plus = snap({
|
|
482
565
|
experimentId: "exp/plus",
|
|
483
566
|
results: [res("A", "passed"), res("B", "passed")], // 无任何成本数据
|
|
484
567
|
});
|
|
485
|
-
const data = await
|
|
568
|
+
const data = await DeltaTable.data([base, plus], {
|
|
486
569
|
pairs: [{ a: "exp/base", b: "exp/plus", label: "memory" }],
|
|
487
570
|
metrics: [passRate, costUSD],
|
|
488
571
|
});
|
|
@@ -503,12 +586,45 @@ describe("delta", () => {
|
|
|
503
586
|
expect(cost.b.value).toBeNull();
|
|
504
587
|
expect(cost.delta).toBeNull(); // 单侧缺数据:不硬算
|
|
505
588
|
expect(cost.display).toBe("—");
|
|
589
|
+
|
|
590
|
+
const flat = await DeltaTable.data([base, base], {
|
|
591
|
+
pairs: [{ a: "exp/base", b: "exp/base", label: "same" }],
|
|
592
|
+
metrics: [passRate],
|
|
593
|
+
});
|
|
594
|
+
expect(flat.rows[0].cells["pass-rate"].display).toBe("±0");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("时间轴对比:pairs 的 a/b 收快照键 <experimentId> @ <startedAt>", async () => {
|
|
598
|
+
const older = snap({
|
|
599
|
+
experimentId: "exp/x",
|
|
600
|
+
runStartedAt: "2026-07-01T08:00:00Z",
|
|
601
|
+
results: [res("A", "failed")],
|
|
602
|
+
});
|
|
603
|
+
const newer = snap({
|
|
604
|
+
experimentId: "exp/x",
|
|
605
|
+
runStartedAt: "2026-07-02T08:00:00Z",
|
|
606
|
+
results: [res("A", "passed")],
|
|
607
|
+
});
|
|
608
|
+
const data = await DeltaTable.data([older, newer], {
|
|
609
|
+
pairs: [
|
|
610
|
+
{
|
|
611
|
+
a: "exp/x @ 2026-07-01T08:00:00Z",
|
|
612
|
+
b: "exp/x @ 2026-07-02T08:00:00Z",
|
|
613
|
+
label: "this week vs last",
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
metrics: [passRate],
|
|
617
|
+
});
|
|
618
|
+
const cell = data.rows[0].cells["pass-rate"];
|
|
619
|
+
expect(cell.a.value).toBe(0); // 旧快照那份
|
|
620
|
+
expect(cell.b.value).toBe(1); // 新快照那份
|
|
621
|
+
expect(cell.display).toBe("+100%");
|
|
506
622
|
});
|
|
507
623
|
});
|
|
508
624
|
|
|
509
|
-
// ─────────────────────────
|
|
625
|
+
// ───────────────────────── CaseList.data ─────────────────────────
|
|
510
626
|
|
|
511
|
-
describe("
|
|
627
|
+
describe("CaseList.data", () => {
|
|
512
628
|
it("默认只列 failed+errored;redact 作用于 error/detail/evidence;truncated 如实", async () => {
|
|
513
629
|
const s = snap({
|
|
514
630
|
experimentId: "exp/x",
|
|
@@ -532,7 +648,7 @@ describe("cases", () => {
|
|
|
532
648
|
res("E", "skipped"),
|
|
533
649
|
],
|
|
534
650
|
});
|
|
535
|
-
const data = await
|
|
651
|
+
const data = await CaseList.data([s], {
|
|
536
652
|
limit: 2,
|
|
537
653
|
redact: (text) => text.replaceAll("/Users/me/repo", "<repo>"),
|
|
538
654
|
});
|
|
@@ -545,7 +661,7 @@ describe("cases", () => {
|
|
|
545
661
|
expect(first.failedAssertions).toHaveLength(1);
|
|
546
662
|
expect(first.failedAssertions[0].detail).toBe("missing text under <repo>/src");
|
|
547
663
|
expect(first.failedAssertions[0].evidence).toBe("checked <repo>/src/app.ts");
|
|
548
|
-
expect(first.ref
|
|
664
|
+
expect(first.ref.result).toBe(0);
|
|
549
665
|
|
|
550
666
|
expect(second.eval).toBe("B");
|
|
551
667
|
expect(second.outcome).toBe("errored");
|
|
@@ -554,11 +670,8 @@ describe("cases", () => {
|
|
|
554
670
|
});
|
|
555
671
|
|
|
556
672
|
it("outcomes 可收窄;不传 limit 不截断", async () => {
|
|
557
|
-
const s = snap({
|
|
558
|
-
|
|
559
|
-
results: [res("A", "failed"), res("B", "errored")],
|
|
560
|
-
});
|
|
561
|
-
const onlyErrored = await cases([s], { outcomes: ["errored"] });
|
|
673
|
+
const s = snap({ experimentId: "exp/x", results: [res("A", "failed"), res("B", "errored")] });
|
|
674
|
+
const onlyErrored = await CaseList.data([s], { outcomes: ["errored"] });
|
|
562
675
|
expect(onlyErrored.rows.map((r) => r.eval)).toEqual(["B"]);
|
|
563
676
|
expect(onlyErrored.truncated).toBe(0);
|
|
564
677
|
});
|
|
@@ -585,25 +698,29 @@ describe("身份键去重", () => {
|
|
|
585
698
|
results: [res("A", "passed", identity)],
|
|
586
699
|
});
|
|
587
700
|
|
|
588
|
-
for (const order of [
|
|
589
|
-
|
|
701
|
+
for (const order of [
|
|
702
|
+
[older, newer],
|
|
703
|
+
[newer, older],
|
|
704
|
+
]) {
|
|
705
|
+
const data = await MetricTable.data(order, { rows: "agent", columns: [passRate] });
|
|
590
706
|
const cell = data.rows[0].cells["pass-rate"];
|
|
591
707
|
expect(cell.total).toBe(1); // 两份只算一份
|
|
592
708
|
expect(cell.value).toBe(1); // 留的是最新 run 里的 passed,与快照传入顺序无关
|
|
593
709
|
|
|
594
|
-
const ov = await
|
|
710
|
+
const ov = await RunOverview.data(order);
|
|
595
711
|
expect(ov.totals.attempts).toBe(1);
|
|
596
712
|
expect(ov.totals.passed).toBe(1);
|
|
597
713
|
expect(ov.totals.failed).toBe(0);
|
|
598
714
|
}
|
|
599
715
|
});
|
|
600
716
|
|
|
601
|
-
it("startedAt
|
|
717
|
+
it("startedAt 缺失:不去重、如实保留重复,不透出警告", async () => {
|
|
602
718
|
const identity = { experimentId: "exp/x", attempt: 0, startedAt: undefined };
|
|
603
719
|
const one = snap({ experimentId: "exp/x", results: [res("A", "passed", identity)] });
|
|
604
720
|
const two = snap({ experimentId: "exp/x", results: [res("A", "passed", identity)] });
|
|
605
|
-
const ov = await
|
|
721
|
+
const ov = await RunOverview.data([one, two]);
|
|
606
722
|
expect(ov.totals.attempts).toBe(2);
|
|
723
|
+
expect(ov.warnings).toEqual([]); // missing-startedAt 不透出到组件数据(裁决记录 7)
|
|
607
724
|
});
|
|
608
725
|
});
|
|
609
726
|
|
|
@@ -633,14 +750,14 @@ describe("unit 驱动格式化", () => {
|
|
|
633
750
|
value: () => 0.5,
|
|
634
751
|
});
|
|
635
752
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
636
|
-
const data = await
|
|
753
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [raw] });
|
|
637
754
|
expect(data.rows[0].cells["raw"].display).toBe("0.5 raw");
|
|
638
755
|
});
|
|
639
756
|
});
|
|
640
757
|
|
|
641
|
-
// ─────────────────────────
|
|
758
|
+
// ───────────────────────── MetricMatrix.data(= MetricBars.data)─────────────────────────
|
|
642
759
|
|
|
643
|
-
describe("
|
|
760
|
+
describe("MetricMatrix.data", () => {
|
|
644
761
|
it("稀疏:没有 attempt 的 (row, column) 组合不出格", async () => {
|
|
645
762
|
const a = snap({
|
|
646
763
|
experimentId: "exp/a",
|
|
@@ -652,16 +769,19 @@ describe("matrix", () => {
|
|
|
652
769
|
agent: "b1",
|
|
653
770
|
results: [res("A", "failed", { agent: "b1" })], // b1 没跑 B
|
|
654
771
|
});
|
|
655
|
-
const data = await
|
|
772
|
+
const data = await MetricMatrix.data([a, b], { rows: "eval", columns: "agent", cell: passRate });
|
|
656
773
|
expect(data.rows).toBe("eval");
|
|
657
774
|
expect(data.columns).toBe("agent");
|
|
658
775
|
expect(data.metric.key).toBe("pass-rate");
|
|
659
776
|
expect(data.cells).toHaveLength(3); // A×a1、B×a1、A×b1;B×b1 不出现
|
|
660
|
-
const find = (row: string, column: string) =>
|
|
661
|
-
data.cells.find((c) => c.row === row && c.column === column);
|
|
777
|
+
const find = (row: string, column: string) => data.cells.find((c) => c.row === row && c.column === column);
|
|
662
778
|
expect(find("A", "a1")?.cell.value).toBe(1);
|
|
663
779
|
expect(find("B", "a1")?.cell.value).toBe(0);
|
|
664
780
|
expect(find("A", "b1")?.cell.value).toBe(0);
|
|
665
781
|
expect(find("B", "b1")).toBeUndefined();
|
|
666
782
|
});
|
|
783
|
+
|
|
784
|
+
it("MetricBars.data 就是 MetricMatrix.data 的别名(同一个函数)", () => {
|
|
785
|
+
expect(MetricBars.data).toBe(MetricMatrix.data);
|
|
786
|
+
});
|
|
667
787
|
});
|