niceeval 0.6.2 → 0.7.1
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/INDEX.md +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +2 -2
- package/dist/i18n/zh-CN.d.ts +3 -3
- package/dist/report/aggregate.d.ts +32 -26
- package/dist/report/aggregate.js +157 -76
- package/dist/report/built-in/index.d.ts +2 -0
- package/dist/report/built-in/index.js +8 -0
- package/dist/report/components.d.ts +91 -164
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +86 -73
- package/dist/report/compute.js +592 -432
- package/dist/report/flag.d.ts +28 -17
- package/dist/report/flag.js +86 -16
- package/dist/report/format.d.ts +11 -11
- package/dist/report/format.js +17 -15
- package/dist/report/index.d.ts +16 -17
- package/dist/report/index.js +20 -22
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +49 -34
- package/dist/report/locale.js +106 -58
- package/dist/report/metrics.d.ts +10 -3
- package/dist/report/metrics.js +46 -12
- package/dist/report/primitives.d.ts +42 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +10 -8
- package/dist/report/react/AttemptList.js +18 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +3 -3
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +4 -2
- package/dist/report/react/ExperimentComparison.js +5 -4
- package/dist/report/react/ExperimentList.d.ts +3 -3
- package/dist/report/react/ExperimentList.js +16 -15
- package/dist/report/react/MetricBars.js +5 -4
- package/dist/report/react/MetricLine.js +12 -5
- package/dist/report/react/MetricMatrix.js +1 -1
- package/dist/report/react/MetricScatter.js +54 -17
- package/dist/report/react/MetricTable.js +2 -12
- package/dist/report/react/ScopeSummary.d.ts +10 -0
- package/dist/report/react/ScopeSummary.js +17 -0
- package/dist/report/react/Scoreboard.js +6 -6
- package/dist/report/react/cell.js +2 -2
- package/dist/report/react/fixtures.d.ts +5 -9
- package/dist/report/react/fixtures.js +105 -149
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -20
- package/dist/report/report.js +261 -34
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +225 -157
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +2 -2
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +245 -300
- package/dist/report/types.js +4 -3
- package/dist/report/web.d.ts +21 -5
- package/dist/report/web.js +42 -16
- package/dist/results/select.d.ts +38 -16
- package/dist/results/select.js +73 -25
- package/dist/results/types.d.ts +38 -14
- package/dist/shared/aggregate.d.ts +3 -2
- package/dist/shared/aggregate.js +5 -4
- package/docs-site/zh/README.md +44 -0
- package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
- package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
- package/docs-site/zh/examples/index.mdx +50 -0
- package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
- package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
- package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
- package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
- package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
- package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
- package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
- package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
- package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
- package/docs-site/zh/index.mdx +23 -25
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +5 -5
- package/docs-site/zh/reference/capabilities.mdx +6 -6
- package/docs-site/zh/reference/cli.mdx +9 -7
- package/docs-site/zh/reference/define-agent.mdx +1 -1
- package/docs-site/zh/reference/events.mdx +3 -3
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
- package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
- package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
- package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
- package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +6 -2
- package/src/agents/index.ts +2 -2
- package/src/agents/openai-compat.ts +1 -1
- package/src/agents/streaming.ts +2 -2
- package/src/agents/types.ts +3 -3
- package/src/cli.ts +42 -23
- package/src/context/context.ts +1 -1
- package/src/context/session.test.ts +1 -1
- package/src/context/session.ts +1 -1
- package/src/i18n/en.ts +18 -16
- package/src/i18n/zh-CN.ts +16 -15
- package/src/report/aggregate.ts +175 -87
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -285
- package/src/report/compute.ts +717 -515
- package/src/report/dual-render.test.tsx +738 -1148
- package/src/report/flag.ts +97 -33
- package/src/report/format.ts +18 -22
- package/src/report/index.ts +113 -58
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +120 -69
- package/src/report/metrics.ts +42 -12
- package/src/report/primitives.tsx +190 -45
- package/src/report/react/AttemptList.tsx +32 -20
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +12 -7
- package/src/report/react/ExperimentList.tsx +38 -26
- package/src/report/react/MetricBars.tsx +5 -4
- package/src/report/react/MetricLine.tsx +13 -8
- package/src/report/react/MetricMatrix.tsx +2 -2
- package/src/report/react/MetricScatter.tsx +74 -20
- package/src/report/react/MetricTable.tsx +4 -76
- package/src/report/react/ScopeSummary.tsx +86 -0
- package/src/report/react/Scoreboard.tsx +28 -10
- package/src/report/react/cell.tsx +2 -2
- package/src/report/react/enhance.js +57 -5
- package/src/report/react/fixtures.ts +109 -156
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +139 -104
- package/src/report/react/styles.css +181 -91
- package/src/report/report.test.ts +761 -1031
- package/src/report/report.ts +425 -47
- package/src/report/text/faces.ts +257 -164
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +2 -2
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +257 -287
- package/src/report/web.ts +63 -20
- package/src/results/attempt-evidence.test.ts +4 -4
- package/src/results/attempt-evidence.ts +5 -5
- package/src/results/copy.ts +6 -6
- package/src/results/host-equivalence.test.ts +26 -14
- package/src/results/index.ts +10 -4
- package/src/results/open.ts +8 -4
- package/src/results/results.test.ts +4 -3
- package/src/results/select.ts +104 -34
- package/src/results/types.ts +36 -14
- package/src/runner/feedback/human.test.ts +1 -1
- package/src/runner/run.ts +1 -1
- package/src/sandbox/cli-commands.ts +2 -2
- package/src/scoring/judge.test.ts +1 -1
- package/src/shared/aggregate.ts +5 -4
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +107 -56
- package/src/show/render.ts +43 -27
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +86 -36
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +324 -63
- package/src/view/app/components/CodeView.tsx +10 -4
- package/src/view/app/i18n.ts +31 -17
- package/src/view/app/lib/artifact-url.ts +14 -3
- package/src/view/app/main.tsx +13 -8
- package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
- package/src/view/app/types.ts +4 -1
- package/src/view/artifact-serving.test.ts +21 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +6 -6
- package/src/view/data.test.ts +9 -3
- package/src/view/data.ts +145 -49
- package/src/view/index.ts +48 -44
- package/src/view/server.ts +35 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +224 -0
- package/src/view/view-report.test.ts +161 -57
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
- package/dist/report/built-ins/experiment-comparison.js +0 -119
- package/dist/report/built-ins/index.d.ts +0 -2
- package/dist/report/built-ins/index.js +0 -2
- package/dist/report/react/GroupSummary.d.ts +0 -8
- package/dist/report/react/GroupSummary.js +0 -8
- package/dist/report/react/RunOverview.d.ts +0 -8
- package/dist/report/react/RunOverview.js +0 -12
- package/docs-site/zh/example/ai-agent-application.mdx +0 -152
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
- package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
- package/docs-site/zh/example/showcase.mdx +0 -39
- package/src/report/built-in-user-parity.test.tsx +0 -597
- package/src/report/built-ins/experiment-comparison.tsx +0 -179
- package/src/report/built-ins/index.ts +0 -7
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
- /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
|
@@ -1,51 +1,53 @@
|
|
|
1
1
|
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
2
2
|
// niceeval/report 计算层的单元测试:全部用内存 fake(Snapshot / AttemptHandle 按
|
|
3
|
-
// niceeval/results 的读取契约手工构造)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// niceeval/results 的读取契约手工构造)。覆盖登记行:两级聚合 vs 平铺、errored=0 口径、
|
|
4
|
+
// skipped=null、null≠0、Scoreboard 固定分母(notRun/unscorable 分开)、权重最长前缀、
|
|
5
|
+
// 身份键去重、现刻水位、自定义指标 where/aggregate、evalGroup 完整父路径、verdict 权威、
|
|
6
|
+
// MetricCell 诚实、缺 artifact 指标、repeatedFailedCommands、实体列表 failureSummary /
|
|
7
|
+
// redact、scopeSummaryData 两级计票、experimentComparisonData 分区、pairsByFlag、
|
|
8
|
+
// MetricLine 点身份、空数组反馈、metricTableData sort。
|
|
7
9
|
|
|
8
10
|
import { describe, expect, it } from "vitest";
|
|
9
11
|
|
|
10
12
|
import type { AssertionResult, AttemptError, EvalResult, O11ySummary, Verdict } from "../types.ts";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return { code: "unexpected-error", message, phase: "eval.run" };
|
|
15
|
-
}
|
|
16
|
-
import type { AttemptHandle, Selection, SelectionWarning, Snapshot } from "../results/index.ts";
|
|
17
|
-
import type { Dimension, MetricCell } from "./types.ts";
|
|
13
|
+
import type { AttemptHandle, Scope, ScopeWarning, Snapshot } from "../results/index.ts";
|
|
14
|
+
import { makeScope, selectCurrentResults } from "../results/select.ts";
|
|
15
|
+
import type { Results } from "../results/types.ts";
|
|
18
16
|
import {
|
|
17
|
+
assistantTurns,
|
|
19
18
|
costUSD,
|
|
20
19
|
defineMetric,
|
|
21
20
|
durationMs,
|
|
22
21
|
endToEndPassRate,
|
|
23
22
|
examScore,
|
|
24
23
|
executionReliability,
|
|
24
|
+
repeatedFailedCommands,
|
|
25
25
|
taskPassRate,
|
|
26
|
-
tokens,
|
|
27
|
-
turns,
|
|
28
26
|
} from "./metrics.ts";
|
|
29
|
-
import {
|
|
30
|
-
import { formatMetricValue } from "./format.ts";
|
|
27
|
+
import { flag, numericFlag } from "./flag.ts";
|
|
31
28
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} from "./
|
|
45
|
-
import {
|
|
29
|
+
attemptListData,
|
|
30
|
+
deltaTableData,
|
|
31
|
+
evalListData,
|
|
32
|
+
experimentComparisonData,
|
|
33
|
+
experimentListData,
|
|
34
|
+
metricLineData,
|
|
35
|
+
metricMatrixData,
|
|
36
|
+
metricScatterData,
|
|
37
|
+
metricTableData,
|
|
38
|
+
pairsByFlag,
|
|
39
|
+
scopeSummaryData,
|
|
40
|
+
scoreboardData,
|
|
41
|
+
} from "./compute.ts";
|
|
42
|
+
import { evalGroupOf } from "./aggregate.ts";
|
|
46
43
|
|
|
47
44
|
// ───────────────────────── fake 数据(按 results 读取契约造)─────────────────────────
|
|
48
45
|
|
|
46
|
+
/** 结构化 `AttemptError` 的最小构造(测试用)。 */
|
|
47
|
+
function erroredWith(message: string): AttemptError {
|
|
48
|
+
return { code: "unexpected-error", message, phase: "eval.run" };
|
|
49
|
+
}
|
|
50
|
+
|
|
49
51
|
let seq = 0;
|
|
50
52
|
|
|
51
53
|
/** 造一条结果;默认给每条唯一 startedAt —— 身份键含 startedAt,免得普通样本被去重误伤。 */
|
|
@@ -67,10 +69,10 @@ function softAssertion(name: string, score: number, extra: Partial<AssertionResu
|
|
|
67
69
|
return { name, severity: "soft", score, outcome: "passed" as const, ...extra } as AssertionResult;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
/** 最小合规 O11ySummary
|
|
71
|
-
function o11ySummary(
|
|
72
|
+
/** 最小合规 O11ySummary;shellCommands / totalTurns 按需变。 */
|
|
73
|
+
function o11ySummary(partial: Partial<O11ySummary> = {}): O11ySummary {
|
|
72
74
|
return {
|
|
73
|
-
totalTurns,
|
|
75
|
+
totalTurns: 0,
|
|
74
76
|
toolCalls: {},
|
|
75
77
|
totalToolCalls: 0,
|
|
76
78
|
filesRead: [],
|
|
@@ -82,7 +84,8 @@ function o11ySummary(totalTurns: number): O11ySummary {
|
|
|
82
84
|
compactions: 0,
|
|
83
85
|
durationMs: 0,
|
|
84
86
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
85
|
-
|
|
87
|
+
...partial,
|
|
88
|
+
} as O11ySummary;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
interface SnapSpec {
|
|
@@ -92,6 +95,7 @@ interface SnapSpec {
|
|
|
92
95
|
model?: string;
|
|
93
96
|
runStartedAt?: string;
|
|
94
97
|
knownEvalIds?: string[];
|
|
98
|
+
experiment?: Snapshot["experiment"];
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
let runSeq = 0;
|
|
@@ -107,6 +111,7 @@ function snap(spec: SnapSpec): Snapshot {
|
|
|
107
111
|
completedAt: startedAt,
|
|
108
112
|
agent: spec.agent ?? "agent-x",
|
|
109
113
|
model: spec.model,
|
|
114
|
+
experiment: spec.experiment,
|
|
110
115
|
schemaVersion: 1,
|
|
111
116
|
dir,
|
|
112
117
|
knownEvalIds: spec.knownEvalIds,
|
|
@@ -135,1165 +140,890 @@ function snap(spec: SnapSpec): Snapshot {
|
|
|
135
140
|
return snapshot;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
snapshots,
|
|
142
|
-
warnings,
|
|
143
|
-
filter(predicate) {
|
|
144
|
-
const kept = snapshots.filter(predicate);
|
|
145
|
-
const survivors = new Set(kept.map((s) => s.experimentId));
|
|
146
|
-
return selection(
|
|
147
|
-
kept,
|
|
148
|
-
warnings.filter((w) => typeof w.experimentId !== "string" || survivors.has(w.experimentId)),
|
|
149
|
-
);
|
|
150
|
-
},
|
|
151
|
-
};
|
|
143
|
+
function scopeOf(snapshots: Snapshot[], warnings: ScopeWarning[] = []): Scope {
|
|
144
|
+
return makeScope("current-evals", snapshots, warnings);
|
|
152
145
|
}
|
|
153
146
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}),
|
|
169
|
-
res("memory/a", "passed", {
|
|
170
|
-
attempt: 1,
|
|
171
|
-
durationMs: 2_000,
|
|
172
|
-
usage: { inputTokens: 20, outputTokens: 10, costUSD: 0.2 },
|
|
173
|
-
}),
|
|
174
|
-
res("memory/b", "errored", { error: erroredWith("timeout"), durationMs: 3_000 }),
|
|
175
|
-
],
|
|
176
|
-
});
|
|
177
|
-
s.experiment = {
|
|
178
|
-
runs: 2,
|
|
179
|
-
earlyExit: false,
|
|
180
|
-
sandbox: { provider: "e2b", params: { template: "fast" } },
|
|
181
|
-
budget: 2,
|
|
182
|
-
flags: { cache: true },
|
|
183
|
-
selectedEvalIds: [],
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const items = await ExperimentList.data([s]);
|
|
187
|
-
expect(items).toHaveLength(1);
|
|
188
|
-
const item = items[0]!;
|
|
189
|
-
expect(item).toMatchObject({
|
|
190
|
-
experimentId: "group/codex-e2b--mempal",
|
|
191
|
-
agent: "codex",
|
|
192
|
-
model: "gpt-5.4-mini",
|
|
193
|
-
flags: { cache: true },
|
|
194
|
-
verdicts: { passed: 1, failed: 0, errored: 1, skipped: 0 },
|
|
195
|
-
evals: 2,
|
|
196
|
-
attempts: 3,
|
|
197
|
-
lastRunAt: "2026-07-10T22:44:00.000Z",
|
|
198
|
-
});
|
|
199
|
-
// 官方端到端两级聚合:memory/a 题内均值 (0+1)/2=0.5;
|
|
200
|
-
// memory/b errored → 0 → 跨题均值 (0.5+0)/2=0.25
|
|
201
|
-
expect(item.passRate.value).toBeCloseTo(0.25);
|
|
202
|
-
expect(item.cost.samples).toBe(2); // 只有 memory/a 的两次 attempt 报了成本
|
|
203
|
-
|
|
204
|
-
const evalA = item.evalRows.find((e) => e.evalId === "memory/a")!;
|
|
205
|
-
expect(evalA.verdict).toBe("passed"); // 任一轮通过 → 该题通过
|
|
206
|
-
expect(evalA.attempts.map((a) => a.attempt)).toEqual([0, 1]);
|
|
207
|
-
expect(evalA.attempts[0]!.locator).toMatch(/^@1[0-9a-z]{7}$/);
|
|
208
|
-
// 每次 attempt 独立编码,即使同一道题也不撞车
|
|
209
|
-
expect(evalA.attempts[0]!.locator).not.toBe(evalA.attempts[1]!.locator);
|
|
210
|
-
|
|
211
|
-
const evalB = item.evalRows.find((e) => e.evalId === "memory/b")!;
|
|
212
|
-
expect(evalB.verdict).toBe("errored");
|
|
213
|
-
expect(evalB).not.toHaveProperty("reason");
|
|
214
|
-
expect(evalB.attempts[0]!.error?.message).toBe("timeout");
|
|
147
|
+
// ───────────────────────── 指标聚合口径 ─────────────────────────
|
|
148
|
+
|
|
149
|
+
describe("两级聚合口径", () => {
|
|
150
|
+
// 区分力 fixture:题级值 [1, 2/3, 0] → 两级 5/9;attempt 平铺 3/5;
|
|
151
|
+
// 条件任务通过率 5/6(errored 不进分母);「任一轮通过」2/3。四种口径互不相等。
|
|
152
|
+
const discriminating = snap({
|
|
153
|
+
experimentId: "exp/a",
|
|
154
|
+
results: [
|
|
155
|
+
res("e1", "passed"),
|
|
156
|
+
res("e2", "passed", { attempt: 0 }),
|
|
157
|
+
res("e2", "passed", { attempt: 1 }),
|
|
158
|
+
res("e2", "failed", { attempt: 2 }),
|
|
159
|
+
res("e3", "errored", { error: erroredWith("boom") }),
|
|
160
|
+
],
|
|
215
161
|
});
|
|
216
162
|
|
|
217
|
-
it("
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
expect(
|
|
222
|
-
expect(
|
|
163
|
+
it("endToEndPassRate 先题内折叠再跨题折叠:5/9,不是平铺 3/5、条件 5/6 或任一轮 2/3", async () => {
|
|
164
|
+
const data = await scopeSummaryData([discriminating]);
|
|
165
|
+
expect(data.endToEndPassRate.value).toBeCloseTo(5 / 9);
|
|
166
|
+
expect(data.endToEndPassRate.value).not.toBeCloseTo(3 / 5);
|
|
167
|
+
expect(data.endToEndPassRate.value).not.toBeCloseTo(5 / 6);
|
|
168
|
+
expect(data.endToEndPassRate.value).not.toBeCloseTo(2 / 3);
|
|
169
|
+
expect(data.endToEndPassRate.display).toBe("55.6%");
|
|
170
|
+
expect(data.endToEndPassRate.samples).toBe(5);
|
|
171
|
+
expect(data.endToEndPassRate.total).toBe(5);
|
|
223
172
|
});
|
|
224
173
|
|
|
225
|
-
it("
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
res("pass/a", "passed"),
|
|
230
|
-
res("pass/b", "passed"),
|
|
231
|
-
...Array.from({ length: 5 }, (_, i) => res(`error/${i}`, "errored", { error: erroredWith("boom") })),
|
|
232
|
-
],
|
|
233
|
-
});
|
|
234
|
-
const stable = snap({
|
|
235
|
-
experimentId: "exp/stable",
|
|
236
|
-
results: [res("pass", "passed"), res("fail", "failed")],
|
|
174
|
+
it("taskPassRate 排除 errored,只能作为带限定名称的诊断指标:同 fixture 得 5/6", async () => {
|
|
175
|
+
const table = await metricTableData([discriminating], {
|
|
176
|
+
rows: "agent",
|
|
177
|
+
columns: [endToEndPassRate, taskPassRate, executionReliability],
|
|
237
178
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
expect(
|
|
241
|
-
|
|
179
|
+
const cells = table.rows[0]!.cells;
|
|
180
|
+
expect(cells[taskPassRate.name]!.value).toBeCloseTo(5 / 6);
|
|
181
|
+
expect(cells[endToEndPassRate.name]!.value).toBeCloseTo(5 / 9);
|
|
182
|
+
// executionReliability:e1=1、e2=1、e3=0 → 2/3
|
|
183
|
+
expect(cells[executionReliability.name]!.value).toBeCloseTo(2 / 3);
|
|
242
184
|
});
|
|
243
|
-
});
|
|
244
185
|
|
|
245
|
-
|
|
246
|
-
it("每项一个 experimentId + evalId,父项保留判定与题级聚合并展开到含失败事实的 Attempt", async () => {
|
|
186
|
+
it("2 passed + 5 errored 的默认成功率是 2/7,不是 100%", async () => {
|
|
247
187
|
const s = snap({
|
|
248
|
-
experimentId: "
|
|
188
|
+
experimentId: "exp/err",
|
|
249
189
|
results: [
|
|
250
|
-
res("
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.02 },
|
|
254
|
-
assertions: [{ name: 'calledTool("get_weather")', severity: "gate", outcome: "failed" as const, score: 0, expected: "at least one matching call", received: "no tool calls" }],
|
|
255
|
-
}),
|
|
256
|
-
res("weather/brooklyn", "failed", {
|
|
257
|
-
attempt: 1,
|
|
258
|
-
durationMs: 42_000,
|
|
259
|
-
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.06 },
|
|
260
|
-
assertions: [{ name: 'calledTool("get_weather")', severity: "gate", outcome: "failed" as const, score: 0, expected: "at least one matching call", received: "no tool calls" }],
|
|
261
|
-
}),
|
|
190
|
+
res("q1", "passed"),
|
|
191
|
+
res("q2", "passed"),
|
|
192
|
+
...[3, 4, 5, 6, 7].map((n) => res(`q${n}`, "errored", { error: erroredWith("x") })),
|
|
262
193
|
],
|
|
263
194
|
});
|
|
264
|
-
const
|
|
265
|
-
expect(
|
|
266
|
-
const item = items[0]!;
|
|
267
|
-
expect(item.evalId).toBe("weather/brooklyn");
|
|
268
|
-
expect(item.experimentId).toBe("compare/codex");
|
|
269
|
-
expect(item.verdict).toBe("failed");
|
|
270
|
-
expect(item).not.toHaveProperty("reason");
|
|
271
|
-
expect(item.score.value).toBe(0); // examScore: failed → 0
|
|
272
|
-
expect(item.duration.value).toBeCloseTo(41_000); // 平均耗时
|
|
273
|
-
expect(item.cost.value).toBeCloseTo(0.04); // 平均成本
|
|
274
|
-
expect(item.attempts.map((a) => a.attempt)).toEqual([0, 1]);
|
|
275
|
-
const assertion = item.attempts[0]!.assertions[0]!;
|
|
276
|
-
expect(assertion.outcome).toBe("failed");
|
|
277
|
-
if (assertion.outcome !== "failed") throw new Error("expected failed assertion fixture");
|
|
278
|
-
expect(assertion.received).toBe("no tool calls");
|
|
279
|
-
expect(item.attempts[0]!.locator).not.toBe(item.attempts[1]!.locator);
|
|
195
|
+
const data = await scopeSummaryData([s]);
|
|
196
|
+
expect(data.endToEndPassRate.value).toBeCloseTo(2 / 7);
|
|
280
197
|
});
|
|
281
198
|
|
|
282
|
-
it("
|
|
283
|
-
const s1 = snap({ experimentId: "exp/a", results: [res("shared", "passed")] });
|
|
284
|
-
const s2 = snap({ experimentId: "exp/b", results: [res("shared", "failed")] });
|
|
285
|
-
const items = await EvalList.data([s1, s2]);
|
|
286
|
-
expect(items).toHaveLength(2);
|
|
287
|
-
expect(items.map((i) => `${i.experimentId}:${i.verdict}`).sort()).toEqual(["exp/a:passed", "exp/b:failed"]);
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
describe("AttemptList.data", () => {
|
|
292
|
-
it("每项一个 Attempt:身份、判定、assertions、耗时、成本与 locator", async () => {
|
|
199
|
+
it("skipped 对内置指标返回 null:不进有效样本但保留在 total,value 不受影响", async () => {
|
|
293
200
|
const s = snap({
|
|
294
|
-
experimentId: "exp/
|
|
295
|
-
results: [
|
|
296
|
-
res("A", "failed", {
|
|
297
|
-
durationMs: 5_000,
|
|
298
|
-
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.05 },
|
|
299
|
-
assertions: [{ name: "gate", severity: "gate", outcome: "failed" as const, score: 0, detail: "nope" }],
|
|
300
|
-
hasEvents: true,
|
|
301
|
-
hasSources: true,
|
|
302
|
-
}),
|
|
303
|
-
],
|
|
201
|
+
experimentId: "exp/skip",
|
|
202
|
+
results: [res("a", "passed"), res("b", "skipped"), res("c", "failed")],
|
|
304
203
|
});
|
|
305
|
-
const
|
|
306
|
-
expect(
|
|
307
|
-
|
|
308
|
-
expect(
|
|
309
|
-
evalId: "A",
|
|
310
|
-
experimentId: "exp/x",
|
|
311
|
-
attempt: 0,
|
|
312
|
-
agent: "agent-x",
|
|
313
|
-
verdict: "failed",
|
|
314
|
-
durationMs: 5_000,
|
|
315
|
-
costUSD: 0.05,
|
|
316
|
-
});
|
|
317
|
-
expect(item.assertions).toHaveLength(1);
|
|
318
|
-
expect(item.locator).toMatch(/^@1[0-9a-z]{7}$/);
|
|
204
|
+
const data = await scopeSummaryData([s]);
|
|
205
|
+
expect(data.endToEndPassRate.value).toBeCloseTo(0.5); // (1+0)/2,skipped 不稀释
|
|
206
|
+
expect(data.endToEndPassRate.samples).toBe(2);
|
|
207
|
+
expect(data.endToEndPassRate.total).toBe(3);
|
|
319
208
|
});
|
|
320
209
|
|
|
321
|
-
it("
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
stack: "Error: ENOENT /Users/me/repo/tool\n at spawn (/Users/me/repo/src/run.ts:3:1)",
|
|
331
|
-
cause: { name: "SystemError", code: "ENOENT", message: "no such file /Users/me/repo/tool" },
|
|
332
|
-
},
|
|
333
|
-
diagnostics: [
|
|
334
|
-
{
|
|
335
|
-
code: "teardown-failed",
|
|
336
|
-
level: "warning",
|
|
337
|
-
message: "cleanup left /Users/me/repo/tmp behind",
|
|
338
|
-
phase: "sandbox.teardown",
|
|
339
|
-
data: { path: "/Users/me/repo/tmp", attempts: 2 },
|
|
340
|
-
},
|
|
341
|
-
],
|
|
342
|
-
}),
|
|
343
|
-
res("B", "failed", {
|
|
344
|
-
assertions: [
|
|
345
|
-
{
|
|
346
|
-
name: "includes",
|
|
347
|
-
severity: "gate",
|
|
348
|
-
score: 0,
|
|
349
|
-
outcome: "failed" as const,
|
|
350
|
-
detail: "missing text under /Users/me/repo/src",
|
|
351
|
-
evidence: "checked /Users/me/repo/src/app.ts",
|
|
352
|
-
},
|
|
353
|
-
],
|
|
354
|
-
}),
|
|
355
|
-
],
|
|
210
|
+
it("null 表示测不了不参与聚合,0 正常参与:[null, 0, 1] 的 mean 是 0.5 而非 1/3", async () => {
|
|
211
|
+
const values = new Map([
|
|
212
|
+
["a", null],
|
|
213
|
+
["b", 0],
|
|
214
|
+
["c", 1],
|
|
215
|
+
]);
|
|
216
|
+
const metric = defineMetric({
|
|
217
|
+
name: "tri",
|
|
218
|
+
value: (attempt) => values.get(attempt.evalId) ?? null,
|
|
356
219
|
});
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
expect(a.error?.message).toBe("ENOENT <repo>/tool");
|
|
361
|
-
expect(a.error?.stack).toBe("Error: ENOENT <repo>/tool\n at spawn (<repo>/src/run.ts:3:1)");
|
|
362
|
-
expect(a.error?.cause?.message).toBe("no such file <repo>/tool");
|
|
363
|
-
// 分类字段(code/operation/cause.name/cause.code)原样保留,不经钩子
|
|
364
|
-
expect(a.error?.code).toBe("sandbox-provision-failed");
|
|
365
|
-
expect(a.error?.phase).toBe("sandbox.create");
|
|
366
|
-
expect(a.error?.cause?.name).toBe("SystemError");
|
|
367
|
-
expect(a.error?.cause?.code).toBe("ENOENT");
|
|
368
|
-
// diagnostics:message 与 data 的字符串值经钩子;code/operation/level 原样
|
|
369
|
-
expect(a.diagnostics).toHaveLength(1);
|
|
370
|
-
expect(a.diagnostics![0]!.message).toBe("cleanup left <repo>/tmp behind");
|
|
371
|
-
expect(a.diagnostics![0]!.data).toEqual({ path: "<repo>/tmp", attempts: 2 });
|
|
372
|
-
expect(a.diagnostics![0]!.code).toBe("teardown-failed");
|
|
373
|
-
expect(a.diagnostics![0]!.phase).toBe("sandbox.teardown");
|
|
374
|
-
const b = items.find((i) => i.evalId === "B")!;
|
|
375
|
-
const bAssert = b.assertions[0]!;
|
|
376
|
-
expect(bAssert.detail).toBe("missing text under <repo>/src");
|
|
377
|
-
expect(bAssert.outcome !== "unavailable" ? bAssert.evidence : undefined).toBe("checked <repo>/src/app.ts");
|
|
378
|
-
// 身份字段不经 redact
|
|
379
|
-
expect(items.every((i) => i.experimentId === "exp/x")).toBe(true);
|
|
220
|
+
const s = snap({ experimentId: "exp/tri", results: [res("a", "passed"), res("b", "passed"), res("c", "passed")] });
|
|
221
|
+
const table = await metricTableData([s], { rows: "agent", columns: [metric] });
|
|
222
|
+
expect(table.rows[0]!.cells.tri!.value).toBeCloseTo(0.5);
|
|
380
223
|
});
|
|
381
224
|
|
|
382
|
-
it("
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
});
|
|
387
|
-
const
|
|
388
|
-
expect(
|
|
389
|
-
expect(
|
|
390
|
-
expect(items[0]!.assertions).toHaveLength(1);
|
|
225
|
+
it("跨快照计算先按身份键去重:局部补跑重叠快照下 samples 不虚增", async () => {
|
|
226
|
+
const carried = res("dup/a", "passed", { startedAt: "2026-07-01T09:00:00.000Z" });
|
|
227
|
+
const s1 = snap({ experimentId: "exp/dup", results: [carried], runStartedAt: "2026-07-01T09:00:00.000Z" });
|
|
228
|
+
// 携带合入:同一条结果(同身份键)原样出现在更新的快照里
|
|
229
|
+
const s2 = snap({ experimentId: "exp/dup", results: [{ ...carried }], runStartedAt: "2026-07-02T09:00:00.000Z" });
|
|
230
|
+
const data = await scopeSummaryData([s1, s2]);
|
|
231
|
+
expect(data.attempts).toBe(1);
|
|
232
|
+
expect(data.endToEndPassRate.samples).toBe(1);
|
|
391
233
|
});
|
|
392
|
-
});
|
|
393
234
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
describe("两级聚合引擎", () => {
|
|
397
|
-
it("题内先折再跨题平均:A=[1]、B=[0,0,0] → 0.5,不是平铺的 0.25", async () => {
|
|
235
|
+
it("自定义指标:where 是进入计算前的过滤;perEval + acrossEvals 两级分别生效", async () => {
|
|
398
236
|
const s = snap({
|
|
399
|
-
experimentId: "exp/
|
|
400
|
-
results: [
|
|
237
|
+
experimentId: "exp/custom",
|
|
238
|
+
results: [
|
|
239
|
+
res("a", "passed", { durationMs: 100, attempt: 0 }),
|
|
240
|
+
res("a", "passed", { durationMs: 300, attempt: 1 }),
|
|
241
|
+
res("b", "failed", { durationMs: 900 }),
|
|
242
|
+
res("c", "passed", { durationMs: 500 }),
|
|
243
|
+
],
|
|
401
244
|
});
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
245
|
+
const fastest = defineMetric({
|
|
246
|
+
name: "fastest-pass",
|
|
247
|
+
where: (attempt) => attempt.result.verdict === "passed",
|
|
248
|
+
value: (attempt) => attempt.result.durationMs,
|
|
249
|
+
aggregate: { perEval: "min", acrossEvals: "mean" },
|
|
250
|
+
});
|
|
251
|
+
const table = await metricTableData([s], { rows: "agent", columns: [fastest] });
|
|
252
|
+
const cell = table.rows[0]!.cells["fastest-pass"]!;
|
|
253
|
+
// b 被 where 排除;a 题内 min = 100,c = 500 → mean 300(双 mean 会是 (200+500)/2=350,可区分)
|
|
254
|
+
expect(cell.value).toBe(300);
|
|
255
|
+
expect(cell.samples).toBe(3);
|
|
409
256
|
expect(cell.total).toBe(4);
|
|
410
|
-
expect(cell.refs).toHaveLength(4);
|
|
411
|
-
});
|
|
412
257
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
unit: "%",
|
|
418
|
-
value: (a) => (a.result.verdict === "skipped" ? null : a.result.verdict === "passed" ? 1 : 0),
|
|
419
|
-
aggregate: { perEval: "max", across: "mean" },
|
|
258
|
+
const allExcluded = defineMetric({
|
|
259
|
+
name: "none",
|
|
260
|
+
where: () => false,
|
|
261
|
+
value: () => 1,
|
|
420
262
|
});
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
results: [res("A", "failed"), res("A", "failed"), res("B", "failed"), res("B", "passed")],
|
|
424
|
-
});
|
|
425
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, taskPassRate] });
|
|
426
|
-
// A: max(0,0)=0;B: max(0,1)=1 → (0+1)/2
|
|
427
|
-
expect(data.rows[0].cells["pass@k"].value).toBe(0.5);
|
|
428
|
-
// 对照:默认 mean/mean 的 taskPassRate = (0 + 0.5)/2
|
|
429
|
-
expect(data.rows[0].cells["task-pass-rate"].value).toBe(0.25);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it("skipped 是 null:不稀释均值,但计入 total(覆盖率如实)", async () => {
|
|
433
|
-
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "skipped")] });
|
|
434
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
|
|
435
|
-
const cell = data.rows[0].cells["task-pass-rate"];
|
|
436
|
-
expect(cell.value).toBe(1); // B 整桶为 null,不参与 across,不是 0.5
|
|
437
|
-
expect(cell.samples).toBe(1);
|
|
438
|
-
expect(cell.total).toBe(2);
|
|
439
|
-
expect(cell.refs).toHaveLength(1);
|
|
263
|
+
const empty = await metricTableData([s], { rows: "agent", columns: [allExcluded] });
|
|
264
|
+
expect(empty.rows[0]!.cells.none!.value).toBeNull();
|
|
440
265
|
});
|
|
441
266
|
|
|
442
|
-
it("
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
expect(cell.total).toBe(1);
|
|
450
|
-
expect(cell.refs).toEqual([]);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it("where 不满足 → null,不进聚合", async () => {
|
|
454
|
-
const onlyPassed = defineMetric({
|
|
455
|
-
name: "only-passed",
|
|
456
|
-
where: (a) => a.result.verdict === "passed",
|
|
457
|
-
value: () => 5,
|
|
267
|
+
it("evalGroup 按完整父路径分组(无 / 取完整 id),与可比组同一条派生规则", async () => {
|
|
268
|
+
expect(evalGroupOf("a/b/c")).toBe("a/b");
|
|
269
|
+
expect(evalGroupOf("security/sql-injection")).toBe("security");
|
|
270
|
+
expect(evalGroupOf("standalone")).toBe("standalone");
|
|
271
|
+
const s = snap({
|
|
272
|
+
experimentId: "exp/g",
|
|
273
|
+
results: [res("a/b/c", "passed"), res("a/b/d", "failed"), res("solo", "passed")],
|
|
458
274
|
});
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
expect(cell.value).toBe(5);
|
|
463
|
-
expect(cell.samples).toBe(1);
|
|
464
|
-
expect(cell.total).toBe(2);
|
|
275
|
+
const matrix = await metricMatrixData([s], { rows: "evalGroup", columns: "agent", cell: endToEndPassRate });
|
|
276
|
+
const rowKeys = [...new Set(matrix.cells.map((c) => c.row))];
|
|
277
|
+
expect(rowKeys.sort()).toEqual(["a/b", "solo"]);
|
|
465
278
|
});
|
|
466
279
|
|
|
467
|
-
it("
|
|
468
|
-
const byParity: Dimension = {
|
|
469
|
-
name: "parity",
|
|
470
|
-
of: (a) => (a.result.attempt % 2 === 0 ? "even" : "odd"),
|
|
471
|
-
};
|
|
280
|
+
it("报告消费落盘 verdict,不重新判卷:断言与 verdict 矛盾时以 verdict 为准", async () => {
|
|
472
281
|
const s = snap({
|
|
473
|
-
experimentId: "exp/
|
|
282
|
+
experimentId: "exp/v",
|
|
474
283
|
results: [
|
|
475
|
-
res("
|
|
476
|
-
|
|
477
|
-
|
|
284
|
+
res("a", "failed", {
|
|
285
|
+
// 断言看起来全过,但 verdict 是 failed(如 --strict 翻案):按 verdict 记 0
|
|
286
|
+
assertions: [softAssertion("s", 1)],
|
|
287
|
+
}),
|
|
478
288
|
],
|
|
479
289
|
});
|
|
480
|
-
const data = await
|
|
481
|
-
expect(data.
|
|
482
|
-
const byKey = Object.fromEntries(data.rows.map((r) => [r.key, r.cells["task-pass-rate"].value]));
|
|
483
|
-
// 同一道题的 attempt 分进两组:even 组内 [1,1] 折成 1,odd 组内 [0] 折成 0
|
|
484
|
-
expect(byKey).toEqual({ even: 1, odd: 0 });
|
|
290
|
+
const data = await scopeSummaryData([s]);
|
|
291
|
+
expect(data.endToEndPassRate.value).toBe(0);
|
|
485
292
|
});
|
|
293
|
+
});
|
|
486
294
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
295
|
+
describe("宿主现刻水位(selectCurrentResults)", () => {
|
|
296
|
+
function resultsOf(snapshots: Snapshot[]): Results {
|
|
297
|
+
const byId = new Map<string, Snapshot[]>();
|
|
298
|
+
for (const s of snapshots) byId.set(s.experimentId, [...(byId.get(s.experimentId) ?? []), s]);
|
|
299
|
+
const experiments = [...byId.entries()].map(([id, snaps]) => {
|
|
300
|
+
const sorted = [...snaps].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
301
|
+
const evalIds = [...new Set(sorted.flatMap((s) => s.evals.map((e) => e.id)))].sort();
|
|
302
|
+
return { id, snapshots: sorted, latest: sorted[0]!, evalIds };
|
|
303
|
+
});
|
|
304
|
+
const results = {
|
|
305
|
+
experiments,
|
|
306
|
+
skipped: [],
|
|
307
|
+
latest: () => scopeOf(experiments.map((e) => e.latest)),
|
|
308
|
+
current: () => selectCurrentResults(results),
|
|
309
|
+
} as unknown as Results;
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
it("每个 experiment × eval 取跨历史最新判定:先 failed 后 passed 只用最新", async () => {
|
|
314
|
+
const older = snap({
|
|
315
|
+
experimentId: "exp/w",
|
|
316
|
+
results: [res("a", "failed"), res("b", "passed")],
|
|
317
|
+
runStartedAt: "2026-07-01T00:00:00.000Z",
|
|
318
|
+
});
|
|
319
|
+
const newer = snap({
|
|
320
|
+
experimentId: "exp/w",
|
|
321
|
+
results: [res("a", "passed")],
|
|
322
|
+
runStartedAt: "2026-07-02T00:00:00.000Z",
|
|
323
|
+
});
|
|
324
|
+
const scope = selectCurrentResults(resultsOf([older, newer]));
|
|
325
|
+
expect(scope.mode).toBe("current-evals");
|
|
326
|
+
const data = await scopeSummaryData(scope);
|
|
327
|
+
expect(data.evals).toBe(2);
|
|
328
|
+
expect(data.evalVerdicts).toEqual({ passed: 2, failed: 0, errored: 0, skipped: 0 });
|
|
493
329
|
});
|
|
494
330
|
|
|
495
|
-
it("
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
331
|
+
it("可比性前提:配置不一致的旧快照不贡献 attempt,缺口走 partial-coverage", () => {
|
|
332
|
+
const oldConfig = snap({
|
|
333
|
+
experimentId: "exp/cfg",
|
|
334
|
+
model: "gpt-old",
|
|
335
|
+
results: [res("a", "passed"), res("b", "passed")],
|
|
336
|
+
runStartedAt: "2026-07-01T00:00:00.000Z",
|
|
337
|
+
});
|
|
338
|
+
const newConfig = snap({
|
|
339
|
+
experimentId: "exp/cfg",
|
|
340
|
+
model: "gpt-new",
|
|
341
|
+
results: [res("a", "failed")],
|
|
342
|
+
runStartedAt: "2026-07-02T00:00:00.000Z",
|
|
503
343
|
});
|
|
504
|
-
|
|
344
|
+
const scope = selectCurrentResults(resultsOf([oldConfig, newConfig]));
|
|
345
|
+
// 旧 model 的 b 不冒充新配置的水位
|
|
346
|
+
expect(scope.snapshots[0]!.evals.map((e) => e.id)).toEqual(["a"]);
|
|
347
|
+
const warning = scope.warnings.find((w) => w.kind === "partial-coverage");
|
|
348
|
+
expect(warning).toMatchObject({ covered: 1, total: 2, command: "niceeval exp exp/cfg" });
|
|
505
349
|
});
|
|
506
350
|
|
|
507
|
-
it("
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
351
|
+
it("编排字段(runs / maxConcurrency / description…)不参与可比性比较", () => {
|
|
352
|
+
const older = snap({
|
|
353
|
+
experimentId: "exp/orch",
|
|
354
|
+
results: [res("a", "passed"), res("b", "passed")],
|
|
355
|
+
runStartedAt: "2026-07-01T00:00:00.000Z",
|
|
356
|
+
experiment: { runs: 3, earlyExit: true, maxConcurrency: 2, selectedEvalIds: ["a", "b"], description: "old" },
|
|
357
|
+
});
|
|
358
|
+
const newer = snap({
|
|
359
|
+
experimentId: "exp/orch",
|
|
360
|
+
results: [res("a", "failed")],
|
|
361
|
+
runStartedAt: "2026-07-02T00:00:00.000Z",
|
|
362
|
+
experiment: { runs: 1, earlyExit: false, selectedEvalIds: ["a"], description: "new" },
|
|
363
|
+
});
|
|
364
|
+
const scope = selectCurrentResults(resultsOf([older, newer]));
|
|
365
|
+
expect(scope.snapshots[0]!.evals.map((e) => e.id)).toEqual(["a", "b"]);
|
|
366
|
+
expect(scope.warnings.filter((w) => w.kind === "partial-coverage")).toHaveLength(0);
|
|
514
367
|
});
|
|
515
368
|
});
|
|
516
369
|
|
|
517
|
-
// ─────────────────────────
|
|
370
|
+
// ───────────────────────── MetricCell 与缺数据行为 ─────────────────────────
|
|
518
371
|
|
|
519
|
-
describe("
|
|
520
|
-
it("
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
372
|
+
describe("MetricCell 诚实契约", () => {
|
|
373
|
+
it("measuredZero / partial / missing 三种格子互不混淆;refs 序列化后不丢", async () => {
|
|
374
|
+
const zero = snap({ experimentId: "exp/zero", results: [res("a", "failed")] });
|
|
375
|
+
const partial = snap({ experimentId: "exp/partial", results: [res("a", "passed"), res("b", "skipped")] });
|
|
376
|
+
const missing = snap({ experimentId: "exp/missing", results: [res("a", "skipped")] });
|
|
377
|
+
const table = await metricTableData([zero, partial, missing], {
|
|
378
|
+
rows: "experiment",
|
|
379
|
+
columns: [endToEndPassRate],
|
|
524
380
|
});
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
expect(
|
|
528
|
-
expect(
|
|
381
|
+
const cellOf = (key: string) => table.rows.find((r) => r.key === key)!.cells[endToEndPassRate.name]!;
|
|
382
|
+
expect(cellOf("exp/zero")).toMatchObject({ value: 0, samples: 1, total: 1 });
|
|
383
|
+
expect(cellOf("exp/partial")).toMatchObject({ value: 1, samples: 1, total: 2 });
|
|
384
|
+
expect(cellOf("exp/missing")).toMatchObject({ value: null, samples: 0, total: 1 });
|
|
385
|
+
// 覆盖率与 refs 不因 JSON 序列化丢失;refs 跟随覆盖范围(含 null 值的 attempt)
|
|
386
|
+
const roundTrip = JSON.parse(JSON.stringify(table)) as typeof table;
|
|
387
|
+
expect(roundTrip.rows.find((r) => r.key === "exp/partial")!.cells[endToEndPassRate.name]!.refs).toHaveLength(2);
|
|
388
|
+
expect(cellOf("exp/missing").refs).toHaveLength(1);
|
|
529
389
|
});
|
|
530
390
|
|
|
531
|
-
it("
|
|
532
|
-
const s = snap({
|
|
533
|
-
|
|
534
|
-
|
|
391
|
+
it("缺 o11y.json 时 assistantTurns / repeatedFailedCommands 为 missing;result.json 指标不受影响", async () => {
|
|
392
|
+
const s = snap({ experimentId: "exp/noo11y", results: [res("a", "passed", { durationMs: 1234 })] });
|
|
393
|
+
const table = await metricTableData([s], {
|
|
394
|
+
rows: "agent",
|
|
395
|
+
columns: [assistantTurns, repeatedFailedCommands, durationMs],
|
|
535
396
|
});
|
|
536
|
-
const
|
|
537
|
-
expect(
|
|
397
|
+
const cells = table.rows[0]!.cells;
|
|
398
|
+
expect(cells[assistantTurns.name]!.value).toBeNull();
|
|
399
|
+
expect(cells[repeatedFailedCommands.name]!.value).toBeNull();
|
|
400
|
+
expect(cells[durationMs.name]!.value).toBe(1234);
|
|
538
401
|
});
|
|
539
402
|
|
|
540
|
-
it("
|
|
541
|
-
const
|
|
542
|
-
experimentId: "exp/
|
|
403
|
+
it("repeatedFailedCommands:同命令失败 3 次记 2;两条不同命令各失败 1 次记 0", async () => {
|
|
404
|
+
const repeat = snap({
|
|
405
|
+
experimentId: "exp/repeat",
|
|
543
406
|
results: [
|
|
544
|
-
res("
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
407
|
+
res("a", "failed", {
|
|
408
|
+
o11y: o11ySummary({
|
|
409
|
+
shellCommands: [
|
|
410
|
+
{ command: "pnpm test", success: false },
|
|
411
|
+
{ command: "pnpm test", success: false },
|
|
412
|
+
{ command: "pnpm test", success: false },
|
|
413
|
+
{ command: "ls", success: true },
|
|
414
|
+
],
|
|
415
|
+
}),
|
|
550
416
|
}),
|
|
551
417
|
],
|
|
552
418
|
});
|
|
553
|
-
const
|
|
554
|
-
experimentId: "exp/
|
|
419
|
+
const distinct = snap({
|
|
420
|
+
experimentId: "exp/distinct",
|
|
555
421
|
results: [
|
|
556
|
-
res("
|
|
557
|
-
|
|
422
|
+
res("a", "failed", {
|
|
423
|
+
o11y: o11ySummary({
|
|
424
|
+
shellCommands: [
|
|
425
|
+
{ command: "pnpm test", success: false },
|
|
426
|
+
{ command: "pnpm build", success: false },
|
|
427
|
+
],
|
|
428
|
+
}),
|
|
558
429
|
}),
|
|
559
430
|
],
|
|
560
431
|
});
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
|
|
569
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
|
|
570
|
-
expect(data.rows[0].cells["exam-score"].value).toBeNull();
|
|
432
|
+
const table = await metricTableData([repeat, distinct], {
|
|
433
|
+
rows: "experiment",
|
|
434
|
+
columns: [repeatedFailedCommands],
|
|
435
|
+
});
|
|
436
|
+
const cellOf = (key: string) => table.rows.find((r) => r.key === key)!.cells[repeatedFailedCommands.name]!;
|
|
437
|
+
expect(cellOf("exp/repeat").value).toBe(2);
|
|
438
|
+
expect(cellOf("exp/distinct").value).toBe(0);
|
|
571
439
|
});
|
|
572
|
-
});
|
|
573
440
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
// 任何一处偷懒复用另一个公式都会在这里露馅:
|
|
578
|
-
// taskPassRate:errored → null 不进分母 → (1 + 0) / 2 = 50%
|
|
579
|
-
// executionReliability:errored → 0,passed/failed → 1 → (1 + 1 + 0) / 3 ≈ 66.7%
|
|
580
|
-
// endToEndPassRate:failed/errored → 0 → (1 + 0 + 0) / 3 ≈ 33.3%
|
|
581
|
-
|
|
582
|
-
describe("taskPassRate / executionReliability / endToEndPassRate", () => {
|
|
583
|
-
const mixed = () =>
|
|
584
|
-
snap({
|
|
585
|
-
experimentId: "exp/mixed",
|
|
441
|
+
it("value 与 display 分别可断言;display 由 unit 或自定义 display(value, locale) 驱动", async () => {
|
|
442
|
+
const s = snap({
|
|
443
|
+
experimentId: "exp/display",
|
|
586
444
|
results: [
|
|
587
|
-
|
|
588
|
-
res("
|
|
589
|
-
res("C", "errored", { error: erroredWith("adapter crashed") }),
|
|
590
|
-
res("D", "skipped"),
|
|
445
|
+
...[1, 1, 1, 1, 1].map((_, i) => res(`q${i}`, "passed")),
|
|
446
|
+
res("q5", "failed"),
|
|
591
447
|
],
|
|
592
448
|
});
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
const cell = data.rows[0].cells["execution-reliability"];
|
|
605
|
-
expect(cell.value).toBeCloseTo(2 / 3, 10);
|
|
606
|
-
expect(cell.samples).toBe(3); // errored 测得了(= 0),skipped 测不了
|
|
607
|
-
expect(cell.total).toBe(4);
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
it("endToEndPassRate:failed / errored 都记 0 —— 端到端合成,哪边拖累用前两个拆开看", async () => {
|
|
611
|
-
const data = await MetricTable.data([mixed()], { rows: "agent", columns: [endToEndPassRate] });
|
|
612
|
-
const cell = data.rows[0].cells["end-to-end-pass-rate"];
|
|
613
|
-
expect(cell.value).toBeCloseTo(1 / 3, 10);
|
|
614
|
-
expect(cell.samples).toBe(3);
|
|
615
|
-
expect(cell.total).toBe(4);
|
|
449
|
+
const data = await scopeSummaryData([s]);
|
|
450
|
+
expect(data.endToEndPassRate.value).toBeCloseTo(5 / 6);
|
|
451
|
+
expect(data.endToEndPassRate.display).toBe("83.3%");
|
|
452
|
+
|
|
453
|
+
const localized = defineMetric({
|
|
454
|
+
name: "loc",
|
|
455
|
+
value: () => 1,
|
|
456
|
+
display: (value, locale) => (locale === "zh-CN" ? `${value} 个` : `${value} item`),
|
|
457
|
+
});
|
|
458
|
+
const table = await metricTableData([s], { rows: "agent", columns: [localized] });
|
|
459
|
+
expect(table.rows[0]!.cells.loc!.display).toEqual({ en: "1 item", "zh-CN": "1 个" });
|
|
616
460
|
});
|
|
617
461
|
|
|
618
|
-
it("
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
462
|
+
it("value() 抛错时整个计算失败,错误带 metric name 与 attempt locator,不伪装成测不了", async () => {
|
|
463
|
+
const bad = defineMetric({
|
|
464
|
+
name: "explode",
|
|
465
|
+
value: () => {
|
|
466
|
+
throw new Error("boom");
|
|
467
|
+
},
|
|
622
468
|
});
|
|
623
|
-
const
|
|
624
|
-
expect(
|
|
625
|
-
const reliability = await MetricTable.data([s], { rows: "agent", columns: [executionReliability] });
|
|
626
|
-
expect(reliability.rows[0].cells["execution-reliability"].value).toBe(0);
|
|
469
|
+
const s = snap({ experimentId: "exp/bad", results: [res("a", "passed")] });
|
|
470
|
+
await expect(metricTableData([s], { rows: "agent", columns: [bad] })).rejects.toThrow(/explode.*boom/s);
|
|
627
471
|
});
|
|
628
472
|
});
|
|
629
473
|
|
|
630
|
-
// ─────────────────────────
|
|
474
|
+
// ───────────────────────── Scoreboard ─────────────────────────
|
|
631
475
|
|
|
632
|
-
describe("
|
|
633
|
-
it("
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
res("A", "passed", {
|
|
638
|
-
usage: { inputTokens: 1000, outputTokens: 200, cacheReadTokens: 999_999, cacheWriteTokens: 888 },
|
|
639
|
-
}),
|
|
640
|
-
res("B", "failed"), // 无 usage → null,不稀释
|
|
641
|
-
],
|
|
476
|
+
describe("scoreboardData", () => {
|
|
477
|
+
it("固定题集分母:未跑题按 0 分计入 notRun;跑了但 null 的题计入 unscorable,两个计数不合并", async () => {
|
|
478
|
+
const nullScore = defineMetric({
|
|
479
|
+
name: "maybe-score",
|
|
480
|
+
value: (attempt) => (attempt.evalId === "s/unscorable" ? null : attempt.result.verdict === "passed" ? 1 : 0),
|
|
642
481
|
});
|
|
643
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [tokens] });
|
|
644
|
-
const cell = data.rows[0].cells["tokens"];
|
|
645
|
-
expect(cell.value).toBe(1200);
|
|
646
|
-
expect(cell.display).toBe("1.2k tokens");
|
|
647
|
-
expect(cell.samples).toBe(1);
|
|
648
|
-
expect(cell.total).toBe(2);
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
it("costUSD:网关实测优先于估算;durationMs 对 errored 取实测", async () => {
|
|
652
482
|
const s = snap({
|
|
653
|
-
experimentId: "exp/
|
|
654
|
-
results: [
|
|
655
|
-
res("A", "passed", {
|
|
656
|
-
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.5 },
|
|
657
|
-
estimatedCostUSD: 999, // 实测优先,不该被用到
|
|
658
|
-
}),
|
|
659
|
-
res("B", "errored", { durationMs: 3000 }),
|
|
660
|
-
],
|
|
483
|
+
experimentId: "exp/board",
|
|
484
|
+
results: [res("s/ran", "passed"), res("s/unscorable", "passed")],
|
|
661
485
|
});
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
it("turns:读 o11y.totalTurns;o11y 缺失(未随发布带上)→ null,不是 0;skipped 不进聚合", async () => {
|
|
669
|
-
const s = snap({
|
|
670
|
-
experimentId: "exp/x",
|
|
671
|
-
results: [
|
|
672
|
-
res("A", "passed", { o11y: o11ySummary(12) }),
|
|
673
|
-
res("B", "failed"), // 没带 o11y(如 copySnapshots 漏选 artifact)→ null,不稀释成 0
|
|
674
|
-
res("C", "skipped", { o11y: o11ySummary(3) }), // skipped 恒 null,哪怕 o11y 在场
|
|
675
|
-
],
|
|
486
|
+
const data = await scoreboardData([s], {
|
|
487
|
+
rows: "agent",
|
|
488
|
+
questions: ["s/ran", "s/unscorable", "s/never-1", "s/never-2"],
|
|
489
|
+
score: nullScore,
|
|
676
490
|
});
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
expect(
|
|
680
|
-
expect(
|
|
681
|
-
expect(
|
|
491
|
+
const row = data.rows[0]!;
|
|
492
|
+
// 分母恒 4:1 分挣到 1(s/ran),其余 0 → 100 × 1/4 = 25
|
|
493
|
+
expect(row.total.value).toBe(25);
|
|
494
|
+
expect(row.total.notRun).toBe(2);
|
|
495
|
+
expect(row.total.unscorable).toBe(1);
|
|
496
|
+
expect(row.subjects[0]!.questions).toBe(4);
|
|
682
497
|
});
|
|
683
|
-
});
|
|
684
498
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const alpha = snap({
|
|
690
|
-
experimentId: "exp/alpha",
|
|
691
|
-
agent: "alpha",
|
|
692
|
-
results: [
|
|
693
|
-
res("algebra/x", "passed", { agent: "alpha" }),
|
|
694
|
-
res("algebra/hard/y", "passed", { agent: "alpha" }),
|
|
695
|
-
res("geometry/z", "passed", { agent: "alpha" }),
|
|
696
|
-
],
|
|
697
|
-
});
|
|
698
|
-
const beta = snap({
|
|
699
|
-
experimentId: "exp/beta",
|
|
700
|
-
agent: "beta",
|
|
701
|
-
results: [res("algebra/x", "passed", { agent: "beta" })],
|
|
499
|
+
it("权重按最长前缀命中;无命中默认 1;总分 fullMarks × earned / possible", async () => {
|
|
500
|
+
const s = snap({
|
|
501
|
+
experimentId: "exp/w",
|
|
502
|
+
results: [res("security/auth/a", "passed"), res("security/b", "failed"), res("misc", "passed")],
|
|
702
503
|
});
|
|
703
|
-
const
|
|
504
|
+
const data = await scoreboardData([s], {
|
|
704
505
|
rows: "agent",
|
|
705
|
-
|
|
706
|
-
weights: { "
|
|
506
|
+
questions: ["security/auth/a", "security/b", "misc"],
|
|
507
|
+
weights: { "security/": 2, "security/auth/": 4 },
|
|
707
508
|
fullMarks: 100,
|
|
708
509
|
});
|
|
709
|
-
|
|
710
|
-
//
|
|
711
|
-
expect(
|
|
712
|
-
|
|
713
|
-
{ prefix: "algebra/", weight: 3 },
|
|
714
|
-
]);
|
|
715
|
-
|
|
716
|
-
const alphaRow = board.rows.find((r) => r.key === "alpha")!;
|
|
717
|
-
const betaRow = board.rows.find((r) => r.key === "beta")!;
|
|
718
|
-
|
|
719
|
-
// Σ分值 = 3(algebra/x)+ 9(algebra/hard/y,最长前缀 9 不是 3)+ 1(geometry/z,默认)= 13
|
|
720
|
-
expect(alphaRow.total.value).toBeCloseTo(100);
|
|
721
|
-
// beta 只答了 algebra/x:总分 = 100 × 3/13 ≈ 23.1。
|
|
722
|
-
// 若最长前缀错配成 "algebra/" 的 3,分母变 7,会得 100 × 3/7 ≈ 42.9 —— 钉死歧义。
|
|
723
|
-
expect(betaRow.total.value).toBeCloseTo((100 * 3) / 13);
|
|
724
|
-
|
|
725
|
-
// 固定分母:beta 没跑的题留在分母,missing 如实报在科目行
|
|
726
|
-
const betaAlgebra = betaRow.subjects.find((s) => s.key === "algebra")!;
|
|
727
|
-
expect(betaAlgebra.possible).toBe(12); // 3 + 9
|
|
728
|
-
expect(betaAlgebra.earned).toBe(3);
|
|
729
|
-
expect(betaAlgebra.evals).toBe(2);
|
|
730
|
-
expect(betaAlgebra.missing).toBe(1);
|
|
731
|
-
const betaGeometry = betaRow.subjects.find((s) => s.key === "geometry")!;
|
|
732
|
-
expect(betaGeometry.possible).toBe(1);
|
|
733
|
-
expect(betaGeometry.earned).toBe(0);
|
|
734
|
-
expect(betaGeometry.missing).toBe(1);
|
|
735
|
-
|
|
736
|
-
const alphaAlgebra = alphaRow.subjects.find((s) => s.key === "algebra")!;
|
|
737
|
-
expect(alphaAlgebra.earned).toBe(12);
|
|
738
|
-
expect(alphaAlgebra.missing).toBe(0);
|
|
510
|
+
const row = data.rows[0]!;
|
|
511
|
+
// earned = 4(auth/a) + 0 + 1(misc) = 5;possible = 4 + 2 + 1 = 7
|
|
512
|
+
expect(row.total.value).toBeCloseTo((100 * 5) / 7);
|
|
513
|
+
expect(data.weights[0]).toEqual({ prefix: "security/auth/", weight: 4 }); // 最长前缀在前
|
|
739
514
|
});
|
|
740
515
|
|
|
741
|
-
it("
|
|
742
|
-
const
|
|
743
|
-
experimentId: "exp/
|
|
744
|
-
|
|
745
|
-
results: [
|
|
746
|
-
res("algebra/x", "passed", { agent: "solo", assertions: [softAssertion("judge", 0.5)] }),
|
|
747
|
-
res("algebra/y", "skipped", { agent: "solo" }), // 无有效样本 → missing(按 0 计但如实标注)
|
|
748
|
-
],
|
|
516
|
+
it("subject 缺省与 evalGroup 同一条规则(完整父路径);题集外的 eval 忽略并计入 ignoredEvals", async () => {
|
|
517
|
+
const s = snap({
|
|
518
|
+
experimentId: "exp/subject",
|
|
519
|
+
results: [res("a/b/c", "passed"), res("outside", "passed")],
|
|
749
520
|
});
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
expect(row.total.value).toBeCloseTo(25);
|
|
754
|
-
expect(row.subjects[0].missing).toBe(1);
|
|
755
|
-
expect(row.subjects[0].evals).toBe(2);
|
|
521
|
+
const data = await scoreboardData([s], { rows: "agent", questions: ["a/b/c"] });
|
|
522
|
+
expect(data.rows[0]!.subjects[0]!.key).toBe("a/b");
|
|
523
|
+
expect(data.ignoredEvals).toBe(1);
|
|
756
524
|
});
|
|
757
525
|
|
|
758
|
-
it("
|
|
759
|
-
const s = snap({
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
expect(
|
|
767
|
-
|
|
768
|
-
|
|
526
|
+
it("questions 空数组 / 重复、非法权重、fullMarks<=0、score 出界、subject 空串都按完整用户反馈失败", async () => {
|
|
527
|
+
const s = snap({ experimentId: "exp/e", results: [res("a", "passed")] });
|
|
528
|
+
await expect(scoreboardData([s], { rows: "agent", questions: [] })).rejects.toThrow(/non-empty/);
|
|
529
|
+
await expect(scoreboardData([s], { rows: "agent", questions: ["a", "a"] })).rejects.toThrow(/twice/);
|
|
530
|
+
await expect(
|
|
531
|
+
scoreboardData([s], { rows: "agent", questions: ["a"], weights: { a: 0 } }),
|
|
532
|
+
).rejects.toThrow(/positive finite/);
|
|
533
|
+
await expect(scoreboardData([s], { rows: "agent", questions: ["a"], fullMarks: 0 })).rejects.toThrow(/fullMarks/);
|
|
534
|
+
await expect(
|
|
535
|
+
scoreboardData([s], { rows: "agent", questions: ["a"], score: defineMetric({ name: "big", value: () => 2 }) }),
|
|
536
|
+
).rejects.toThrow(/\[0, 1\]/);
|
|
537
|
+
await expect(
|
|
538
|
+
scoreboardData([s], { rows: "agent", questions: ["a"], subject: () => "" }),
|
|
539
|
+
).rejects.toThrow(/empty/);
|
|
769
540
|
});
|
|
770
541
|
});
|
|
771
542
|
|
|
772
|
-
// ─────────────────────────
|
|
773
|
-
|
|
774
|
-
describe("
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
expect(
|
|
543
|
+
// ───────────────────────── 实体列表 ─────────────────────────
|
|
544
|
+
|
|
545
|
+
describe("实体列表 data", () => {
|
|
546
|
+
const failed = res("list/failed", "failed", {
|
|
547
|
+
assertions: [
|
|
548
|
+
{
|
|
549
|
+
name: "equals",
|
|
550
|
+
severity: "gate",
|
|
551
|
+
outcome: "failed" as const,
|
|
552
|
+
score: 0,
|
|
553
|
+
detail: "equals(42)",
|
|
554
|
+
expected: "42",
|
|
555
|
+
received: "41",
|
|
556
|
+
},
|
|
557
|
+
{ name: "second", severity: "gate", outcome: "failed" as const, score: 0, detail: "second-check" },
|
|
558
|
+
] as AssertionResult[],
|
|
559
|
+
usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.1 },
|
|
560
|
+
});
|
|
561
|
+
const errored = res("list/errored", "errored", {
|
|
562
|
+
error: {
|
|
563
|
+
code: "sandbox-create-failed",
|
|
564
|
+
message: "docker daemon unreachable",
|
|
565
|
+
phase: "sandbox.create",
|
|
566
|
+
stack: "Error: docker daemon unreachable\n at boot (sandbox.ts:10:3)",
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
const passed = res("list/passed", "passed");
|
|
570
|
+
const skipped = res("list/skipped", "skipped");
|
|
571
|
+
const listSnap = () => snap({ experimentId: "exp/list", results: [failed, errored, passed, skipped] });
|
|
572
|
+
|
|
573
|
+
it("failureSummary 三态:failed 取主失败断言摘要、errored 取 error 一层摘要(phase · code · message)、passed/skipped 为 null", async () => {
|
|
574
|
+
const items = await attemptListData([listSnap()]);
|
|
575
|
+
const byEval = new Map(items.map((item) => [item.evalId, item]));
|
|
576
|
+
expect(byEval.get("list/failed")!.failureSummary).toContain("equals(42)");
|
|
577
|
+
expect(byEval.get("list/failed")!.failureSummary).toContain("received 41");
|
|
578
|
+
expect(byEval.get("list/failed")!.moreFailures).toBe(1);
|
|
579
|
+
expect(byEval.get("list/errored")!.failureSummary).toBe(
|
|
580
|
+
"sandbox.create · sandbox-create-failed · docker daemon unreachable",
|
|
581
|
+
);
|
|
582
|
+
expect(byEval.get("list/passed")!.failureSummary).toBeNull();
|
|
583
|
+
expect(byEval.get("list/skipped")!.failureSummary).toBeNull();
|
|
806
584
|
});
|
|
807
|
-
});
|
|
808
585
|
|
|
809
|
-
|
|
586
|
+
it("序列化 JSON 不含第二条断言文本、stack、evidence 或 diagnostics;costUSD 缺失一律 null", async () => {
|
|
587
|
+
const items = await attemptListData([listSnap()]);
|
|
588
|
+
const json = JSON.stringify(items);
|
|
589
|
+
expect(json).not.toContain("second-check");
|
|
590
|
+
expect(json).not.toContain("sandbox.ts:10:3");
|
|
591
|
+
expect(json).not.toContain('"assertions"');
|
|
592
|
+
expect(json).not.toContain('"diagnostics"');
|
|
593
|
+
const byEval = new Map(items.map((item) => [item.evalId, item]));
|
|
594
|
+
expect(byEval.get("list/failed")!.costUSD).toBe(0.1);
|
|
595
|
+
expect(byEval.get("list/errored")!.costUSD).toBeNull();
|
|
596
|
+
});
|
|
810
597
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
598
|
+
it("redact 只改写 failureSummary(含嵌套 attempt 条目);身份字段、locator 与数值指标原样", async () => {
|
|
599
|
+
const redact = (text: string) => text.replaceAll("41", "[redacted]");
|
|
600
|
+
const attempts = await attemptListData([listSnap()], { redact });
|
|
601
|
+
const failedItem = attempts.find((item) => item.evalId === "list/failed")!;
|
|
602
|
+
expect(failedItem.failureSummary).toContain("[redacted]");
|
|
603
|
+
expect(failedItem.evalId).toBe("list/failed");
|
|
604
|
+
expect(failedItem.experimentId).toBe("exp/list");
|
|
605
|
+
expect(failedItem.costUSD).toBe(0.1);
|
|
606
|
+
|
|
607
|
+
const evals = await evalListData([listSnap()], { redact });
|
|
608
|
+
const nested = evals.find((item) => item.evalId === "list/failed")!.attempts[0]!;
|
|
609
|
+
expect(nested.failureSummary).toContain("[redacted]");
|
|
610
|
+
|
|
611
|
+
const experiments = await experimentListData([listSnap()], { redact });
|
|
612
|
+
const nestedInExp = experiments[0]!.evalRows.find((row) => row.evalId === "list/failed")!.attempts[0]!;
|
|
613
|
+
expect(nestedInExp.failureSummary).toContain("[redacted]");
|
|
614
|
+
});
|
|
817
615
|
|
|
818
|
-
it("
|
|
819
|
-
const
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
});
|
|
827
|
-
expect(data.x).toEqual({ key: "latencyMs", label: "Simulated latency", unit: "ms" });
|
|
828
|
-
expect(data.series).toBe("agents");
|
|
829
|
-
expect(data.rows).toHaveLength(3);
|
|
830
|
-
|
|
831
|
-
const p100 = data.rows.find((r) => r.key === "ultra/lat-100")!;
|
|
832
|
-
expect(p100.x).toBe(100);
|
|
833
|
-
expect(p100.xDisplay).toBe("100ms");
|
|
834
|
-
expect(p100.series).toBe("1 agents");
|
|
835
|
-
expect(p100.y.value).toBe(1);
|
|
836
|
-
|
|
837
|
-
// 未声明 flag 的 experiment 不猜:作轴 x=null(组件不画、注脚报数),分组归 (unset)
|
|
838
|
-
const legacyRow = data.rows.find((r) => r.key === "ultra/legacy")!;
|
|
839
|
-
expect(legacyRow.x).toBeNull();
|
|
840
|
-
expect(legacyRow.xDisplay).toBe("");
|
|
841
|
-
expect(legacyRow.series).toBe("(unset)");
|
|
616
|
+
it("experimentListData:evalVerdicts / endToEndPassRate / costUSD / durationMs / tokens 齐全,默认按端到端成功率降序", async () => {
|
|
617
|
+
const winner = snap({ experimentId: "exp/win", results: [res("a", "passed"), res("b", "passed")] });
|
|
618
|
+
const loser = snap({ experimentId: "exp/lose", results: [res("a", "failed"), res("b", "passed")] });
|
|
619
|
+
const items = await experimentListData([loser, winner]);
|
|
620
|
+
expect(items.map((item) => item.experimentId)).toEqual(["exp/win", "exp/lose"]);
|
|
621
|
+
expect(items[0]!.evalVerdicts).toEqual({ passed: 2, failed: 0, errored: 0, skipped: 0 });
|
|
622
|
+
expect(items[0]!.endToEndPassRate.value).toBe(1);
|
|
623
|
+
expect(items[1]!.evals).toBe(2);
|
|
842
624
|
});
|
|
843
625
|
|
|
844
|
-
it("
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
expect(data.dimension).toBe("agents");
|
|
852
|
-
expect(data.rows.map((r) => r.key)).toEqual(["1 agents", "16 agents"]);
|
|
626
|
+
it("同一 experiment 的输入含不一致可比性配置时按完整用户反馈失败,指引 snapshot 维度 / MetricLine", async () => {
|
|
627
|
+
const a = snap({ experimentId: "exp/mixed", model: "gpt-a", results: [res("x", "passed")] });
|
|
628
|
+
const b = snap({ experimentId: "exp/mixed", model: "gpt-b", results: [res("y", "passed")] });
|
|
629
|
+
await expect(experimentListData([a, b])).rejects.toThrow(/snapshot.*MetricLine|MetricLine/s);
|
|
630
|
+
// current() 口径的 Scope(一实验一配置)照常计算
|
|
631
|
+
const clean = scopeOf([a]);
|
|
632
|
+
await expect(experimentListData(clean)).resolves.toHaveLength(1);
|
|
853
633
|
});
|
|
854
634
|
});
|
|
855
635
|
|
|
856
|
-
// ─────────────────────────
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
636
|
+
// ───────────────────────── scopeSummaryData ─────────────────────────
|
|
637
|
+
|
|
638
|
+
describe("scopeSummaryData", () => {
|
|
639
|
+
it("evals 按 experimentId + evalId 计数(2 实验 × 6 题 = 12),与 evalVerdicts 同分母;两级计票在含重试时不同", async () => {
|
|
640
|
+
const mk = (experimentId: string) =>
|
|
641
|
+
snap({
|
|
642
|
+
experimentId,
|
|
643
|
+
results: [
|
|
644
|
+
res("q1", "passed"),
|
|
645
|
+
res("q2", "failed", { attempt: 0 }),
|
|
646
|
+
res("q2", "passed", { attempt: 1 }), // 重试后过:eval 级 passed,attempt 级 1 failed + 1 passed
|
|
647
|
+
res("q3", "passed"),
|
|
648
|
+
res("q4", "failed"),
|
|
649
|
+
res("q5", "errored", { error: erroredWith("x") }),
|
|
650
|
+
res("q6", "skipped"),
|
|
651
|
+
],
|
|
652
|
+
});
|
|
653
|
+
const data = await scopeSummaryData([mk("cmp/a"), mk("cmp/b")]);
|
|
654
|
+
expect(data.experiments).toBe(2);
|
|
655
|
+
expect(data.evals).toBe(12);
|
|
656
|
+
expect(data.evalVerdicts).toEqual({ passed: 6, failed: 2, errored: 2, skipped: 2 });
|
|
657
|
+
expect(
|
|
658
|
+
data.evalVerdicts.passed + data.evalVerdicts.failed + data.evalVerdicts.errored + data.evalVerdicts.skipped,
|
|
659
|
+
).toBe(data.evals);
|
|
660
|
+
expect(data.attemptVerdicts).toEqual({ passed: 6, failed: 4, errored: 2, skipped: 2 });
|
|
661
|
+
expect(data.attemptVerdicts).not.toEqual(data.evalVerdicts);
|
|
662
|
+
expect(data.range.earliestStartedAt).not.toBeNull();
|
|
663
|
+
expect(data.range.latestStartedAt).not.toBeNull();
|
|
664
|
+
});
|
|
868
665
|
|
|
869
|
-
it("
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
666
|
+
it("totalCostUSD 按 attempt 求和;一次成本都没报时 value 为 null,不伪造 0", async () => {
|
|
667
|
+
const withCost = snap({
|
|
668
|
+
experimentId: "exp/cost",
|
|
669
|
+
results: [
|
|
670
|
+
res("a", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.25 } }),
|
|
671
|
+
res("b", "failed", { estimatedCostUSD: 0.05 }),
|
|
672
|
+
res("c", "passed"),
|
|
673
|
+
],
|
|
876
674
|
});
|
|
877
|
-
|
|
878
|
-
expect(data.
|
|
675
|
+
const data = await scopeSummaryData([withCost]);
|
|
676
|
+
expect(data.totalCostUSD.value).toBeCloseTo(0.3);
|
|
677
|
+
const none = await scopeSummaryData([snap({ experimentId: "exp/free", results: [res("a", "passed")] })]);
|
|
678
|
+
expect(none.totalCostUSD.value).toBeNull();
|
|
879
679
|
});
|
|
880
680
|
|
|
881
|
-
it("
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const data = await MetricLine.data([b1, b2, none], {
|
|
886
|
-
x: config("budget", { label: "Budget", unit: "USD" }),
|
|
887
|
-
y: taskPassRate,
|
|
888
|
-
});
|
|
889
|
-
expect(data.x).toEqual({ key: "budget", label: "Budget", unit: "USD" });
|
|
890
|
-
const one = data.rows.find((r) => r.key === "budget/one")!;
|
|
891
|
-
expect(one.x).toBe(1);
|
|
892
|
-
expect(one.y.value).toBe(1);
|
|
893
|
-
const missing = data.rows.find((r) => r.key === "budget/none")!;
|
|
894
|
-
expect(missing.x).toBeNull();
|
|
895
|
-
expect(missing.xDisplay).toBe("");
|
|
896
|
-
expect(data.rows.filter((r) => r.x === null)).toHaveLength(1);
|
|
681
|
+
it("空范围的 range 为 null,不编造当前时间", async () => {
|
|
682
|
+
const data = await scopeSummaryData([]);
|
|
683
|
+
expect(data.range).toEqual({ earliestStartedAt: null, latestStartedAt: null });
|
|
684
|
+
expect(data.evals).toBe(0);
|
|
897
685
|
});
|
|
686
|
+
});
|
|
898
687
|
|
|
899
|
-
|
|
900
|
-
const mini = withConfig("m/mini", { budget: 1 }, "passed", "gpt-mini");
|
|
901
|
-
const maxi = withConfig("m/maxi", { budget: 1 }, "failed", "gpt-maxi");
|
|
902
|
-
const data = await MetricTable.data([mini, maxi], {
|
|
903
|
-
rows: config("model"),
|
|
904
|
-
columns: [taskPassRate],
|
|
905
|
-
});
|
|
906
|
-
expect(data.rows.map((r) => r.key)).toEqual(["gpt-mini", "gpt-maxi"]);
|
|
688
|
+
// ───────────────────────── experimentComparisonData ─────────────────────────
|
|
907
689
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
690
|
+
describe("experimentComparisonData", () => {
|
|
691
|
+
it("计算前按完整父路径分区,根目录 experiment 单例组;每组与独立调用三个计算函数深等,refs 不跨组", async () => {
|
|
692
|
+
const g1a = snap({ experimentId: "compare/a", agent: "bub", results: [res("q", "passed")] });
|
|
693
|
+
const g1b = snap({ experimentId: "compare/b", agent: "codex", results: [res("q", "failed")] });
|
|
694
|
+
const g2 = snap({ experimentId: "bench/long/x", results: [res("q", "passed")] });
|
|
695
|
+
const solo = snap({ experimentId: "standalone", results: [res("q", "errored", { error: erroredWith("x") })] });
|
|
911
696
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const s2 = withConfig("runs/5", { runs: 5 }, "passed");
|
|
915
|
-
const data = await MetricTable.data([s1, s2], {
|
|
916
|
-
rows: config("runs", { label: (v) => `${v} runs` }),
|
|
917
|
-
columns: [taskPassRate],
|
|
918
|
-
});
|
|
919
|
-
expect(data.rows.map((r) => r.key)).toEqual(["3 runs", "5 runs"]);
|
|
920
|
-
});
|
|
697
|
+
const data = await experimentComparisonData([g1a, g1b, g2, solo]);
|
|
698
|
+
expect(data.groups.map((g) => g.key)).toEqual(["bench/long", "compare", "standalone"]);
|
|
921
699
|
|
|
922
|
-
|
|
923
|
-
|
|
700
|
+
const compare = data.groups.find((g) => g.key === "compare")!;
|
|
701
|
+
const [summary, scatter, experiments] = await Promise.all([
|
|
702
|
+
scopeSummaryData([g1a, g1b]),
|
|
703
|
+
metricScatterData([g1a, g1b], { points: "experiment", series: "agent", x: costUSD, y: endToEndPassRate }),
|
|
704
|
+
experimentListData([g1a, g1b]),
|
|
705
|
+
]);
|
|
706
|
+
expect(compare.summary).toEqual(summary);
|
|
707
|
+
expect(compare.scatter).toEqual(scatter);
|
|
708
|
+
expect(compare.experiments).toEqual(experiments);
|
|
709
|
+
|
|
710
|
+
// 组外 attempt 不污染:compare 组的 refs 不含 bench/standalone 的 locator
|
|
711
|
+
const soloRefs = data.groups.find((g) => g.key === "standalone")!.summary.endToEndPassRate.refs;
|
|
712
|
+
const compareRefs = compare.summary.endToEndPassRate.refs;
|
|
713
|
+
expect(compareRefs.some((ref) => soloRefs.includes(ref))).toBe(false);
|
|
924
714
|
});
|
|
925
715
|
});
|
|
926
716
|
|
|
927
|
-
// ─────────────────────────
|
|
717
|
+
// ───────────────────────── metricScatterData / metricMatrixData ─────────────────────────
|
|
928
718
|
|
|
929
|
-
describe("
|
|
930
|
-
it("
|
|
931
|
-
const
|
|
932
|
-
experimentId: "
|
|
933
|
-
|
|
719
|
+
describe("metricScatterData / metricMatrixData", () => {
|
|
720
|
+
it("缺 x 或 y 的点留在 rows 里可数(组件不画并报数);矩阵稀疏:无 attempt 的组合不生成格子", async () => {
|
|
721
|
+
const withCost = snap({
|
|
722
|
+
experimentId: "cmp/priced",
|
|
723
|
+
agent: "bub",
|
|
724
|
+
results: [res("a", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } })],
|
|
934
725
|
});
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
// ───────────────────────── reasonFor(原因优先级)─────────────────────────
|
|
943
|
-
// docs/feature/reports/show.md 裸 `show` 的 RESULT 列口径:error → skipReason → 未通过的 gate 断言。
|
|
944
|
-
// ExperimentList / EvalList 的 evalRows.reason、AttemptList 的 attemptItemReason 都port这同一份材料,
|
|
945
|
-
// 这里直接测 reasonFor 本身,不必再经 MetricTable 之类的中间组件绕一圈。
|
|
946
|
-
|
|
947
|
-
describe("reasonFor", () => {
|
|
948
|
-
it("同一 result 同时含 error、失败 gate、失败 soft → 只显示 error", () => {
|
|
949
|
-
const result = res("A", "errored", {
|
|
950
|
-
error: erroredWith("adapter crashed"),
|
|
951
|
-
assertions: [
|
|
952
|
-
{ name: "includes", severity: "gate", score: 0, outcome: "failed" as const, detail: "missing text" },
|
|
953
|
-
softAssertion("judge", 0.2, { outcome: "failed" as const }),
|
|
954
|
-
],
|
|
726
|
+
const noCost = snap({ experimentId: "cmp/free", agent: "codex", results: [res("b", "passed")] });
|
|
727
|
+
const scatter = await metricScatterData([withCost, noCost], {
|
|
728
|
+
points: "experiment",
|
|
729
|
+
series: "agent",
|
|
730
|
+
x: costUSD,
|
|
731
|
+
y: endToEndPassRate,
|
|
955
732
|
});
|
|
956
|
-
expect(
|
|
733
|
+
expect(scatter.pointDimension).toBe("experiment");
|
|
734
|
+
expect(scatter.rows).toHaveLength(2);
|
|
735
|
+
expect(scatter.rows.find((r) => r.key === "cmp/free")!.x.value).toBeNull();
|
|
736
|
+
|
|
737
|
+
const matrix = await metricMatrixData([withCost, noCost], {
|
|
738
|
+
rows: "eval",
|
|
739
|
+
columns: "agent",
|
|
740
|
+
cell: endToEndPassRate,
|
|
741
|
+
});
|
|
742
|
+
// a×codex、b×bub 没有样本 → 不出现(不是 value: 0)
|
|
743
|
+
expect(matrix.cells).toHaveLength(2);
|
|
744
|
+
expect(matrix.rowDimension).toBe("eval");
|
|
745
|
+
expect(matrix.columnDimension).toBe("agent");
|
|
957
746
|
});
|
|
958
747
|
|
|
959
|
-
it("
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
748
|
+
it("分组维度上未声明的 flag 归 (missing) 组,不丢行", async () => {
|
|
749
|
+
const withFlag = snap({
|
|
750
|
+
experimentId: "f/on",
|
|
751
|
+
results: [res("a", "passed")],
|
|
752
|
+
experiment: { runs: 1, earlyExit: false, selectedEvalIds: [], flags: { memory: "mempal" } },
|
|
753
|
+
});
|
|
754
|
+
const withoutFlag = snap({ experimentId: "f/off", results: [res("a", "failed")] });
|
|
755
|
+
const table = await metricTableData([withFlag, withoutFlag], {
|
|
756
|
+
rows: flag("memory"),
|
|
757
|
+
columns: [endToEndPassRate],
|
|
963
758
|
});
|
|
964
|
-
expect(
|
|
759
|
+
expect(table.rows.map((r) => r.key).sort()).toEqual(["(missing)", "mempal"]);
|
|
965
760
|
});
|
|
966
761
|
|
|
967
|
-
it("
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
762
|
+
it("metricTableData sort:必须是 columns 中同一实例且声明 better;方向随 better,缺数据沉底", async () => {
|
|
763
|
+
const hi = snap({ experimentId: "s/hi", results: [res("a", "passed")] });
|
|
764
|
+
const lo = snap({ experimentId: "s/lo", results: [res("a", "failed")] });
|
|
765
|
+
const na = snap({ experimentId: "s/na", results: [res("a", "skipped")] });
|
|
766
|
+
const byPass = await metricTableData([lo, hi, na], {
|
|
767
|
+
rows: "experiment",
|
|
768
|
+
columns: [endToEndPassRate],
|
|
769
|
+
sort: endToEndPassRate,
|
|
770
|
+
});
|
|
771
|
+
expect(byPass.rows.map((r) => r.key)).toEqual(["s/hi", "s/lo", "s/na"]);
|
|
772
|
+
|
|
773
|
+
const fast = snap({ experimentId: "d/fast", results: [res("a", "passed", { durationMs: 10 })] });
|
|
774
|
+
const slow = snap({ experimentId: "d/slow", results: [res("a", "passed", { durationMs: 99 })] });
|
|
775
|
+
const byDuration = await metricTableData([slow, fast], {
|
|
776
|
+
rows: "experiment",
|
|
777
|
+
columns: [durationMs],
|
|
778
|
+
sort: durationMs,
|
|
975
779
|
});
|
|
976
|
-
expect(
|
|
780
|
+
expect(byDuration.rows.map((r) => r.key)).toEqual(["d/fast", "d/slow"]); // lower better:低在前
|
|
781
|
+
|
|
782
|
+
await expect(
|
|
783
|
+
metricTableData([hi], { rows: "experiment", columns: [endToEndPassRate], sort: durationMs }),
|
|
784
|
+
).rejects.toThrow(/columns/);
|
|
785
|
+
const noBetter = defineMetric({ name: "plain", value: () => 1 });
|
|
786
|
+
await expect(
|
|
787
|
+
metricTableData([hi], { rows: "experiment", columns: [noBetter], sort: noBetter }),
|
|
788
|
+
).rejects.toThrow(/better/);
|
|
977
789
|
});
|
|
978
790
|
|
|
979
|
-
it("
|
|
980
|
-
|
|
791
|
+
it("省略 sort 时按行 key 字典序(维度 domain 稳定序,不随文件扫描顺序)", async () => {
|
|
792
|
+
const b = snap({ experimentId: "o/bbb", results: [res("a", "passed")] });
|
|
793
|
+
const a = snap({ experimentId: "o/aaa", results: [res("a", "failed")] });
|
|
794
|
+
const table = await metricTableData([b, a], { rows: "experiment", columns: [endToEndPassRate] });
|
|
795
|
+
expect(table.rows.map((r) => r.key)).toEqual(["o/aaa", "o/bbb"]);
|
|
981
796
|
});
|
|
982
797
|
});
|
|
983
798
|
|
|
984
|
-
// ─────────────────────────
|
|
799
|
+
// ───────────────────────── metricLineData ─────────────────────────
|
|
985
800
|
|
|
986
|
-
describe("
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
experimentId: "exp/y",
|
|
998
|
-
results: [
|
|
999
|
-
res("A", "passed", { estimatedCostUSD: 0.1 }),
|
|
1000
|
-
res("B", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } }),
|
|
1001
|
-
],
|
|
801
|
+
describe("metricLineData", () => {
|
|
802
|
+
const flaggedSnap = (experimentId: string, budget: number | undefined, verdicts: Verdict[]) =>
|
|
803
|
+
snap({
|
|
804
|
+
experimentId,
|
|
805
|
+
results: verdicts.map((v, i) => res(`q${i}`, v)),
|
|
806
|
+
experiment: {
|
|
807
|
+
runs: 1,
|
|
808
|
+
earlyExit: false,
|
|
809
|
+
selectedEvalIds: [],
|
|
810
|
+
...(budget !== undefined ? { flags: { budget } } : {}),
|
|
811
|
+
},
|
|
1002
812
|
});
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
{
|
|
1008
|
-
|
|
813
|
+
|
|
814
|
+
it("未声明数值 flag 的 experiment 不伪造 x 值(不落到 x=0)并可数", async () => {
|
|
815
|
+
const data = await metricLineData(
|
|
816
|
+
[flaggedSnap("l/100", 100, ["passed"]), flaggedSnap("l/none", undefined, ["passed"])],
|
|
817
|
+
{ x: numericFlag("budget"), y: endToEndPassRate },
|
|
818
|
+
);
|
|
819
|
+
const missing = data.rows.filter((r) => r.x === null);
|
|
820
|
+
expect(missing).toHaveLength(1);
|
|
821
|
+
expect(data.rows.some((r) => r.x === 0)).toBe(false);
|
|
1009
822
|
});
|
|
1010
823
|
|
|
1011
|
-
it("
|
|
1012
|
-
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
expect(data.warnings).toEqual([warning]);
|
|
824
|
+
it("点身份 = (series, x):同桶多 experiment 合成一个点,y 按 (series, x, experiment, eval) 顺序聚合", async () => {
|
|
825
|
+
// 两个 experiment 同 x=100:各 1 题,一个 passed 一个 failed → 合成一点 y = (1+0)/2
|
|
826
|
+
const data = await metricLineData(
|
|
827
|
+
[flaggedSnap("m/one", 100, ["passed"]), flaggedSnap("m/two", 100, ["failed"])],
|
|
828
|
+
{ x: numericFlag("budget"), y: endToEndPassRate },
|
|
829
|
+
);
|
|
830
|
+
expect(data.rows).toHaveLength(1);
|
|
831
|
+
expect(data.rows[0]!.key).toBe("100"); // x 的稳定十进制字符串
|
|
832
|
+
expect(data.rows[0]!.x).toBe(100);
|
|
833
|
+
expect(data.rows[0]!.y.value).toBeCloseTo(0.5);
|
|
1022
834
|
});
|
|
1023
835
|
|
|
1024
|
-
it("
|
|
836
|
+
it("自定义 NumericAxis.of 在同一 experiment × eval 内不恒定时报完整用户反馈,不静默取首值", async () => {
|
|
1025
837
|
const s = snap({
|
|
1026
|
-
experimentId: "
|
|
1027
|
-
results: [
|
|
1028
|
-
res("pass/a", "passed"),
|
|
1029
|
-
res("pass/b", "passed"),
|
|
1030
|
-
...Array.from({ length: 5 }, (_, i) => res(`error/${i}`, "errored", { error: erroredWith("boom") })),
|
|
1031
|
-
],
|
|
838
|
+
experimentId: "l/vary",
|
|
839
|
+
results: [res("q", "passed", { attempt: 0, durationMs: 10 }), res("q", "passed", { attempt: 1, durationMs: 20 })],
|
|
1032
840
|
});
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
expect(
|
|
841
|
+
const perAttempt = {
|
|
842
|
+
name: "per-attempt",
|
|
843
|
+
of: (attempt: AttemptHandle) => attempt.result.durationMs,
|
|
844
|
+
};
|
|
845
|
+
await expect(metricLineData([s], { x: perAttempt, y: endToEndPassRate })).rejects.toThrow(/not constant/);
|
|
1038
846
|
});
|
|
1039
847
|
});
|
|
1040
848
|
|
|
1041
|
-
// ─────────────────────────
|
|
849
|
+
// ───────────────────────── deltaTableData 与 pairsByFlag ─────────────────────────
|
|
1042
850
|
|
|
1043
|
-
describe("
|
|
1044
|
-
it("
|
|
1045
|
-
const
|
|
1046
|
-
experimentId: "
|
|
1047
|
-
|
|
1048
|
-
results: [
|
|
1049
|
-
// "shared" 两轮:一败一过 → 折叠成 passed(与 exp/b 同名的 "shared" 各算各的,见下方独立测试)
|
|
1050
|
-
res("shared", "failed", { attempt: 0 }),
|
|
1051
|
-
res("shared", "passed", { attempt: 1, usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.5 } }),
|
|
1052
|
-
// "only-a" 只有 exp/a 跑过,失败、没报成本
|
|
1053
|
-
res("only-a", "failed"),
|
|
1054
|
-
],
|
|
851
|
+
describe("deltaTableData", () => {
|
|
852
|
+
it("任一侧缺数据时 delta 保持缺失;方向按指标 better 判断改善/退化", async () => {
|
|
853
|
+
const a = snap({
|
|
854
|
+
experimentId: "d/base",
|
|
855
|
+
results: [res("q", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.4 } })],
|
|
1055
856
|
});
|
|
1056
|
-
const
|
|
1057
|
-
experimentId: "
|
|
1058
|
-
|
|
1059
|
-
results: [
|
|
1060
|
-
// 与 exp/a 同名的 "shared":不同 experiment,必须独立计一票,不能被误合并
|
|
1061
|
-
res("shared", "errored", { estimatedCostUSD: 0.2 }),
|
|
1062
|
-
],
|
|
857
|
+
const b = snap({
|
|
858
|
+
experimentId: "d/next",
|
|
859
|
+
results: [res("q", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } })],
|
|
1063
860
|
});
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
expect(data.
|
|
1070
|
-
|
|
1071
|
-
expect(
|
|
1072
|
-
//
|
|
1073
|
-
expect(
|
|
1074
|
-
|
|
1075
|
-
expect(
|
|
1076
|
-
expect(data.passRate.total).toBe(3);
|
|
1077
|
-
// 总成本 null-safe 求和:0.5(实测)+ 0.2(估算)= 0.7;only-a 的失败 attempt 没报成本,不计 0
|
|
1078
|
-
expect(data.totalCostUSD).toBeCloseTo(0.7, 10);
|
|
1079
|
-
// 最后运行时间:组内两个快照的 startedAt 取最大值(exp/b 更晚)
|
|
1080
|
-
expect(data.lastRunAt).toBe(expB.startedAt);
|
|
1081
|
-
});
|
|
1082
|
-
|
|
1083
|
-
it("全组没有任何 attempt 报成本 → totalCostUSD null,不编 0", async () => {
|
|
1084
|
-
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
|
|
1085
|
-
const data = await GroupSummary.data([s]);
|
|
1086
|
-
expect(data.totalCostUSD).toBeNull();
|
|
861
|
+
const data = await deltaTableData([a, b], {
|
|
862
|
+
by: "experiment",
|
|
863
|
+
pairs: [{ label: "next vs base", a: "d/base", b: "d/next" }],
|
|
864
|
+
metrics: [costUSD, assistantTurns],
|
|
865
|
+
});
|
|
866
|
+
expect(data.byDimension).toBe("experiment");
|
|
867
|
+
const row = data.rows[0]!;
|
|
868
|
+
expect(row.label).toBe("next vs base");
|
|
869
|
+
// costUSD 下降且 better: "lower" → improved
|
|
870
|
+
expect(row.cells[costUSD.name]).toMatchObject({ delta: expect.closeTo(-0.2, 5), outcome: "improved" });
|
|
871
|
+
// assistantTurns 两侧都缺 o11y → delta null → unavailable
|
|
872
|
+
expect(row.cells[assistantTurns.name]).toMatchObject({ delta: null, outcome: "unavailable" });
|
|
1087
873
|
});
|
|
1088
874
|
|
|
1089
|
-
it("
|
|
1090
|
-
const s = snap({ experimentId: "
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
expect(
|
|
1096
|
-
|
|
875
|
+
it("pairs 空数组在计算时按完整用户反馈报错;运行期构造的非空 pairs 直接可用", async () => {
|
|
876
|
+
const s = snap({ experimentId: "d/x", results: [res("q", "passed")] });
|
|
877
|
+
await expect(
|
|
878
|
+
deltaTableData([s], { by: "experiment", pairs: [] as { label: string; a: string; b: string }[], metrics: [costUSD] }),
|
|
879
|
+
).rejects.toThrow(/empty/);
|
|
880
|
+
const dynamic = [{ label: "run", a: "d/x", b: "d/y" }].filter(() => true);
|
|
881
|
+
await expect(
|
|
882
|
+
deltaTableData([s], { by: "experiment", pairs: dynamic, metrics: [costUSD] }),
|
|
883
|
+
).resolves.toMatchObject({ rows: [{ label: "run" }] });
|
|
1097
884
|
});
|
|
1098
885
|
|
|
1099
|
-
it(
|
|
1100
|
-
const s = snap({
|
|
1101
|
-
|
|
1102
|
-
|
|
886
|
+
it("字面 pair 校验:label 空/重复、a === b 报错;a/b 精确匹配维度 key,未命中保留 pair、对应侧缺失", async () => {
|
|
887
|
+
const s = snap({ experimentId: "d/only", results: [res("q", "passed")] });
|
|
888
|
+
await expect(
|
|
889
|
+
deltaTableData([s], {
|
|
890
|
+
by: "experiment",
|
|
891
|
+
pairs: [
|
|
892
|
+
{ label: "dup", a: "d/only", b: "d/gone" },
|
|
893
|
+
{ label: "dup", a: "d/gone", b: "d/only" },
|
|
894
|
+
],
|
|
895
|
+
metrics: [endToEndPassRate],
|
|
896
|
+
}),
|
|
897
|
+
).rejects.toThrow(/twice/);
|
|
898
|
+
await expect(
|
|
899
|
+
deltaTableData([s], { by: "experiment", pairs: [{ label: "self", a: "d/only", b: "d/only" }], metrics: [endToEndPassRate] }),
|
|
900
|
+
).rejects.toThrow(/itself/);
|
|
901
|
+
const data = await deltaTableData([s], {
|
|
902
|
+
by: "experiment",
|
|
903
|
+
pairs: [{ label: "half", a: "d/only", b: "d/gone" }],
|
|
904
|
+
metrics: [endToEndPassRate],
|
|
1103
905
|
});
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
expect(
|
|
906
|
+
const cell = data.rows[0]!.cells[endToEndPassRate.name]!;
|
|
907
|
+
expect(cell.a.value).toBe(1);
|
|
908
|
+
expect(cell.b.value).toBeNull();
|
|
909
|
+
expect(cell.delta).toBeNull();
|
|
1107
910
|
});
|
|
1108
|
-
});
|
|
1109
911
|
|
|
1110
|
-
|
|
912
|
+
describe("pairsByFlag", () => {
|
|
913
|
+
/** 三 agent × baseline / agents-md / mempal 矩阵;bub 无 mempal,如实少一对。 */
|
|
914
|
+
const matrixSnaps = () => {
|
|
915
|
+
const mk = (agent: string, memory?: string) =>
|
|
916
|
+
snap({
|
|
917
|
+
experimentId: `mem/${agent}${memory ? `--${memory}` : ""}`,
|
|
918
|
+
agent,
|
|
919
|
+
results: [res("q", "passed")],
|
|
920
|
+
experiment: {
|
|
921
|
+
runs: 1,
|
|
922
|
+
earlyExit: false,
|
|
923
|
+
selectedEvalIds: [],
|
|
924
|
+
...(memory !== undefined ? { flags: { memory } } : {}),
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
return [
|
|
928
|
+
mk("bub"),
|
|
929
|
+
mk("bub", "agents-md"),
|
|
930
|
+
mk("codex"),
|
|
931
|
+
mk("codex", "agents-md"),
|
|
932
|
+
mk("codex", "mempal"),
|
|
933
|
+
mk("gemini"),
|
|
934
|
+
mk("gemini", "agents-md"),
|
|
935
|
+
mk("gemini", "mempal"),
|
|
936
|
+
];
|
|
937
|
+
};
|
|
1111
938
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
939
|
+
it("同可比组 + 删除该 flag 后配置深相等才配对;a 取 baseline(缺省=未声明),label 自动生成,按 (a 末段, 显示键) 字典序", async () => {
|
|
940
|
+
const data = await deltaTableData(matrixSnaps(), {
|
|
941
|
+
by: "experiment",
|
|
942
|
+
pairs: pairsByFlag("memory"),
|
|
943
|
+
metrics: [endToEndPassRate],
|
|
944
|
+
});
|
|
945
|
+
expect(data.rows.map((r) => r.label)).toEqual([
|
|
946
|
+
"bub · memory=agents-md",
|
|
947
|
+
"codex · memory=agents-md",
|
|
948
|
+
"codex · memory=mempal",
|
|
949
|
+
"gemini · memory=agents-md",
|
|
950
|
+
"gemini · memory=mempal",
|
|
951
|
+
]);
|
|
952
|
+
expect(data.experiments).toBe(8);
|
|
1121
953
|
});
|
|
1122
|
-
const data = await DeltaTable.data([base, plus], {
|
|
1123
|
-
pairs: [{ a: "exp/base", b: "exp/plus", label: "memory" }],
|
|
1124
|
-
metrics: [taskPassRate, costUSD],
|
|
1125
|
-
});
|
|
1126
|
-
expect(data.rows).toHaveLength(1);
|
|
1127
|
-
const row = data.rows[0];
|
|
1128
|
-
expect(row.key).toBe("memory");
|
|
1129
|
-
expect(row.a).toEqual({ experimentId: "exp/base" });
|
|
1130
|
-
expect(row.b).toEqual({ experimentId: "exp/plus" });
|
|
1131
|
-
|
|
1132
|
-
const pass = row.cells["task-pass-rate"];
|
|
1133
|
-
expect(pass.a.value).toBe(0.5);
|
|
1134
|
-
expect(pass.b.value).toBe(1);
|
|
1135
|
-
expect(pass.delta).toBeCloseTo(0.5);
|
|
1136
|
-
expect(pass.display).toBe("+50%");
|
|
1137
|
-
|
|
1138
|
-
const cost = row.cells["cost"];
|
|
1139
|
-
expect(cost.a.value).toBeCloseTo(0.2);
|
|
1140
|
-
expect(cost.b.value).toBeNull();
|
|
1141
|
-
expect(cost.delta).toBeNull(); // 单侧缺数据:不硬算
|
|
1142
|
-
expect(cost.display).toBe("—");
|
|
1143
|
-
|
|
1144
|
-
const flat = await DeltaTable.data([base, base], {
|
|
1145
|
-
pairs: [{ a: "exp/base", b: "exp/base", label: "same" }],
|
|
1146
|
-
metrics: [taskPassRate],
|
|
1147
|
-
});
|
|
1148
|
-
expect(flat.rows[0].cells["task-pass-rate"].display).toBe("±0");
|
|
1149
|
-
});
|
|
1150
954
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
955
|
+
it("可比性配置不同(model 不同)的两实验不配对", async () => {
|
|
956
|
+
const base = snap({
|
|
957
|
+
experimentId: "mm/a",
|
|
958
|
+
model: "gpt-a",
|
|
959
|
+
results: [res("q", "passed")],
|
|
960
|
+
experiment: { runs: 1, earlyExit: false, selectedEvalIds: [] },
|
|
961
|
+
});
|
|
962
|
+
const other = snap({
|
|
963
|
+
experimentId: "mm/b",
|
|
964
|
+
model: "gpt-b",
|
|
965
|
+
results: [res("q", "passed")],
|
|
966
|
+
experiment: { runs: 1, earlyExit: false, selectedEvalIds: [], flags: { memory: "on" } },
|
|
967
|
+
});
|
|
968
|
+
const data = await deltaTableData([base, other], {
|
|
969
|
+
by: "experiment",
|
|
970
|
+
pairs: pairsByFlag("memory"),
|
|
971
|
+
metrics: [endToEndPassRate],
|
|
972
|
+
});
|
|
973
|
+
expect(data.rows).toHaveLength(0);
|
|
974
|
+
expect(data.experiments).toBe(2);
|
|
1161
975
|
});
|
|
1162
|
-
const data = await DeltaTable.data([older, newer], {
|
|
1163
|
-
pairs: [
|
|
1164
|
-
{
|
|
1165
|
-
a: "exp/x @ 2026-07-01T08:00:00Z",
|
|
1166
|
-
b: "exp/x @ 2026-07-02T08:00:00Z",
|
|
1167
|
-
label: "this week vs last",
|
|
1168
|
-
},
|
|
1169
|
-
],
|
|
1170
|
-
metrics: [taskPassRate],
|
|
1171
|
-
});
|
|
1172
|
-
const cell = data.rows[0].cells["task-pass-rate"];
|
|
1173
|
-
expect(cell.a.value).toBe(0); // 旧快照那份
|
|
1174
|
-
expect(cell.b.value).toBe(1); // 新快照那份
|
|
1175
|
-
expect(cell.display).toBe("+100%");
|
|
1176
|
-
});
|
|
1177
|
-
});
|
|
1178
976
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
results: [res("A", "failed", identity)],
|
|
977
|
+
it("收窄到单实验时 0 对不是错误:空 rows + 配对域实验数;by 非 experiment 报完整用户反馈", async () => {
|
|
978
|
+
const single = snap({ experimentId: "solo/x", results: [res("q", "passed")] });
|
|
979
|
+
const data = await deltaTableData([single], {
|
|
980
|
+
by: "experiment",
|
|
981
|
+
pairs: pairsByFlag("memory"),
|
|
982
|
+
metrics: [endToEndPassRate],
|
|
983
|
+
});
|
|
984
|
+
expect(data.rows).toHaveLength(0);
|
|
985
|
+
expect(data.experiments).toBe(1);
|
|
986
|
+
|
|
987
|
+
await expect(
|
|
988
|
+
deltaTableData([single], { by: "agent", pairs: pairsByFlag("memory"), metrics: [endToEndPassRate] }),
|
|
989
|
+
).rejects.toThrow(/by: "experiment"/);
|
|
1193
990
|
});
|
|
1194
|
-
const newer = snap({
|
|
1195
|
-
experimentId: "exp/x",
|
|
1196
|
-
runStartedAt: "2026-07-02T08:00:00Z",
|
|
1197
|
-
results: [res("A", "passed", identity)],
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
for (const order of [
|
|
1201
|
-
[older, newer],
|
|
1202
|
-
[newer, older],
|
|
1203
|
-
]) {
|
|
1204
|
-
const data = await MetricTable.data(order, { rows: "agent", columns: [taskPassRate] });
|
|
1205
|
-
const cell = data.rows[0].cells["task-pass-rate"];
|
|
1206
|
-
expect(cell.total).toBe(1); // 两份只算一份
|
|
1207
|
-
expect(cell.value).toBe(1); // 留的是最新 run 里的 passed,与快照传入顺序无关
|
|
1208
|
-
|
|
1209
|
-
const ov = await RunOverview.data(order);
|
|
1210
|
-
expect(ov.totals.attempts).toBe(1);
|
|
1211
|
-
expect(ov.totals.passed).toBe(1);
|
|
1212
|
-
expect(ov.totals.failed).toBe(0);
|
|
1213
|
-
}
|
|
1214
|
-
});
|
|
1215
|
-
|
|
1216
|
-
it("startedAt 缺失:不去重、如实保留重复,不透出警告", async () => {
|
|
1217
|
-
const identity = { experimentId: "exp/x", attempt: 0, startedAt: undefined };
|
|
1218
|
-
const one = snap({ experimentId: "exp/x", results: [res("A", "passed", identity)] });
|
|
1219
|
-
const two = snap({ experimentId: "exp/x", results: [res("A", "passed", identity)] });
|
|
1220
|
-
const ov = await RunOverview.data([one, two]);
|
|
1221
|
-
expect(ov.totals.attempts).toBe(2);
|
|
1222
|
-
expect(ov.warnings).toEqual([]); // missing-startedAt 不透出到组件数据(裁决记录 7)
|
|
1223
|
-
});
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
// ───────────────────────── 格式化 ─────────────────────────
|
|
1227
|
-
|
|
1228
|
-
describe("unit 驱动格式化", () => {
|
|
1229
|
-
it('"%" / "ms" / "$" / 其余缩写', () => {
|
|
1230
|
-
expect(formatMetricValue(0.87, "%")).toBe("87%");
|
|
1231
|
-
expect(formatMetricValue(0.875, "%")).toBe("87.5%");
|
|
1232
|
-
expect(formatMetricValue(-0.008, "%")).toBe("-0.8%");
|
|
1233
|
-
expect(formatMetricValue(850, "ms")).toBe("850ms");
|
|
1234
|
-
expect(formatMetricValue(1234, "ms")).toBe("1.2s");
|
|
1235
|
-
expect(formatMetricValue(125_000, "ms")).toBe("2m 5s");
|
|
1236
|
-
expect(formatMetricValue(0.31, "$")).toBe("$0.31");
|
|
1237
|
-
expect(formatMetricValue(0.0042, "$")).toBe("$0.0042");
|
|
1238
|
-
expect(formatMetricValue(-0.8, "$")).toBe("-$0.80");
|
|
1239
|
-
expect(formatMetricValue(1234, "lines")).toBe("1.2k lines");
|
|
1240
|
-
expect(formatMetricValue(3_400_000, "tokens")).toBe("3.4M tokens");
|
|
1241
|
-
expect(formatMetricValue(42)).toBe("42");
|
|
1242
|
-
});
|
|
1243
991
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
992
|
+
it("baseline 显式声明时 a 侧取该 flag 值", async () => {
|
|
993
|
+
const data = await deltaTableData(matrixSnaps(), {
|
|
994
|
+
by: "experiment",
|
|
995
|
+
pairs: pairsByFlag("memory", { baseline: "agents-md" }),
|
|
996
|
+
metrics: [endToEndPassRate],
|
|
997
|
+
});
|
|
998
|
+
// a = *--agents-md;b = 未声明(显示键 (missing))与 mempal
|
|
999
|
+
expect(data.rows.map((r) => r.label)).toEqual([
|
|
1000
|
+
"bub--agents-md · memory=(missing)",
|
|
1001
|
+
"codex--agents-md · memory=(missing)",
|
|
1002
|
+
"codex--agents-md · memory=mempal",
|
|
1003
|
+
"gemini--agents-md · memory=(missing)",
|
|
1004
|
+
"gemini--agents-md · memory=mempal",
|
|
1005
|
+
]);
|
|
1250
1006
|
});
|
|
1251
|
-
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
1252
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [raw] });
|
|
1253
|
-
expect(data.rows[0].cells["raw"].display).toBe("0.5 raw");
|
|
1254
1007
|
});
|
|
1255
1008
|
});
|
|
1256
1009
|
|
|
1257
|
-
// ─────────────────────────
|
|
1258
|
-
|
|
1259
|
-
describe("MetricMatrix.data", () => {
|
|
1260
|
-
it("稀疏:没有 attempt 的 (row, column) 组合不出格", async () => {
|
|
1261
|
-
const a = snap({
|
|
1262
|
-
experimentId: "exp/a",
|
|
1263
|
-
agent: "a1",
|
|
1264
|
-
results: [res("A", "passed", { agent: "a1" }), res("B", "failed", { agent: "a1" })],
|
|
1265
|
-
});
|
|
1266
|
-
const b = snap({
|
|
1267
|
-
experimentId: "exp/b",
|
|
1268
|
-
agent: "b1",
|
|
1269
|
-
results: [res("A", "failed", { agent: "b1" })], // b1 没跑 B
|
|
1270
|
-
});
|
|
1271
|
-
const data = await MetricMatrix.data([a, b], { rows: "eval", columns: "agent", cell: taskPassRate });
|
|
1272
|
-
expect(data.rows).toBe("eval");
|
|
1273
|
-
expect(data.columns).toBe("agent");
|
|
1274
|
-
expect(data.metric.key).toBe("task-pass-rate");
|
|
1275
|
-
expect(data.cells).toHaveLength(3); // A×a1、B×a1、A×b1;B×b1 不出现
|
|
1276
|
-
const find = (row: string, column: string) => data.cells.find((c) => c.row === row && c.column === column);
|
|
1277
|
-
expect(find("A", "a1")?.cell.value).toBe(1);
|
|
1278
|
-
expect(find("B", "a1")?.cell.value).toBe(0);
|
|
1279
|
-
expect(find("A", "b1")?.cell.value).toBe(0);
|
|
1280
|
-
expect(find("B", "b1")).toBeUndefined();
|
|
1281
|
-
});
|
|
1010
|
+
// ───────────────────────── examScore ─────────────────────────
|
|
1282
1011
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
results: [
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
results: [res("A", "failed", { agent: "b1" })],
|
|
1012
|
+
describe("examScore", () => {
|
|
1013
|
+
it("gate 决定能否得分,soft 给质量分;errored 交白卷是 0 分不是缺数据", async () => {
|
|
1014
|
+
const s = snap({
|
|
1015
|
+
experimentId: "exam/x",
|
|
1016
|
+
results: [
|
|
1017
|
+
res("soft", "passed", { assertions: [softAssertion("a", 0.5), softAssertion("b", 1)] }),
|
|
1018
|
+
res("allgate", "passed"),
|
|
1019
|
+
res("crashed", "errored", { error: erroredWith("boom") }),
|
|
1020
|
+
],
|
|
1293
1021
|
});
|
|
1294
|
-
const
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
expect(
|
|
1022
|
+
const table = await metricTableData([s], { rows: "eval", columns: [examScore] });
|
|
1023
|
+
const cellOf = (key: string) => table.rows.find((r) => r.key === key)!.cells[examScore.name]!;
|
|
1024
|
+
expect(cellOf("soft").value).toBeCloseTo(0.75);
|
|
1025
|
+
expect(cellOf("allgate").value).toBe(1);
|
|
1026
|
+
expect(cellOf("crashed").value).toBe(0);
|
|
1027
|
+
expect(cellOf("crashed").samples).toBe(1); // 0 分是测得的事实,不是缺数据
|
|
1298
1028
|
});
|
|
1299
1029
|
});
|