niceeval 0.6.1 → 0.6.2
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/dist/agents/types.d.ts +67 -5
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +55 -1
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +5 -3
- package/dist/report/aggregate.js +32 -5
- package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
- package/dist/report/built-ins/experiment-comparison.js +116 -10
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +1 -1
- package/dist/report/components.d.ts +8 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +54 -34
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -8
- package/dist/report/format.js +27 -12
- package/dist/report/index.d.ts +4 -3
- package/dist/report/index.js +5 -4
- package/dist/report/locale.d.ts +11 -2
- package/dist/report/locale.js +23 -5
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +65 -14
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +2 -2
- package/dist/report/react/AttemptList.js +5 -6
- package/dist/report/react/EvalList.d.ts +1 -1
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +8 -0
- package/dist/report/react/ExperimentComparison.js +11 -0
- package/dist/report/react/ExperimentList.d.ts +2 -1
- package/dist/report/react/ExperimentList.js +8 -10
- package/dist/report/react/MetricScatter.js +5 -11
- package/dist/report/react/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +3 -3
- package/dist/report/react/fixtures.js +21 -14
- package/dist/report/report.d.ts +5 -1
- package/dist/report/report.js +6 -2
- package/dist/report/text/faces.d.ts +1 -1
- package/dist/report/text/faces.js +42 -41
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +39 -21
- package/dist/results/types.d.ts +11 -0
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +1 -0
- package/dist/shared/aggregate.js +3 -3
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- package/docs-site/zh/concepts/adapter.mdx +22 -4
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
- package/docs-site/zh/guides/authoring.mdx +33 -0
- package/docs-site/zh/guides/ci-integration.mdx +23 -12
- package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
- package/docs-site/zh/guides/custom-reports.mdx +29 -34
- package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
- package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
- package/docs-site/zh/guides/debugging.mdx +210 -0
- package/docs-site/zh/guides/experiments.mdx +10 -3
- package/docs-site/zh/guides/official-adapters.mdx +26 -2
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +42 -30
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +17 -9
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +79 -36
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +17 -1
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +27 -0
- package/docs-site/zh/reference/capabilities.mdx +2 -2
- package/docs-site/zh/reference/cli.mdx +33 -7
- package/docs-site/zh/reference/define-agent.mdx +57 -4
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/expect.mdx +26 -1
- package/package.json +5 -1
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +11 -0
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/types.ts +68 -5
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +411 -108
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +161 -29
- package/src/context/session.test.ts +1 -0
- package/src/context/session.ts +114 -6
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +64 -2
- package/src/i18n/zh-CN.ts +65 -3
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +34 -5
- package/src/report/built-in-user-parity.test.tsx +110 -153
- package/src/report/built-ins/experiment-comparison.tsx +173 -13
- package/src/report/built-ins/index.ts +6 -1
- package/src/report/components.tsx +9 -3
- package/src/report/compute.ts +70 -40
- package/src/report/dual-render.test.tsx +194 -67
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +35 -11
- package/src/report/index.ts +22 -4
- package/src/report/locale.ts +25 -5
- package/src/report/metrics.ts +67 -14
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +6 -31
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +15 -9
- package/src/report/react/MetricScatter.tsx +12 -14
- package/src/report/react/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +33 -1
- package/src/report/react/fixtures.ts +24 -17
- package/src/report/react/render.test.tsx +9 -64
- package/src/report/react/styles.css +73 -2
- package/src/report/report.test.ts +306 -98
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +47 -43
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +41 -21
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +9 -7
- package/src/results/attempt-evidence.ts +15 -8
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +145 -55
- package/src/results/host-equivalence.test.ts +8 -6
- package/src/results/index.ts +2 -0
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +7 -1
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +85 -51
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +7 -0
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +242 -36
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +3 -2
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +2 -2
- package/src/show/index.ts +21 -1
- package/src/show/render.ts +619 -104
- package/src/show/show.test.ts +235 -19
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/components/AttemptModal.tsx +153 -2
- package/src/view/app/components/CodeView.tsx +32 -11
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +6 -0
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/artifact-serving.test.ts +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +1 -0
- package/src/view/data.ts +11 -1
- package/src/view/index.ts +11 -0
- package/src/view/server.ts +2 -0
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +6 -5
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
1
2
|
// niceeval/report 计算层的单元测试:全部用内存 fake(Snapshot / AttemptHandle 按
|
|
2
3
|
// niceeval/results 的读取契约手工构造),专门覆盖 docs/feature/reports/architecture.md 点名的坑 ——
|
|
3
4
|
// 两级聚合 vs 平铺、pass@k、examScore 空真、skipped 稀释、scoreboard 固定分母与
|
|
@@ -6,11 +7,26 @@
|
|
|
6
7
|
|
|
7
8
|
import { describe, expect, it } from "vitest";
|
|
8
9
|
|
|
9
|
-
import type { AssertionResult, EvalResult, O11ySummary, Verdict } from "../types.ts";
|
|
10
|
+
import type { AssertionResult, AttemptError, EvalResult, O11ySummary, Verdict } from "../types.ts";
|
|
11
|
+
|
|
12
|
+
/** 结构化 `AttemptError` 的最小构造(测试用):只关心一层 message,operation/code 取中性默认值。 */
|
|
13
|
+
function erroredWith(message: string): AttemptError {
|
|
14
|
+
return { code: "unexpected-error", message, phase: "eval.run" };
|
|
15
|
+
}
|
|
10
16
|
import type { AttemptHandle, Selection, SelectionWarning, Snapshot } from "../results/index.ts";
|
|
11
17
|
import type { Dimension, MetricCell } from "./types.ts";
|
|
12
|
-
import {
|
|
13
|
-
|
|
18
|
+
import {
|
|
19
|
+
costUSD,
|
|
20
|
+
defineMetric,
|
|
21
|
+
durationMs,
|
|
22
|
+
endToEndPassRate,
|
|
23
|
+
examScore,
|
|
24
|
+
executionReliability,
|
|
25
|
+
taskPassRate,
|
|
26
|
+
tokens,
|
|
27
|
+
turns,
|
|
28
|
+
} from "./metrics.ts";
|
|
29
|
+
import { config, flag } from "./flag.ts";
|
|
14
30
|
import { formatMetricValue } from "./format.ts";
|
|
15
31
|
import {
|
|
16
32
|
AttemptList,
|
|
@@ -48,7 +64,7 @@ function res(id: string, verdict: Verdict, extra: Partial<EvalResult> = {}): Eva
|
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
function softAssertion(name: string, score: number, extra: Partial<AssertionResult> = {}): AssertionResult {
|
|
51
|
-
return { name, severity: "soft", score,
|
|
67
|
+
return { name, severity: "soft", score, outcome: "passed" as const, ...extra } as AssertionResult;
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
/** 最小合规 O11ySummary,只有 totalTurns 会变;供 turns 指标测试内联挂到 EvalResult.o11y。 */
|
|
@@ -146,7 +162,7 @@ describe("ExperimentList.data", () => {
|
|
|
146
162
|
res("memory/a", "failed", {
|
|
147
163
|
attempt: 0,
|
|
148
164
|
durationMs: 1_000,
|
|
149
|
-
assertions: [{ name: "gate", severity: "gate",
|
|
165
|
+
assertions: [{ name: "gate", severity: "gate", outcome: "failed" as const, score: 0, detail: "wrong" }],
|
|
150
166
|
usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.1 },
|
|
151
167
|
hasEvents: true,
|
|
152
168
|
}),
|
|
@@ -155,10 +171,17 @@ describe("ExperimentList.data", () => {
|
|
|
155
171
|
durationMs: 2_000,
|
|
156
172
|
usage: { inputTokens: 20, outputTokens: 10, costUSD: 0.2 },
|
|
157
173
|
}),
|
|
158
|
-
res("memory/b", "errored", { error: "timeout", durationMs: 3_000 }),
|
|
174
|
+
res("memory/b", "errored", { error: erroredWith("timeout"), durationMs: 3_000 }),
|
|
159
175
|
],
|
|
160
176
|
});
|
|
161
|
-
s.experiment = {
|
|
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
|
+
};
|
|
162
185
|
|
|
163
186
|
const items = await ExperimentList.data([s]);
|
|
164
187
|
expect(items).toHaveLength(1);
|
|
@@ -173,7 +196,8 @@ describe("ExperimentList.data", () => {
|
|
|
173
196
|
attempts: 3,
|
|
174
197
|
lastRunAt: "2026-07-10T22:44:00.000Z",
|
|
175
198
|
});
|
|
176
|
-
//
|
|
199
|
+
// 官方端到端两级聚合:memory/a 题内均值 (0+1)/2=0.5;
|
|
200
|
+
// memory/b errored → 0 → 跨题均值 (0.5+0)/2=0.25
|
|
177
201
|
expect(item.passRate.value).toBeCloseTo(0.25);
|
|
178
202
|
expect(item.cost.samples).toBe(2); // 只有 memory/a 的两次 attempt 报了成本
|
|
179
203
|
|
|
@@ -186,7 +210,8 @@ describe("ExperimentList.data", () => {
|
|
|
186
210
|
|
|
187
211
|
const evalB = item.evalRows.find((e) => e.evalId === "memory/b")!;
|
|
188
212
|
expect(evalB.verdict).toBe("errored");
|
|
189
|
-
expect(evalB
|
|
213
|
+
expect(evalB).not.toHaveProperty("reason");
|
|
214
|
+
expect(evalB.attempts[0]!.error?.message).toBe("timeout");
|
|
190
215
|
});
|
|
191
216
|
|
|
192
217
|
it("展开数组按 evalId 升序,顶层数组按 experimentId 升序", async () => {
|
|
@@ -196,10 +221,29 @@ describe("ExperimentList.data", () => {
|
|
|
196
221
|
expect(items.map((i) => i.experimentId)).toEqual(["exp/a", "exp/b"]);
|
|
197
222
|
expect(items.find((i) => i.experimentId === "exp/b")!.evalRows.map((e) => e.evalId)).toEqual(["a", "z"]);
|
|
198
223
|
});
|
|
224
|
+
|
|
225
|
+
it("默认成功率包含 errored 并据此排序:2 passed / 5 errored = 2/7,不能以条件 100% 排第一", async () => {
|
|
226
|
+
const unstable = snap({
|
|
227
|
+
experimentId: "exp/unstable",
|
|
228
|
+
results: [
|
|
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")],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const items = await ExperimentList.data([unstable, stable]);
|
|
240
|
+
expect(items.find((item) => item.experimentId === "exp/unstable")!.passRate.value).toBeCloseTo(2 / 7, 10);
|
|
241
|
+
expect(items.map((item) => item.experimentId)).toEqual(["exp/stable", "exp/unstable"]);
|
|
242
|
+
});
|
|
199
243
|
});
|
|
200
244
|
|
|
201
245
|
describe("EvalList.data", () => {
|
|
202
|
-
it("每项一个 experimentId + evalId
|
|
246
|
+
it("每项一个 experimentId + evalId,父项保留判定与题级聚合并展开到含失败事实的 Attempt", async () => {
|
|
203
247
|
const s = snap({
|
|
204
248
|
experimentId: "compare/codex",
|
|
205
249
|
results: [
|
|
@@ -207,13 +251,13 @@ describe("EvalList.data", () => {
|
|
|
207
251
|
attempt: 0,
|
|
208
252
|
durationMs: 40_000,
|
|
209
253
|
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.02 },
|
|
210
|
-
assertions: [{ name: 'calledTool("get_weather")', severity: "gate",
|
|
254
|
+
assertions: [{ name: 'calledTool("get_weather")', severity: "gate", outcome: "failed" as const, score: 0, expected: "at least one matching call", received: "no tool calls" }],
|
|
211
255
|
}),
|
|
212
256
|
res("weather/brooklyn", "failed", {
|
|
213
257
|
attempt: 1,
|
|
214
258
|
durationMs: 42_000,
|
|
215
259
|
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.06 },
|
|
216
|
-
assertions: [{ name: 'calledTool("get_weather")', severity: "gate",
|
|
260
|
+
assertions: [{ name: 'calledTool("get_weather")', severity: "gate", outcome: "failed" as const, score: 0, expected: "at least one matching call", received: "no tool calls" }],
|
|
217
261
|
}),
|
|
218
262
|
],
|
|
219
263
|
});
|
|
@@ -223,11 +267,15 @@ describe("EvalList.data", () => {
|
|
|
223
267
|
expect(item.evalId).toBe("weather/brooklyn");
|
|
224
268
|
expect(item.experimentId).toBe("compare/codex");
|
|
225
269
|
expect(item.verdict).toBe("failed");
|
|
226
|
-
expect(item
|
|
270
|
+
expect(item).not.toHaveProperty("reason");
|
|
227
271
|
expect(item.score.value).toBe(0); // examScore: failed → 0
|
|
228
272
|
expect(item.duration.value).toBeCloseTo(41_000); // 平均耗时
|
|
229
273
|
expect(item.cost.value).toBeCloseTo(0.04); // 平均成本
|
|
230
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");
|
|
231
279
|
expect(item.attempts[0]!.locator).not.toBe(item.attempts[1]!.locator);
|
|
232
280
|
});
|
|
233
281
|
|
|
@@ -248,7 +296,7 @@ describe("AttemptList.data", () => {
|
|
|
248
296
|
res("A", "failed", {
|
|
249
297
|
durationMs: 5_000,
|
|
250
298
|
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.05 },
|
|
251
|
-
assertions: [{ name: "gate", severity: "gate",
|
|
299
|
+
assertions: [{ name: "gate", severity: "gate", outcome: "failed" as const, score: 0, detail: "nope" }],
|
|
252
300
|
hasEvents: true,
|
|
253
301
|
hasSources: true,
|
|
254
302
|
}),
|
|
@@ -270,18 +318,35 @@ describe("AttemptList.data", () => {
|
|
|
270
318
|
expect(item.locator).toMatch(/^@1[0-9a-z]{7}$/);
|
|
271
319
|
});
|
|
272
320
|
|
|
273
|
-
it("redact 作用于 error
|
|
321
|
+
it("redact 作用于 error 的 message/cause/stack、diagnostic 的 message/data、断言 detail/evidence,不作用于身份与分类字段", async () => {
|
|
274
322
|
const s = snap({
|
|
275
323
|
experimentId: "exp/x",
|
|
276
324
|
results: [
|
|
277
|
-
res("A", "errored", {
|
|
325
|
+
res("A", "errored", {
|
|
326
|
+
error: {
|
|
327
|
+
code: "sandbox-provision-failed",
|
|
328
|
+
message: "ENOENT /Users/me/repo/tool",
|
|
329
|
+
phase: "sandbox.create",
|
|
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
|
+
}),
|
|
278
343
|
res("B", "failed", {
|
|
279
344
|
assertions: [
|
|
280
345
|
{
|
|
281
346
|
name: "includes",
|
|
282
347
|
severity: "gate",
|
|
283
348
|
score: 0,
|
|
284
|
-
|
|
349
|
+
outcome: "failed" as const,
|
|
285
350
|
detail: "missing text under /Users/me/repo/src",
|
|
286
351
|
evidence: "checked /Users/me/repo/src/app.ts",
|
|
287
352
|
},
|
|
@@ -291,10 +356,25 @@ describe("AttemptList.data", () => {
|
|
|
291
356
|
});
|
|
292
357
|
const items = await AttemptList.data([s], { redact: (text) => text.replaceAll("/Users/me/repo", "<repo>") });
|
|
293
358
|
const a = items.find((i) => i.evalId === "A")!;
|
|
294
|
-
|
|
359
|
+
// error:自由文本(message/stack/cause.message)全部经钩子
|
|
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");
|
|
295
374
|
const b = items.find((i) => i.evalId === "B")!;
|
|
296
|
-
|
|
297
|
-
expect(
|
|
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");
|
|
298
378
|
// 身份字段不经 redact
|
|
299
379
|
expect(items.every((i) => i.experimentId === "exp/x")).toBe(true);
|
|
300
380
|
});
|
|
@@ -302,7 +382,7 @@ describe("AttemptList.data", () => {
|
|
|
302
382
|
it("不预设只看失败:passed 的 attempt 同样列出,原样携带 assertions", async () => {
|
|
303
383
|
const s = snap({
|
|
304
384
|
experimentId: "exp/x",
|
|
305
|
-
results: [res("A", "passed", { assertions: [{ name: "ok", severity: "gate",
|
|
385
|
+
results: [res("A", "passed", { assertions: [{ name: "ok", severity: "gate", outcome: "passed" as const, score: 1 }] })],
|
|
306
386
|
});
|
|
307
387
|
const items = await AttemptList.data([s]);
|
|
308
388
|
expect(items).toHaveLength(1);
|
|
@@ -319,10 +399,10 @@ describe("两级聚合引擎", () => {
|
|
|
319
399
|
experimentId: "exp/x",
|
|
320
400
|
results: [res("A", "passed"), res("B", "failed"), res("B", "failed"), res("B", "failed")],
|
|
321
401
|
});
|
|
322
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [
|
|
402
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
|
|
323
403
|
expect(data.dimension).toBe("agent");
|
|
324
404
|
expect(data.rows).toHaveLength(1);
|
|
325
|
-
const cell = data.rows[0].cells["pass-rate"];
|
|
405
|
+
const cell = data.rows[0].cells["task-pass-rate"];
|
|
326
406
|
expect(cell.value).toBe(0.5);
|
|
327
407
|
expect(cell.display).toBe("50%");
|
|
328
408
|
expect(cell.samples).toBe(4);
|
|
@@ -342,17 +422,17 @@ describe("两级聚合引擎", () => {
|
|
|
342
422
|
experimentId: "exp/x",
|
|
343
423
|
results: [res("A", "failed"), res("A", "failed"), res("B", "failed"), res("B", "passed")],
|
|
344
424
|
});
|
|
345
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK,
|
|
425
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, taskPassRate] });
|
|
346
426
|
// A: max(0,0)=0;B: max(0,1)=1 → (0+1)/2
|
|
347
427
|
expect(data.rows[0].cells["pass@k"].value).toBe(0.5);
|
|
348
|
-
// 对照:默认 mean/mean 的
|
|
349
|
-
expect(data.rows[0].cells["pass-rate"].value).toBe(0.25);
|
|
428
|
+
// 对照:默认 mean/mean 的 taskPassRate = (0 + 0.5)/2
|
|
429
|
+
expect(data.rows[0].cells["task-pass-rate"].value).toBe(0.25);
|
|
350
430
|
});
|
|
351
431
|
|
|
352
432
|
it("skipped 是 null:不稀释均值,但计入 total(覆盖率如实)", async () => {
|
|
353
433
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "skipped")] });
|
|
354
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [
|
|
355
|
-
const cell = data.rows[0].cells["pass-rate"];
|
|
434
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
|
|
435
|
+
const cell = data.rows[0].cells["task-pass-rate"];
|
|
356
436
|
expect(cell.value).toBe(1); // B 整桶为 null,不参与 across,不是 0.5
|
|
357
437
|
expect(cell.samples).toBe(1);
|
|
358
438
|
expect(cell.total).toBe(2);
|
|
@@ -361,8 +441,8 @@ describe("两级聚合引擎", () => {
|
|
|
361
441
|
|
|
362
442
|
it("全组 null → value null、display 兜底,不编 0;refs 必填(空数组)", async () => {
|
|
363
443
|
const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
|
|
364
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [
|
|
365
|
-
const cell = data.rows[0].cells["pass-rate"];
|
|
444
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
|
|
445
|
+
const cell = data.rows[0].cells["task-pass-rate"];
|
|
366
446
|
expect(cell.value).toBeNull();
|
|
367
447
|
expect(cell.display).toBe("—");
|
|
368
448
|
expect(cell.samples).toBe(0);
|
|
@@ -397,18 +477,18 @@ describe("两级聚合引擎", () => {
|
|
|
397
477
|
res("A", "passed", { attempt: 2 }),
|
|
398
478
|
],
|
|
399
479
|
});
|
|
400
|
-
const data = await MetricTable.data([s], { rows: byParity, columns: [
|
|
480
|
+
const data = await MetricTable.data([s], { rows: byParity, columns: [taskPassRate] });
|
|
401
481
|
expect(data.dimension).toBe("parity");
|
|
402
|
-
const byKey = Object.fromEntries(data.rows.map((r) => [r.key, r.cells["pass-rate"].value]));
|
|
482
|
+
const byKey = Object.fromEntries(data.rows.map((r) => [r.key, r.cells["task-pass-rate"].value]));
|
|
403
483
|
// 同一道题的 attempt 分进两组:even 组内 [1,1] 折成 1,odd 组内 [0] 折成 0
|
|
404
484
|
expect(byKey).toEqual({ even: 1, odd: 0 });
|
|
405
485
|
});
|
|
406
486
|
|
|
407
487
|
it("同一次计算里指标重名是错误", async () => {
|
|
408
|
-
const dup = defineMetric({ name: "pass-rate", value: () => 1 });
|
|
488
|
+
const dup = defineMetric({ name: "task-pass-rate", value: () => 1 });
|
|
409
489
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
410
|
-
await expect(MetricTable.data([s], { rows: "agent", columns: [
|
|
411
|
-
/Duplicate metric name "pass-rate"/,
|
|
490
|
+
await expect(MetricTable.data([s], { rows: "agent", columns: [taskPassRate, dup] })).rejects.toThrow(
|
|
491
|
+
/Duplicate metric name "task-pass-rate"/,
|
|
412
492
|
);
|
|
413
493
|
});
|
|
414
494
|
|
|
@@ -418,16 +498,16 @@ describe("两级聚合引擎", () => {
|
|
|
418
498
|
const none = snap({ experimentId: "exp/none", agent: "none", results: [res("A", "skipped", { agent: "none" })] });
|
|
419
499
|
const data = await MetricTable.data([none, bad, good], {
|
|
420
500
|
rows: "agent",
|
|
421
|
-
columns: [
|
|
422
|
-
sort:
|
|
501
|
+
columns: [taskPassRate],
|
|
502
|
+
sort: taskPassRate,
|
|
423
503
|
});
|
|
424
504
|
expect(data.rows.map((r) => r.key)).toEqual(["good", "bad", "none"]);
|
|
425
505
|
});
|
|
426
506
|
|
|
427
507
|
it("列键是字面量联合:拼错列名编译不过", async () => {
|
|
428
508
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
429
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [
|
|
430
|
-
const cell: MetricCell = data.rows[0].cells[
|
|
509
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate, costUSD] });
|
|
510
|
+
const cell: MetricCell = data.rows[0].cells[taskPassRate.name]; // 键锚在指标对象上
|
|
431
511
|
expect(cell.value).toBe(1);
|
|
432
512
|
// @ts-expect-error 列里没有这个键 —— 编译期挡住,不是运行时 undefined
|
|
433
513
|
data.rows[0].cells["pass-rat"];
|
|
@@ -440,7 +520,7 @@ describe("examScore", () => {
|
|
|
440
520
|
it("errored(断言空数组)得 0 —— 不因「gate 全过」空真得满分", async () => {
|
|
441
521
|
const s = snap({
|
|
442
522
|
experimentId: "exp/x",
|
|
443
|
-
results: [res("A", "errored", { assertions: [], error: "adapter crashed" })],
|
|
523
|
+
results: [res("A", "errored", { assertions: [], error: erroredWith("adapter crashed") })],
|
|
444
524
|
});
|
|
445
525
|
const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
|
|
446
526
|
const cell = data.rows[0].cells["exam-score"];
|
|
@@ -465,7 +545,7 @@ describe("examScore", () => {
|
|
|
465
545
|
assertions: [
|
|
466
546
|
softAssertion("judge-1", 0.5),
|
|
467
547
|
softAssertion("judge-2", 1),
|
|
468
|
-
{ name: "includes", severity: "gate", score: 1,
|
|
548
|
+
{ name: "includes", severity: "gate", score: 1, outcome: "passed" as const },
|
|
469
549
|
],
|
|
470
550
|
}),
|
|
471
551
|
],
|
|
@@ -474,7 +554,7 @@ describe("examScore", () => {
|
|
|
474
554
|
experimentId: "exp/b",
|
|
475
555
|
results: [
|
|
476
556
|
res("B", "passed", {
|
|
477
|
-
assertions: [{ name: "includes", severity: "gate", score: 1,
|
|
557
|
+
assertions: [{ name: "includes", severity: "gate", score: 1, outcome: "passed" as const }],
|
|
478
558
|
}),
|
|
479
559
|
],
|
|
480
560
|
});
|
|
@@ -491,6 +571,62 @@ describe("examScore", () => {
|
|
|
491
571
|
});
|
|
492
572
|
});
|
|
493
573
|
|
|
574
|
+
// ───────────────────────── 三个通过率指标(docs/feature/reports/library.md「内置指标」)─────────────────────────
|
|
575
|
+
//
|
|
576
|
+
// 同一份 passed / failed / errored / skipped 混合 fixture 上,三个口径必须给出三个不同的数,
|
|
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",
|
|
586
|
+
results: [
|
|
587
|
+
res("A", "passed"),
|
|
588
|
+
res("B", "failed"),
|
|
589
|
+
res("C", "errored", { error: erroredWith("adapter crashed") }),
|
|
590
|
+
res("D", "skipped"),
|
|
591
|
+
],
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("taskPassRate:errored 记 null 不进分母 —— 基建故障不伪装成 Agent 答错", async () => {
|
|
595
|
+
const data = await MetricTable.data([mixed()], { rows: "agent", columns: [taskPassRate] });
|
|
596
|
+
const cell = data.rows[0].cells["task-pass-rate"];
|
|
597
|
+
expect(cell.value).toBeCloseTo(0.5, 10); // (1 + 0) / 2:errored 与 skipped 都不在分母
|
|
598
|
+
expect(cell.samples).toBe(2); // errored、skipped 都是 null,不算有效样本
|
|
599
|
+
expect(cell.total).toBe(4);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("executionReliability:跑到可判定(passed/failed)= 1,errored = 0,skipped → null", async () => {
|
|
603
|
+
const data = await MetricTable.data([mixed()], { rows: "agent", columns: [executionReliability] });
|
|
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);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("全部 errored:taskPassRate 缺数据(null),executionReliability 如实 0 —— 两个指标不互相冒充", async () => {
|
|
619
|
+
const s = snap({
|
|
620
|
+
experimentId: "exp/broken",
|
|
621
|
+
results: [res("A", "errored", { error: erroredWith("boom") })],
|
|
622
|
+
});
|
|
623
|
+
const task = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
|
|
624
|
+
expect(task.rows[0].cells["task-pass-rate"].value).toBeNull(); // 没有一道题真正被答过
|
|
625
|
+
const reliability = await MetricTable.data([s], { rows: "agent", columns: [executionReliability] });
|
|
626
|
+
expect(reliability.rows[0].cells["execution-reliability"].value).toBe(0);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
494
630
|
// ───────────────────────── 内置指标口径 ─────────────────────────
|
|
495
631
|
|
|
496
632
|
describe("内置指标", () => {
|
|
@@ -651,7 +787,7 @@ describe("MetricScatter.data", () => {
|
|
|
651
787
|
points: "experiment",
|
|
652
788
|
series: "agent",
|
|
653
789
|
x: costUSD,
|
|
654
|
-
y:
|
|
790
|
+
y: taskPassRate,
|
|
655
791
|
});
|
|
656
792
|
expect(data.points).toBe("experiment");
|
|
657
793
|
expect(data.series).toBe("agent");
|
|
@@ -676,7 +812,7 @@ describe("flag()", () => {
|
|
|
676
812
|
const withFlags = (id: string, flags: Record<string, unknown> | undefined, verdict: Verdict) =>
|
|
677
813
|
snap({
|
|
678
814
|
experimentId: id,
|
|
679
|
-
results: [res("A", verdict, { experimentId: id, experiment: {
|
|
815
|
+
results: [res("A", verdict, { experimentId: id, experiment: { flags, runs: 1, earlyExit: true, selectedEvalIds: [] } as import("../types.ts").ExperimentRunInfo })],
|
|
680
816
|
});
|
|
681
817
|
|
|
682
818
|
it("MetricLine.data:x 收 flag、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
|
|
@@ -686,7 +822,7 @@ describe("flag()", () => {
|
|
|
686
822
|
const data = await MetricLine.data([s1, s2, legacy], {
|
|
687
823
|
x: flag("latencyMs", { label: "Simulated latency", unit: "ms" }),
|
|
688
824
|
series: flag("agents", { label: (v) => `${v} agents` }),
|
|
689
|
-
y:
|
|
825
|
+
y: taskPassRate,
|
|
690
826
|
});
|
|
691
827
|
expect(data.x).toEqual({ key: "latencyMs", label: "Simulated latency", unit: "ms" });
|
|
692
828
|
expect(data.series).toBe("agents");
|
|
@@ -710,13 +846,84 @@ describe("flag()", () => {
|
|
|
710
846
|
const s2 = withFlags("exp/b", { agents: 16 }, "failed");
|
|
711
847
|
const data = await MetricTable.data([s1, s2], {
|
|
712
848
|
rows: flag("agents", { label: (v) => `${v} agents` }),
|
|
713
|
-
columns: [
|
|
849
|
+
columns: [taskPassRate],
|
|
714
850
|
});
|
|
715
851
|
expect(data.dimension).toBe("agents");
|
|
716
852
|
expect(data.rows.map((r) => r.key)).toEqual(["1 agents", "16 agents"]);
|
|
717
853
|
});
|
|
718
854
|
});
|
|
719
855
|
|
|
856
|
+
// ───────────────────────── config():顶层运行配置当维度与轴 ─────────────────────────
|
|
857
|
+
// flag() 只读 ExperimentDef.flags 的显式声明;model / reasoningEffort / budget / runs 这类
|
|
858
|
+
// 顶层运行配置用 config() 读快照的 ExperimentRunInfo 投影,外加桥接 model / agent 两个键
|
|
859
|
+
// (docs/feature/reports/library.md「维度与 flags」)。
|
|
860
|
+
|
|
861
|
+
describe("config()", () => {
|
|
862
|
+
/** 快照声明 ExperimentRunInfo 投影;字段全集以 docs 的接口为准,这里按键名读值。 */
|
|
863
|
+
const withConfig = (id: string, info: Record<string, unknown> | undefined, verdict: Verdict, model?: string) => {
|
|
864
|
+
const s = snap({ experimentId: id, model, results: [res("A", verdict, { experimentId: id })] });
|
|
865
|
+
if (info) s.experiment = info as unknown as (typeof s)["experiment"];
|
|
866
|
+
return s;
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
it("config 当维度用:按投影值分组;未投影的 experiment 如实归 (unset)", async () => {
|
|
870
|
+
const high = withConfig("effort/high", { reasoningEffort: "high" }, "passed");
|
|
871
|
+
const low = withConfig("effort/low", { reasoningEffort: "low" }, "failed");
|
|
872
|
+
const legacy = withConfig("effort/legacy", undefined, "passed");
|
|
873
|
+
const data = await MetricTable.data([high, low, legacy], {
|
|
874
|
+
rows: config("reasoningEffort", { label: "Reasoning effort" }),
|
|
875
|
+
columns: [taskPassRate],
|
|
876
|
+
});
|
|
877
|
+
expect(data.dimension).toBe("reasoningEffort");
|
|
878
|
+
expect(data.rows.map((r) => r.key)).toEqual(["high", "low", "(unset)"]);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it("config 当数值轴用(MetricLine.data 的 x):非数值或未投影 → x=null 不伪造,注脚可数", async () => {
|
|
882
|
+
const b1 = withConfig("budget/one", { budget: 1 }, "passed");
|
|
883
|
+
const b2 = withConfig("budget/two", { budget: 2 }, "failed");
|
|
884
|
+
const none = withConfig("budget/none", { reasoningEffort: "high" }, "passed"); // 没投影 budget
|
|
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);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it("model / agent 两个键桥接到快照顶层权威字段,ExperimentRunInfo 不复制这两个事实", async () => {
|
|
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"]);
|
|
907
|
+
|
|
908
|
+
const byAgent = await MetricTable.data([mini, maxi], { rows: config("agent"), columns: [taskPassRate] });
|
|
909
|
+
expect(byAgent.rows.map((r) => r.key)).toEqual(["agent-x"]); // res() 默认 agent-x
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it("label 函数折组名,与 flag() 同一套规则", async () => {
|
|
913
|
+
const s1 = withConfig("runs/3", { runs: 3 }, "passed");
|
|
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
|
+
});
|
|
921
|
+
|
|
922
|
+
it("空 name 直接报错,与 flag() 同一门槛", () => {
|
|
923
|
+
expect(() => config("")).toThrow(/non-empty string/);
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
|
|
720
927
|
// ───────────────────────── MetricTable.data:没有实体下钻 ─────────────────────────
|
|
721
928
|
|
|
722
929
|
describe("MetricTable.data", () => {
|
|
@@ -725,7 +932,7 @@ describe("MetricTable.data", () => {
|
|
|
725
932
|
experimentId: "exp/x",
|
|
726
933
|
results: [res("A", "passed"), res("B", "failed")],
|
|
727
934
|
});
|
|
728
|
-
const data = await MetricTable.data([s], { rows: "experiment", columns: [
|
|
935
|
+
const data = await MetricTable.data([s], { rows: "experiment", columns: [taskPassRate] });
|
|
729
936
|
expect(data.rows).toHaveLength(1);
|
|
730
937
|
expect(data.rows[0]!.meta).not.toHaveProperty("subRows");
|
|
731
938
|
// 展开到 Eval 是 ExperimentList 的职责,不在这里验证 —— 见 ExperimentList.data 的测试组。
|
|
@@ -740,10 +947,10 @@ describe("MetricTable.data", () => {
|
|
|
740
947
|
describe("reasonFor", () => {
|
|
741
948
|
it("同一 result 同时含 error、失败 gate、失败 soft → 只显示 error", () => {
|
|
742
949
|
const result = res("A", "errored", {
|
|
743
|
-
error: "adapter crashed",
|
|
950
|
+
error: erroredWith("adapter crashed"),
|
|
744
951
|
assertions: [
|
|
745
|
-
{ name: "includes", severity: "gate", score: 0,
|
|
746
|
-
softAssertion("judge", 0.2, {
|
|
952
|
+
{ name: "includes", severity: "gate", score: 0, outcome: "failed" as const, detail: "missing text" },
|
|
953
|
+
softAssertion("judge", 0.2, { outcome: "failed" as const }),
|
|
747
954
|
],
|
|
748
955
|
});
|
|
749
956
|
expect(reasonFor(result)).toBe("adapter crashed");
|
|
@@ -752,21 +959,21 @@ describe("reasonFor", () => {
|
|
|
752
959
|
it("skipReason 优先于 gate 断言,但让位给 error", () => {
|
|
753
960
|
const result = res("A", "skipped", {
|
|
754
961
|
skipReason: "missing fixture",
|
|
755
|
-
assertions: [{ name: "includes", severity: "gate", score: 0,
|
|
962
|
+
assertions: [{ name: "includes", severity: "gate", score: 0, outcome: "failed" as const }],
|
|
756
963
|
});
|
|
757
964
|
expect(reasonFor(result)).toBe("missing fixture");
|
|
758
965
|
});
|
|
759
966
|
|
|
760
|
-
it("
|
|
967
|
+
it("只展示第一条失败 gate,其余 gate 计数;失败 soft 不进入 gate 计数", () => {
|
|
761
968
|
const result = res("A", "failed", {
|
|
762
969
|
assertions: [
|
|
763
|
-
softAssertion("judge-first", 0.1, {
|
|
764
|
-
{ name: "includes", severity: "gate", score: 0,
|
|
765
|
-
{ name: "matches", severity: "gate", score: 0,
|
|
766
|
-
softAssertion("judge-last", 0.3, {
|
|
970
|
+
softAssertion("judge-first", 0.1, { outcome: "failed" as const }), // 声明顺序第一,但 soft 不进 reason
|
|
971
|
+
{ name: "includes", groupPath: ["answer contains required text"], severity: "gate", score: 0, outcome: "failed" as const, expected: "contains Brooklyn", received: "Manhattan" },
|
|
972
|
+
{ name: "matches", severity: "gate", score: 0, outcome: "failed" as const }, // 无 detail,只有 name
|
|
973
|
+
softAssertion("judge-last", 0.3, { outcome: "failed" as const }), // soft 依旧不进 reason
|
|
767
974
|
],
|
|
768
975
|
});
|
|
769
|
-
expect(reasonFor(result)).toBe("
|
|
976
|
+
expect(reasonFor(result)).toBe("gate: answer contains required text · includes · expected contains Brooklyn · received Manhattan · +1 more failures");
|
|
770
977
|
});
|
|
771
978
|
|
|
772
979
|
it("都缺席 → 无原因", () => {
|
|
@@ -813,6 +1020,22 @@ describe("RunOverview.data", () => {
|
|
|
813
1020
|
const data = await RunOverview.data(selection([s], [warning]));
|
|
814
1021
|
expect(data.warnings).toEqual([warning]);
|
|
815
1022
|
});
|
|
1023
|
+
|
|
1024
|
+
it("默认成功率把 errored 记 0:2 passed / 5 errored = 2/7,不是 100% 2/7", async () => {
|
|
1025
|
+
const s = snap({
|
|
1026
|
+
experimentId: "exp/unstable",
|
|
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
|
+
],
|
|
1032
|
+
});
|
|
1033
|
+
const data = await RunOverview.data([s]);
|
|
1034
|
+
expect(data.totals.passRate.value).toBeCloseTo(2 / 7, 10);
|
|
1035
|
+
expect(data.totals.passRate.display).toBe("28.6%");
|
|
1036
|
+
expect(data.totals.passRate.samples).toBe(7);
|
|
1037
|
+
expect(data.totals.passRate.total).toBe(7);
|
|
1038
|
+
});
|
|
816
1039
|
});
|
|
817
1040
|
|
|
818
1041
|
// ───────────────────────── GroupSummary.data ─────────────────────────
|
|
@@ -857,16 +1080,6 @@ describe("GroupSummary.data", () => {
|
|
|
857
1080
|
expect(data.lastRunAt).toBe(expB.startedAt);
|
|
858
1081
|
});
|
|
859
1082
|
|
|
860
|
-
it('跨 experiment 同名 eval 不合并:两个 experiment 各自的 "x" 独立计一票,不是被折成一票', async () => {
|
|
861
|
-
const a = snap({ experimentId: "exp/a", results: [res("x", "passed")] });
|
|
862
|
-
const b = snap({ experimentId: "exp/b", results: [res("x", "failed")] });
|
|
863
|
-
const data = await GroupSummary.data([a, b]);
|
|
864
|
-
expect(data.experiments).toBe(2);
|
|
865
|
-
expect(data.evals).toBe(2); // 只按 eval id 折叠(误把两个 experiment 的 "x" 当一道题)会变成 1
|
|
866
|
-
expect(data.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
|
|
867
|
-
expect(data.passRate.value).toBeCloseTo(0.5, 10); // 误合并会因为「任一轮过即过」变成 100%
|
|
868
|
-
});
|
|
869
|
-
|
|
870
1083
|
it("全组没有任何 attempt 报成本 → totalCostUSD null,不编 0", async () => {
|
|
871
1084
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
|
|
872
1085
|
const data = await GroupSummary.data([s]);
|
|
@@ -883,30 +1096,12 @@ describe("GroupSummary.data", () => {
|
|
|
883
1096
|
expect(data.passRate.total).toBe(2);
|
|
884
1097
|
});
|
|
885
1098
|
|
|
886
|
-
it("lastRunAt 只从传入的组 Selection 取值,不读全局最新 —— 组外快照更晚也不影响", async () => {
|
|
887
|
-
const inGroup = snap({
|
|
888
|
-
experimentId: "exp/a",
|
|
889
|
-
runStartedAt: "2026-07-01T08:00:00Z",
|
|
890
|
-
results: [res("A", "passed")],
|
|
891
|
-
});
|
|
892
|
-
const outOfGroup = snap({
|
|
893
|
-
experimentId: "exp/b",
|
|
894
|
-
runStartedAt: "2026-07-09T08:00:00Z",
|
|
895
|
-
results: [res("B", "passed")],
|
|
896
|
-
});
|
|
897
|
-
const sel = selection([inGroup, outOfGroup], []);
|
|
898
|
-
const scoped = sel.filter((s) => s.experimentId === "exp/a"); // 只留组内快照,模拟按实验组收窄的 Selection
|
|
899
|
-
const data = await GroupSummary.data(scoped);
|
|
900
|
-
expect(data.lastRunAt).toBe(inGroup.startedAt);
|
|
901
|
-
expect(data.lastRunAt).not.toBe(outOfGroup.startedAt);
|
|
902
|
-
});
|
|
903
|
-
|
|
904
1099
|
it('MetricTable rows: "experiment" 的 verdicts meta 复用同一份 eval 级统计,行为不变(回归)', async () => {
|
|
905
1100
|
const s = snap({
|
|
906
1101
|
experimentId: "exp/x",
|
|
907
1102
|
results: [res("A", "passed", { attempt: 0 }), res("A", "failed", { attempt: 1 }), res("B", "failed")],
|
|
908
1103
|
});
|
|
909
|
-
const data = await MetricTable.data([s], { rows: "experiment", columns: [
|
|
1104
|
+
const data = await MetricTable.data([s], { rows: "experiment", columns: [taskPassRate] });
|
|
910
1105
|
// A 两轮折成 passed(任一轮过即过),B 失败:1 passed / 1 failed
|
|
911
1106
|
expect(data.rows[0].meta?.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
|
|
912
1107
|
});
|
|
@@ -926,7 +1121,7 @@ describe("DeltaTable.data", () => {
|
|
|
926
1121
|
});
|
|
927
1122
|
const data = await DeltaTable.data([base, plus], {
|
|
928
1123
|
pairs: [{ a: "exp/base", b: "exp/plus", label: "memory" }],
|
|
929
|
-
metrics: [
|
|
1124
|
+
metrics: [taskPassRate, costUSD],
|
|
930
1125
|
});
|
|
931
1126
|
expect(data.rows).toHaveLength(1);
|
|
932
1127
|
const row = data.rows[0];
|
|
@@ -934,7 +1129,7 @@ describe("DeltaTable.data", () => {
|
|
|
934
1129
|
expect(row.a).toEqual({ experimentId: "exp/base" });
|
|
935
1130
|
expect(row.b).toEqual({ experimentId: "exp/plus" });
|
|
936
1131
|
|
|
937
|
-
const pass = row.cells["pass-rate"];
|
|
1132
|
+
const pass = row.cells["task-pass-rate"];
|
|
938
1133
|
expect(pass.a.value).toBe(0.5);
|
|
939
1134
|
expect(pass.b.value).toBe(1);
|
|
940
1135
|
expect(pass.delta).toBeCloseTo(0.5);
|
|
@@ -948,9 +1143,9 @@ describe("DeltaTable.data", () => {
|
|
|
948
1143
|
|
|
949
1144
|
const flat = await DeltaTable.data([base, base], {
|
|
950
1145
|
pairs: [{ a: "exp/base", b: "exp/base", label: "same" }],
|
|
951
|
-
metrics: [
|
|
1146
|
+
metrics: [taskPassRate],
|
|
952
1147
|
});
|
|
953
|
-
expect(flat.rows[0].cells["pass-rate"].display).toBe("±0");
|
|
1148
|
+
expect(flat.rows[0].cells["task-pass-rate"].display).toBe("±0");
|
|
954
1149
|
});
|
|
955
1150
|
|
|
956
1151
|
it("时间轴对比:pairs 的 a/b 收快照键 <experimentId> @ <startedAt>", async () => {
|
|
@@ -972,9 +1167,9 @@ describe("DeltaTable.data", () => {
|
|
|
972
1167
|
label: "this week vs last",
|
|
973
1168
|
},
|
|
974
1169
|
],
|
|
975
|
-
metrics: [
|
|
1170
|
+
metrics: [taskPassRate],
|
|
976
1171
|
});
|
|
977
|
-
const cell = data.rows[0].cells["pass-rate"];
|
|
1172
|
+
const cell = data.rows[0].cells["task-pass-rate"];
|
|
978
1173
|
expect(cell.a.value).toBe(0); // 旧快照那份
|
|
979
1174
|
expect(cell.b.value).toBe(1); // 新快照那份
|
|
980
1175
|
expect(cell.display).toBe("+100%");
|
|
@@ -1006,8 +1201,8 @@ describe("身份键去重", () => {
|
|
|
1006
1201
|
[older, newer],
|
|
1007
1202
|
[newer, older],
|
|
1008
1203
|
]) {
|
|
1009
|
-
const data = await MetricTable.data(order, { rows: "agent", columns: [
|
|
1010
|
-
const cell = data.rows[0].cells["pass-rate"];
|
|
1204
|
+
const data = await MetricTable.data(order, { rows: "agent", columns: [taskPassRate] });
|
|
1205
|
+
const cell = data.rows[0].cells["task-pass-rate"];
|
|
1011
1206
|
expect(cell.total).toBe(1); // 两份只算一份
|
|
1012
1207
|
expect(cell.value).toBe(1); // 留的是最新 run 里的 passed,与快照传入顺序无关
|
|
1013
1208
|
|
|
@@ -1073,10 +1268,10 @@ describe("MetricMatrix.data", () => {
|
|
|
1073
1268
|
agent: "b1",
|
|
1074
1269
|
results: [res("A", "failed", { agent: "b1" })], // b1 没跑 B
|
|
1075
1270
|
});
|
|
1076
|
-
const data = await MetricMatrix.data([a, b], { rows: "eval", columns: "agent", cell:
|
|
1271
|
+
const data = await MetricMatrix.data([a, b], { rows: "eval", columns: "agent", cell: taskPassRate });
|
|
1077
1272
|
expect(data.rows).toBe("eval");
|
|
1078
1273
|
expect(data.columns).toBe("agent");
|
|
1079
|
-
expect(data.metric.key).toBe("pass-rate");
|
|
1274
|
+
expect(data.metric.key).toBe("task-pass-rate");
|
|
1080
1275
|
expect(data.cells).toHaveLength(3); // A×a1、B×a1、A×b1;B×b1 不出现
|
|
1081
1276
|
const find = (row: string, column: string) => data.cells.find((c) => c.row === row && c.column === column);
|
|
1082
1277
|
expect(find("A", "a1")?.cell.value).toBe(1);
|
|
@@ -1085,7 +1280,20 @@ describe("MetricMatrix.data", () => {
|
|
|
1085
1280
|
expect(find("B", "b1")).toBeUndefined();
|
|
1086
1281
|
});
|
|
1087
1282
|
|
|
1088
|
-
it("MetricBars.data
|
|
1089
|
-
|
|
1283
|
+
it("MetricBars.data 与 MetricMatrix.data 同一输入给出相同数据(别名契约,不锁函数引用)", async () => {
|
|
1284
|
+
const a = snap({
|
|
1285
|
+
experimentId: "exp/a",
|
|
1286
|
+
agent: "a1",
|
|
1287
|
+
results: [res("A", "passed", { agent: "a1" }), res("B", "failed", { agent: "a1" })],
|
|
1288
|
+
});
|
|
1289
|
+
const b = snap({
|
|
1290
|
+
experimentId: "exp/b",
|
|
1291
|
+
agent: "b1",
|
|
1292
|
+
results: [res("A", "failed", { agent: "b1" })],
|
|
1293
|
+
});
|
|
1294
|
+
const options = { rows: "eval", columns: "agent", cell: taskPassRate } as const;
|
|
1295
|
+
const bars = await MetricBars.data([a, b], options);
|
|
1296
|
+
const matrix = await MetricMatrix.data([a, b], options);
|
|
1297
|
+
expect(bars).toEqual(matrix);
|
|
1090
1298
|
});
|
|
1091
1299
|
});
|