niceeval 0.6.0 → 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 +72 -6
- 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 -0
- package/dist/report/built-ins/experiment-comparison.js +119 -0
- package/dist/report/built-ins/index.d.ts +2 -1
- package/dist/report/built-ins/index.js +2 -2
- package/dist/report/components.d.ts +10 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +68 -66
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -14
- package/dist/report/format.js +28 -30
- package/dist/report/index.d.ts +5 -4
- package/dist/report/index.js +6 -5
- package/dist/report/locale.d.ts +23 -3
- package/dist/report/locale.js +47 -6
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +66 -15
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +4 -4
- package/dist/report/react/AttemptList.js +8 -10
- 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 +4 -2
- package/dist/report/react/ExperimentList.js +57 -7
- package/dist/report/react/MetricScatter.js +6 -16
- 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 +30 -18
- package/dist/report/react/format.d.ts +1 -1
- package/dist/report/react/format.js +1 -1
- package/dist/report/react/index.d.ts +1 -1
- 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 +100 -61
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +40 -34
- 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 +24 -6
- package/docs-site/zh/concepts/assert.mdx +11 -10
- package/docs-site/zh/concepts/evals.mdx +7 -6
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +7 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +35 -31
- 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/fixtures.mdx +3 -1
- package/docs-site/zh/guides/official-adapters.mdx +27 -3
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +49 -37
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +42 -8
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +57 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +258 -10
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +85 -41
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +19 -2
- 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 +35 -9
- package/docs-site/zh/reference/define-agent.mdx +60 -5
- 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/events.mdx +2 -2
- package/docs-site/zh/reference/expect.mdx +36 -6
- 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 +73 -6
- 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 +65 -4
- package/src/i18n/zh-CN.ts +66 -4
- 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 +127 -173
- package/src/report/built-ins/experiment-comparison.tsx +179 -0
- package/src/report/built-ins/index.ts +7 -2
- package/src/report/components.tsx +11 -3
- package/src/report/compute.ts +80 -74
- package/src/report/dual-render.test.tsx +222 -91
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +36 -27
- package/src/report/index.ts +23 -6
- package/src/report/locale.ts +49 -6
- package/src/report/metrics.ts +68 -15
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +9 -36
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +173 -55
- package/src/report/react/MetricScatter.tsx +13 -25
- 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 +72 -1
- package/src/report/react/fixtures.ts +34 -21
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +0 -1
- package/src/report/react/render.test.tsx +30 -69
- package/src/report/react/styles.css +112 -14
- package/src/report/report.test.ts +308 -105
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +111 -67
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +42 -34
- 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 +11 -9
- 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 +29 -16
- package/src/show/render.ts +626 -308
- package/src/show/show.test.ts +251 -36
- 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 +2 -1
- package/src/view/data.ts +17 -7
- package/src/view/index.ts +12 -1
- package/src/view/server.ts +2 -0
- package/src/view/shared/types.ts +1 -1
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +11 -10
- package/dist/o11y/execution-tree.d.ts +0 -103
- package/dist/o11y/otlp/select.d.ts +0 -22
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
- package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
- package/dist/results/annotated-source.d.ts +0 -61
- package/dist/results/attempt-evidence.d.ts +0 -69
- package/dist/results/attempt-source.d.ts +0 -15
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
- 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。 */
|
|
@@ -136,7 +152,7 @@ function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selecti
|
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
describe("ExperimentList.data", () => {
|
|
139
|
-
it("身份、flags、Eval 判定构成、官方两级聚合汇总指标,展开到 Eval
|
|
155
|
+
it("身份、flags、Eval 判定构成、官方两级聚合汇总指标,展开到 Eval 携带全部 Attempt locator", async () => {
|
|
140
156
|
const s = snap({
|
|
141
157
|
experimentId: "group/codex-e2b--mempal",
|
|
142
158
|
agent: "codex",
|
|
@@ -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
|
|
|
@@ -181,14 +205,13 @@ describe("ExperimentList.data", () => {
|
|
|
181
205
|
expect(evalA.verdict).toBe("passed"); // 任一轮通过 → 该题通过
|
|
182
206
|
expect(evalA.attempts.map((a) => a.attempt)).toEqual([0, 1]);
|
|
183
207
|
expect(evalA.attempts[0]!.locator).toMatch(/^@1[0-9a-z]{7}$/);
|
|
184
|
-
expect(evalA.attempts[0]!.capabilities.execution).toBe(true); // attempt0 的 hasEvents:true
|
|
185
|
-
expect(evalA.attempts[1]!.capabilities.execution).toBe(false);
|
|
186
208
|
// 每次 attempt 独立编码,即使同一道题也不撞车
|
|
187
209
|
expect(evalA.attempts[0]!.locator).not.toBe(evalA.attempts[1]!.locator);
|
|
188
210
|
|
|
189
211
|
const evalB = item.evalRows.find((e) => e.evalId === "memory/b")!;
|
|
190
212
|
expect(evalB.verdict).toBe("errored");
|
|
191
|
-
expect(evalB
|
|
213
|
+
expect(evalB).not.toHaveProperty("reason");
|
|
214
|
+
expect(evalB.attempts[0]!.error?.message).toBe("timeout");
|
|
192
215
|
});
|
|
193
216
|
|
|
194
217
|
it("展开数组按 evalId 升序,顶层数组按 experimentId 升序", async () => {
|
|
@@ -198,10 +221,29 @@ describe("ExperimentList.data", () => {
|
|
|
198
221
|
expect(items.map((i) => i.experimentId)).toEqual(["exp/a", "exp/b"]);
|
|
199
222
|
expect(items.find((i) => i.experimentId === "exp/b")!.evalRows.map((e) => e.evalId)).toEqual(["a", "z"]);
|
|
200
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
|
+
});
|
|
201
243
|
});
|
|
202
244
|
|
|
203
245
|
describe("EvalList.data", () => {
|
|
204
|
-
it("每项一个 experimentId + evalId
|
|
246
|
+
it("每项一个 experimentId + evalId,父项保留判定与题级聚合并展开到含失败事实的 Attempt", async () => {
|
|
205
247
|
const s = snap({
|
|
206
248
|
experimentId: "compare/codex",
|
|
207
249
|
results: [
|
|
@@ -209,13 +251,13 @@ describe("EvalList.data", () => {
|
|
|
209
251
|
attempt: 0,
|
|
210
252
|
durationMs: 40_000,
|
|
211
253
|
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.02 },
|
|
212
|
-
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" }],
|
|
213
255
|
}),
|
|
214
256
|
res("weather/brooklyn", "failed", {
|
|
215
257
|
attempt: 1,
|
|
216
258
|
durationMs: 42_000,
|
|
217
259
|
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.06 },
|
|
218
|
-
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" }],
|
|
219
261
|
}),
|
|
220
262
|
],
|
|
221
263
|
});
|
|
@@ -225,11 +267,15 @@ describe("EvalList.data", () => {
|
|
|
225
267
|
expect(item.evalId).toBe("weather/brooklyn");
|
|
226
268
|
expect(item.experimentId).toBe("compare/codex");
|
|
227
269
|
expect(item.verdict).toBe("failed");
|
|
228
|
-
expect(item
|
|
270
|
+
expect(item).not.toHaveProperty("reason");
|
|
229
271
|
expect(item.score.value).toBe(0); // examScore: failed → 0
|
|
230
272
|
expect(item.duration.value).toBeCloseTo(41_000); // 平均耗时
|
|
231
273
|
expect(item.cost.value).toBeCloseTo(0.04); // 平均成本
|
|
232
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");
|
|
233
279
|
expect(item.attempts[0]!.locator).not.toBe(item.attempts[1]!.locator);
|
|
234
280
|
});
|
|
235
281
|
|
|
@@ -243,14 +289,14 @@ describe("EvalList.data", () => {
|
|
|
243
289
|
});
|
|
244
290
|
|
|
245
291
|
describe("AttemptList.data", () => {
|
|
246
|
-
it("每项一个 Attempt:身份、判定、assertions
|
|
292
|
+
it("每项一个 Attempt:身份、判定、assertions、耗时、成本与 locator", async () => {
|
|
247
293
|
const s = snap({
|
|
248
294
|
experimentId: "exp/x",
|
|
249
295
|
results: [
|
|
250
296
|
res("A", "failed", {
|
|
251
297
|
durationMs: 5_000,
|
|
252
298
|
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.05 },
|
|
253
|
-
assertions: [{ name: "gate", severity: "gate",
|
|
299
|
+
assertions: [{ name: "gate", severity: "gate", outcome: "failed" as const, score: 0, detail: "nope" }],
|
|
254
300
|
hasEvents: true,
|
|
255
301
|
hasSources: true,
|
|
256
302
|
}),
|
|
@@ -270,23 +316,37 @@ describe("AttemptList.data", () => {
|
|
|
270
316
|
});
|
|
271
317
|
expect(item.assertions).toHaveLength(1);
|
|
272
318
|
expect(item.locator).toMatch(/^@1[0-9a-z]{7}$/);
|
|
273
|
-
// 证据能力:cheap 计算,不调用完整 loadAttemptEvidence——execution 来自 hasEvents,
|
|
274
|
-
// eval 来自 hasSources,diff 懒加载(fixture 的 diff() 恒返回 null → false)。
|
|
275
|
-
expect(item.capabilities).toEqual({ eval: true, execution: true, timing: false, diff: false });
|
|
276
319
|
});
|
|
277
320
|
|
|
278
|
-
it("redact 作用于 error
|
|
321
|
+
it("redact 作用于 error 的 message/cause/stack、diagnostic 的 message/data、断言 detail/evidence,不作用于身份与分类字段", async () => {
|
|
279
322
|
const s = snap({
|
|
280
323
|
experimentId: "exp/x",
|
|
281
324
|
results: [
|
|
282
|
-
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
|
+
}),
|
|
283
343
|
res("B", "failed", {
|
|
284
344
|
assertions: [
|
|
285
345
|
{
|
|
286
346
|
name: "includes",
|
|
287
347
|
severity: "gate",
|
|
288
348
|
score: 0,
|
|
289
|
-
|
|
349
|
+
outcome: "failed" as const,
|
|
290
350
|
detail: "missing text under /Users/me/repo/src",
|
|
291
351
|
evidence: "checked /Users/me/repo/src/app.ts",
|
|
292
352
|
},
|
|
@@ -296,10 +356,25 @@ describe("AttemptList.data", () => {
|
|
|
296
356
|
});
|
|
297
357
|
const items = await AttemptList.data([s], { redact: (text) => text.replaceAll("/Users/me/repo", "<repo>") });
|
|
298
358
|
const a = items.find((i) => i.evalId === "A")!;
|
|
299
|
-
|
|
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");
|
|
300
374
|
const b = items.find((i) => i.evalId === "B")!;
|
|
301
|
-
|
|
302
|
-
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");
|
|
303
378
|
// 身份字段不经 redact
|
|
304
379
|
expect(items.every((i) => i.experimentId === "exp/x")).toBe(true);
|
|
305
380
|
});
|
|
@@ -307,7 +382,7 @@ describe("AttemptList.data", () => {
|
|
|
307
382
|
it("不预设只看失败:passed 的 attempt 同样列出,原样携带 assertions", async () => {
|
|
308
383
|
const s = snap({
|
|
309
384
|
experimentId: "exp/x",
|
|
310
|
-
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 }] })],
|
|
311
386
|
});
|
|
312
387
|
const items = await AttemptList.data([s]);
|
|
313
388
|
expect(items).toHaveLength(1);
|
|
@@ -324,10 +399,10 @@ describe("两级聚合引擎", () => {
|
|
|
324
399
|
experimentId: "exp/x",
|
|
325
400
|
results: [res("A", "passed"), res("B", "failed"), res("B", "failed"), res("B", "failed")],
|
|
326
401
|
});
|
|
327
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [
|
|
402
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
|
|
328
403
|
expect(data.dimension).toBe("agent");
|
|
329
404
|
expect(data.rows).toHaveLength(1);
|
|
330
|
-
const cell = data.rows[0].cells["pass-rate"];
|
|
405
|
+
const cell = data.rows[0].cells["task-pass-rate"];
|
|
331
406
|
expect(cell.value).toBe(0.5);
|
|
332
407
|
expect(cell.display).toBe("50%");
|
|
333
408
|
expect(cell.samples).toBe(4);
|
|
@@ -347,17 +422,17 @@ describe("两级聚合引擎", () => {
|
|
|
347
422
|
experimentId: "exp/x",
|
|
348
423
|
results: [res("A", "failed"), res("A", "failed"), res("B", "failed"), res("B", "passed")],
|
|
349
424
|
});
|
|
350
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK,
|
|
425
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, taskPassRate] });
|
|
351
426
|
// A: max(0,0)=0;B: max(0,1)=1 → (0+1)/2
|
|
352
427
|
expect(data.rows[0].cells["pass@k"].value).toBe(0.5);
|
|
353
|
-
// 对照:默认 mean/mean 的
|
|
354
|
-
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);
|
|
355
430
|
});
|
|
356
431
|
|
|
357
432
|
it("skipped 是 null:不稀释均值,但计入 total(覆盖率如实)", async () => {
|
|
358
433
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "skipped")] });
|
|
359
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [
|
|
360
|
-
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"];
|
|
361
436
|
expect(cell.value).toBe(1); // B 整桶为 null,不参与 across,不是 0.5
|
|
362
437
|
expect(cell.samples).toBe(1);
|
|
363
438
|
expect(cell.total).toBe(2);
|
|
@@ -366,8 +441,8 @@ describe("两级聚合引擎", () => {
|
|
|
366
441
|
|
|
367
442
|
it("全组 null → value null、display 兜底,不编 0;refs 必填(空数组)", async () => {
|
|
368
443
|
const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
|
|
369
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [
|
|
370
|
-
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"];
|
|
371
446
|
expect(cell.value).toBeNull();
|
|
372
447
|
expect(cell.display).toBe("—");
|
|
373
448
|
expect(cell.samples).toBe(0);
|
|
@@ -402,18 +477,18 @@ describe("两级聚合引擎", () => {
|
|
|
402
477
|
res("A", "passed", { attempt: 2 }),
|
|
403
478
|
],
|
|
404
479
|
});
|
|
405
|
-
const data = await MetricTable.data([s], { rows: byParity, columns: [
|
|
480
|
+
const data = await MetricTable.data([s], { rows: byParity, columns: [taskPassRate] });
|
|
406
481
|
expect(data.dimension).toBe("parity");
|
|
407
|
-
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]));
|
|
408
483
|
// 同一道题的 attempt 分进两组:even 组内 [1,1] 折成 1,odd 组内 [0] 折成 0
|
|
409
484
|
expect(byKey).toEqual({ even: 1, odd: 0 });
|
|
410
485
|
});
|
|
411
486
|
|
|
412
487
|
it("同一次计算里指标重名是错误", async () => {
|
|
413
|
-
const dup = defineMetric({ name: "pass-rate", value: () => 1 });
|
|
488
|
+
const dup = defineMetric({ name: "task-pass-rate", value: () => 1 });
|
|
414
489
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
415
|
-
await expect(MetricTable.data([s], { rows: "agent", columns: [
|
|
416
|
-
/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"/,
|
|
417
492
|
);
|
|
418
493
|
});
|
|
419
494
|
|
|
@@ -423,16 +498,16 @@ describe("两级聚合引擎", () => {
|
|
|
423
498
|
const none = snap({ experimentId: "exp/none", agent: "none", results: [res("A", "skipped", { agent: "none" })] });
|
|
424
499
|
const data = await MetricTable.data([none, bad, good], {
|
|
425
500
|
rows: "agent",
|
|
426
|
-
columns: [
|
|
427
|
-
sort:
|
|
501
|
+
columns: [taskPassRate],
|
|
502
|
+
sort: taskPassRate,
|
|
428
503
|
});
|
|
429
504
|
expect(data.rows.map((r) => r.key)).toEqual(["good", "bad", "none"]);
|
|
430
505
|
});
|
|
431
506
|
|
|
432
507
|
it("列键是字面量联合:拼错列名编译不过", async () => {
|
|
433
508
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
|
|
434
|
-
const data = await MetricTable.data([s], { rows: "agent", columns: [
|
|
435
|
-
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]; // 键锚在指标对象上
|
|
436
511
|
expect(cell.value).toBe(1);
|
|
437
512
|
// @ts-expect-error 列里没有这个键 —— 编译期挡住,不是运行时 undefined
|
|
438
513
|
data.rows[0].cells["pass-rat"];
|
|
@@ -445,7 +520,7 @@ describe("examScore", () => {
|
|
|
445
520
|
it("errored(断言空数组)得 0 —— 不因「gate 全过」空真得满分", async () => {
|
|
446
521
|
const s = snap({
|
|
447
522
|
experimentId: "exp/x",
|
|
448
|
-
results: [res("A", "errored", { assertions: [], error: "adapter crashed" })],
|
|
523
|
+
results: [res("A", "errored", { assertions: [], error: erroredWith("adapter crashed") })],
|
|
449
524
|
});
|
|
450
525
|
const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
|
|
451
526
|
const cell = data.rows[0].cells["exam-score"];
|
|
@@ -470,7 +545,7 @@ describe("examScore", () => {
|
|
|
470
545
|
assertions: [
|
|
471
546
|
softAssertion("judge-1", 0.5),
|
|
472
547
|
softAssertion("judge-2", 1),
|
|
473
|
-
{ name: "includes", severity: "gate", score: 1,
|
|
548
|
+
{ name: "includes", severity: "gate", score: 1, outcome: "passed" as const },
|
|
474
549
|
],
|
|
475
550
|
}),
|
|
476
551
|
],
|
|
@@ -479,7 +554,7 @@ describe("examScore", () => {
|
|
|
479
554
|
experimentId: "exp/b",
|
|
480
555
|
results: [
|
|
481
556
|
res("B", "passed", {
|
|
482
|
-
assertions: [{ name: "includes", severity: "gate", score: 1,
|
|
557
|
+
assertions: [{ name: "includes", severity: "gate", score: 1, outcome: "passed" as const }],
|
|
483
558
|
}),
|
|
484
559
|
],
|
|
485
560
|
});
|
|
@@ -496,6 +571,62 @@ describe("examScore", () => {
|
|
|
496
571
|
});
|
|
497
572
|
});
|
|
498
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
|
+
|
|
499
630
|
// ───────────────────────── 内置指标口径 ─────────────────────────
|
|
500
631
|
|
|
501
632
|
describe("内置指标", () => {
|
|
@@ -656,7 +787,7 @@ describe("MetricScatter.data", () => {
|
|
|
656
787
|
points: "experiment",
|
|
657
788
|
series: "agent",
|
|
658
789
|
x: costUSD,
|
|
659
|
-
y:
|
|
790
|
+
y: taskPassRate,
|
|
660
791
|
});
|
|
661
792
|
expect(data.points).toBe("experiment");
|
|
662
793
|
expect(data.series).toBe("agent");
|
|
@@ -681,7 +812,7 @@ describe("flag()", () => {
|
|
|
681
812
|
const withFlags = (id: string, flags: Record<string, unknown> | undefined, verdict: Verdict) =>
|
|
682
813
|
snap({
|
|
683
814
|
experimentId: id,
|
|
684
|
-
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 })],
|
|
685
816
|
});
|
|
686
817
|
|
|
687
818
|
it("MetricLine.data:x 收 flag、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
|
|
@@ -691,7 +822,7 @@ describe("flag()", () => {
|
|
|
691
822
|
const data = await MetricLine.data([s1, s2, legacy], {
|
|
692
823
|
x: flag("latencyMs", { label: "Simulated latency", unit: "ms" }),
|
|
693
824
|
series: flag("agents", { label: (v) => `${v} agents` }),
|
|
694
|
-
y:
|
|
825
|
+
y: taskPassRate,
|
|
695
826
|
});
|
|
696
827
|
expect(data.x).toEqual({ key: "latencyMs", label: "Simulated latency", unit: "ms" });
|
|
697
828
|
expect(data.series).toBe("agents");
|
|
@@ -715,13 +846,84 @@ describe("flag()", () => {
|
|
|
715
846
|
const s2 = withFlags("exp/b", { agents: 16 }, "failed");
|
|
716
847
|
const data = await MetricTable.data([s1, s2], {
|
|
717
848
|
rows: flag("agents", { label: (v) => `${v} agents` }),
|
|
718
|
-
columns: [
|
|
849
|
+
columns: [taskPassRate],
|
|
719
850
|
});
|
|
720
851
|
expect(data.dimension).toBe("agents");
|
|
721
852
|
expect(data.rows.map((r) => r.key)).toEqual(["1 agents", "16 agents"]);
|
|
722
853
|
});
|
|
723
854
|
});
|
|
724
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
|
+
|
|
725
927
|
// ───────────────────────── MetricTable.data:没有实体下钻 ─────────────────────────
|
|
726
928
|
|
|
727
929
|
describe("MetricTable.data", () => {
|
|
@@ -730,7 +932,7 @@ describe("MetricTable.data", () => {
|
|
|
730
932
|
experimentId: "exp/x",
|
|
731
933
|
results: [res("A", "passed"), res("B", "failed")],
|
|
732
934
|
});
|
|
733
|
-
const data = await MetricTable.data([s], { rows: "experiment", columns: [
|
|
935
|
+
const data = await MetricTable.data([s], { rows: "experiment", columns: [taskPassRate] });
|
|
734
936
|
expect(data.rows).toHaveLength(1);
|
|
735
937
|
expect(data.rows[0]!.meta).not.toHaveProperty("subRows");
|
|
736
938
|
// 展开到 Eval 是 ExperimentList 的职责,不在这里验证 —— 见 ExperimentList.data 的测试组。
|
|
@@ -745,10 +947,10 @@ describe("MetricTable.data", () => {
|
|
|
745
947
|
describe("reasonFor", () => {
|
|
746
948
|
it("同一 result 同时含 error、失败 gate、失败 soft → 只显示 error", () => {
|
|
747
949
|
const result = res("A", "errored", {
|
|
748
|
-
error: "adapter crashed",
|
|
950
|
+
error: erroredWith("adapter crashed"),
|
|
749
951
|
assertions: [
|
|
750
|
-
{ name: "includes", severity: "gate", score: 0,
|
|
751
|
-
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 }),
|
|
752
954
|
],
|
|
753
955
|
});
|
|
754
956
|
expect(reasonFor(result)).toBe("adapter crashed");
|
|
@@ -757,21 +959,21 @@ describe("reasonFor", () => {
|
|
|
757
959
|
it("skipReason 优先于 gate 断言,但让位给 error", () => {
|
|
758
960
|
const result = res("A", "skipped", {
|
|
759
961
|
skipReason: "missing fixture",
|
|
760
|
-
assertions: [{ name: "includes", severity: "gate", score: 0,
|
|
962
|
+
assertions: [{ name: "includes", severity: "gate", score: 0, outcome: "failed" as const }],
|
|
761
963
|
});
|
|
762
964
|
expect(reasonFor(result)).toBe("missing fixture");
|
|
763
965
|
});
|
|
764
966
|
|
|
765
|
-
it("
|
|
967
|
+
it("只展示第一条失败 gate,其余 gate 计数;失败 soft 不进入 gate 计数", () => {
|
|
766
968
|
const result = res("A", "failed", {
|
|
767
969
|
assertions: [
|
|
768
|
-
softAssertion("judge-first", 0.1, {
|
|
769
|
-
{ name: "includes", severity: "gate", score: 0,
|
|
770
|
-
{ name: "matches", severity: "gate", score: 0,
|
|
771
|
-
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
|
|
772
974
|
],
|
|
773
975
|
});
|
|
774
|
-
expect(reasonFor(result)).toBe("
|
|
976
|
+
expect(reasonFor(result)).toBe("gate: answer contains required text · includes · expected contains Brooklyn · received Manhattan · +1 more failures");
|
|
775
977
|
});
|
|
776
978
|
|
|
777
979
|
it("都缺席 → 无原因", () => {
|
|
@@ -818,6 +1020,22 @@ describe("RunOverview.data", () => {
|
|
|
818
1020
|
const data = await RunOverview.data(selection([s], [warning]));
|
|
819
1021
|
expect(data.warnings).toEqual([warning]);
|
|
820
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
|
+
});
|
|
821
1039
|
});
|
|
822
1040
|
|
|
823
1041
|
// ───────────────────────── GroupSummary.data ─────────────────────────
|
|
@@ -862,16 +1080,6 @@ describe("GroupSummary.data", () => {
|
|
|
862
1080
|
expect(data.lastRunAt).toBe(expB.startedAt);
|
|
863
1081
|
});
|
|
864
1082
|
|
|
865
|
-
it('跨 experiment 同名 eval 不合并:两个 experiment 各自的 "x" 独立计一票,不是被折成一票', async () => {
|
|
866
|
-
const a = snap({ experimentId: "exp/a", results: [res("x", "passed")] });
|
|
867
|
-
const b = snap({ experimentId: "exp/b", results: [res("x", "failed")] });
|
|
868
|
-
const data = await GroupSummary.data([a, b]);
|
|
869
|
-
expect(data.experiments).toBe(2);
|
|
870
|
-
expect(data.evals).toBe(2); // 只按 eval id 折叠(误把两个 experiment 的 "x" 当一道题)会变成 1
|
|
871
|
-
expect(data.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
|
|
872
|
-
expect(data.passRate.value).toBeCloseTo(0.5, 10); // 误合并会因为「任一轮过即过」变成 100%
|
|
873
|
-
});
|
|
874
|
-
|
|
875
1083
|
it("全组没有任何 attempt 报成本 → totalCostUSD null,不编 0", async () => {
|
|
876
1084
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
|
|
877
1085
|
const data = await GroupSummary.data([s]);
|
|
@@ -888,30 +1096,12 @@ describe("GroupSummary.data", () => {
|
|
|
888
1096
|
expect(data.passRate.total).toBe(2);
|
|
889
1097
|
});
|
|
890
1098
|
|
|
891
|
-
it("lastRunAt 只从传入的组 Selection 取值,不读全局最新 —— 组外快照更晚也不影响", async () => {
|
|
892
|
-
const inGroup = snap({
|
|
893
|
-
experimentId: "exp/a",
|
|
894
|
-
runStartedAt: "2026-07-01T08:00:00Z",
|
|
895
|
-
results: [res("A", "passed")],
|
|
896
|
-
});
|
|
897
|
-
const outOfGroup = snap({
|
|
898
|
-
experimentId: "exp/b",
|
|
899
|
-
runStartedAt: "2026-07-09T08:00:00Z",
|
|
900
|
-
results: [res("B", "passed")],
|
|
901
|
-
});
|
|
902
|
-
const sel = selection([inGroup, outOfGroup], []);
|
|
903
|
-
const scoped = sel.filter((s) => s.experimentId === "exp/a"); // 只留组内快照,模拟按实验组收窄的 Selection
|
|
904
|
-
const data = await GroupSummary.data(scoped);
|
|
905
|
-
expect(data.lastRunAt).toBe(inGroup.startedAt);
|
|
906
|
-
expect(data.lastRunAt).not.toBe(outOfGroup.startedAt);
|
|
907
|
-
});
|
|
908
|
-
|
|
909
1099
|
it('MetricTable rows: "experiment" 的 verdicts meta 复用同一份 eval 级统计,行为不变(回归)', async () => {
|
|
910
1100
|
const s = snap({
|
|
911
1101
|
experimentId: "exp/x",
|
|
912
1102
|
results: [res("A", "passed", { attempt: 0 }), res("A", "failed", { attempt: 1 }), res("B", "failed")],
|
|
913
1103
|
});
|
|
914
|
-
const data = await MetricTable.data([s], { rows: "experiment", columns: [
|
|
1104
|
+
const data = await MetricTable.data([s], { rows: "experiment", columns: [taskPassRate] });
|
|
915
1105
|
// A 两轮折成 passed(任一轮过即过),B 失败:1 passed / 1 failed
|
|
916
1106
|
expect(data.rows[0].meta?.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
|
|
917
1107
|
});
|
|
@@ -931,7 +1121,7 @@ describe("DeltaTable.data", () => {
|
|
|
931
1121
|
});
|
|
932
1122
|
const data = await DeltaTable.data([base, plus], {
|
|
933
1123
|
pairs: [{ a: "exp/base", b: "exp/plus", label: "memory" }],
|
|
934
|
-
metrics: [
|
|
1124
|
+
metrics: [taskPassRate, costUSD],
|
|
935
1125
|
});
|
|
936
1126
|
expect(data.rows).toHaveLength(1);
|
|
937
1127
|
const row = data.rows[0];
|
|
@@ -939,7 +1129,7 @@ describe("DeltaTable.data", () => {
|
|
|
939
1129
|
expect(row.a).toEqual({ experimentId: "exp/base" });
|
|
940
1130
|
expect(row.b).toEqual({ experimentId: "exp/plus" });
|
|
941
1131
|
|
|
942
|
-
const pass = row.cells["pass-rate"];
|
|
1132
|
+
const pass = row.cells["task-pass-rate"];
|
|
943
1133
|
expect(pass.a.value).toBe(0.5);
|
|
944
1134
|
expect(pass.b.value).toBe(1);
|
|
945
1135
|
expect(pass.delta).toBeCloseTo(0.5);
|
|
@@ -953,9 +1143,9 @@ describe("DeltaTable.data", () => {
|
|
|
953
1143
|
|
|
954
1144
|
const flat = await DeltaTable.data([base, base], {
|
|
955
1145
|
pairs: [{ a: "exp/base", b: "exp/base", label: "same" }],
|
|
956
|
-
metrics: [
|
|
1146
|
+
metrics: [taskPassRate],
|
|
957
1147
|
});
|
|
958
|
-
expect(flat.rows[0].cells["pass-rate"].display).toBe("±0");
|
|
1148
|
+
expect(flat.rows[0].cells["task-pass-rate"].display).toBe("±0");
|
|
959
1149
|
});
|
|
960
1150
|
|
|
961
1151
|
it("时间轴对比:pairs 的 a/b 收快照键 <experimentId> @ <startedAt>", async () => {
|
|
@@ -977,9 +1167,9 @@ describe("DeltaTable.data", () => {
|
|
|
977
1167
|
label: "this week vs last",
|
|
978
1168
|
},
|
|
979
1169
|
],
|
|
980
|
-
metrics: [
|
|
1170
|
+
metrics: [taskPassRate],
|
|
981
1171
|
});
|
|
982
|
-
const cell = data.rows[0].cells["pass-rate"];
|
|
1172
|
+
const cell = data.rows[0].cells["task-pass-rate"];
|
|
983
1173
|
expect(cell.a.value).toBe(0); // 旧快照那份
|
|
984
1174
|
expect(cell.b.value).toBe(1); // 新快照那份
|
|
985
1175
|
expect(cell.display).toBe("+100%");
|
|
@@ -1011,8 +1201,8 @@ describe("身份键去重", () => {
|
|
|
1011
1201
|
[older, newer],
|
|
1012
1202
|
[newer, older],
|
|
1013
1203
|
]) {
|
|
1014
|
-
const data = await MetricTable.data(order, { rows: "agent", columns: [
|
|
1015
|
-
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"];
|
|
1016
1206
|
expect(cell.total).toBe(1); // 两份只算一份
|
|
1017
1207
|
expect(cell.value).toBe(1); // 留的是最新 run 里的 passed,与快照传入顺序无关
|
|
1018
1208
|
|
|
@@ -1078,10 +1268,10 @@ describe("MetricMatrix.data", () => {
|
|
|
1078
1268
|
agent: "b1",
|
|
1079
1269
|
results: [res("A", "failed", { agent: "b1" })], // b1 没跑 B
|
|
1080
1270
|
});
|
|
1081
|
-
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 });
|
|
1082
1272
|
expect(data.rows).toBe("eval");
|
|
1083
1273
|
expect(data.columns).toBe("agent");
|
|
1084
|
-
expect(data.metric.key).toBe("pass-rate");
|
|
1274
|
+
expect(data.metric.key).toBe("task-pass-rate");
|
|
1085
1275
|
expect(data.cells).toHaveLength(3); // A×a1、B×a1、A×b1;B×b1 不出现
|
|
1086
1276
|
const find = (row: string, column: string) => data.cells.find((c) => c.row === row && c.column === column);
|
|
1087
1277
|
expect(find("A", "a1")?.cell.value).toBe(1);
|
|
@@ -1090,7 +1280,20 @@ describe("MetricMatrix.data", () => {
|
|
|
1090
1280
|
expect(find("B", "b1")).toBeUndefined();
|
|
1091
1281
|
});
|
|
1092
1282
|
|
|
1093
|
-
it("MetricBars.data
|
|
1094
|
-
|
|
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);
|
|
1095
1298
|
});
|
|
1096
1299
|
});
|