niceeval 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +55 -0
- package/dist/agents/types.d.ts +317 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/execution-tree.d.ts +103 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/select.d.ts +22 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +178 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +618 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +32 -0
- package/dist/report/format.js +126 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +83 -0
- package/dist/report/locale.js +160 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +20 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +10 -0
- package/dist/report/react/ExperimentList.js +15 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +100 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +393 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +503 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +420 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/annotated-source.d.ts +61 -0
- package/dist/results/attempt-evidence.d.ts +69 -0
- package/dist/results/attempt-source.d.ts +15 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +9 -4
- package/docs-site/zh/concepts/assert.mdx +8 -8
- package/docs-site/zh/concepts/evals.mdx +5 -5
- package/docs-site/zh/concepts/experiment.mdx +3 -1
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +2 -2
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -0
- package/docs-site/zh/guides/fixtures.mdx +1 -1
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +4 -4
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +35 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
- package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
- package/docs-site/zh/guides/viewing-results.mdx +199 -79
- package/docs-site/zh/guides/write-experiment.mdx +23 -1
- package/docs-site/zh/guides/write-send.mdx +2 -2
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +24 -12
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +7 -1
- package/docs-site/zh/reference/expect.mdx +3 -3
- package/package.json +10 -6
- package/src/agents/bub.ts +134 -43
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +11 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +62 -2
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/context.ts +3 -0
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +26 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +82 -0
- package/src/define.ts +72 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +36 -15
- package/src/i18n/zh-CN.ts +39 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +643 -0
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +223 -50
- package/src/report/compute.ts +358 -53
- package/src/report/dual-render.test.tsx +1125 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +50 -2
- package/src/report/index.ts +48 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +206 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +115 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +108 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +136 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +129 -0
- package/src/report/react/fixtures.ts +163 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +22 -6
- package/src/report/react/render.test.tsx +164 -31
- package/src/report/react/styles.css +405 -213
- package/src/report/report.test.ts +403 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +303 -95
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +196 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +97 -15
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +69 -28
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +125 -94
- package/src/show/render.ts +660 -160
- package/src/show/show.test.ts +472 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// niceeval/report 计算层的单元测试:全部用内存 fake(Snapshot / AttemptHandle 按
|
|
2
|
-
// niceeval/results 的读取契约手工构造),专门覆盖 docs/reports.md 点名的坑 ——
|
|
2
|
+
// niceeval/results 的读取契约手工构造),专门覆盖 docs/feature/reports/architecture.md 点名的坑 ——
|
|
3
3
|
// 两级聚合 vs 平铺、pass@k、examScore 空真、skipped 稀释、scoreboard 固定分母与
|
|
4
4
|
// 最长前缀、scatter/delta 的 null 语义、快照键对比、flag 维度与轴、
|
|
5
5
|
// cases 的 redact/truncated、身份键去重、Selection warnings 随行。
|
|
6
6
|
|
|
7
7
|
import { describe, expect, it } from "vitest";
|
|
8
8
|
|
|
9
|
-
import type { AssertionResult, EvalResult,
|
|
10
|
-
import type { AttemptHandle,
|
|
9
|
+
import type { AssertionResult, EvalResult, O11ySummary, Verdict } from "../types.ts";
|
|
10
|
+
import type { AttemptHandle, Selection, SelectionWarning, Snapshot } from "../results/index.ts";
|
|
11
11
|
import type { Dimension, MetricCell } from "./types.ts";
|
|
12
|
-
import { costUSD, defineMetric, durationMs, examScore, passRate, tokens } from "./metrics.ts";
|
|
12
|
+
import { costUSD, defineMetric, durationMs, examScore, passRate, tokens, turns } from "./metrics.ts";
|
|
13
13
|
import { flag } from "./flag.ts";
|
|
14
14
|
import { formatMetricValue } from "./format.ts";
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
AttemptList,
|
|
17
17
|
DeltaTable,
|
|
18
|
+
EvalList,
|
|
19
|
+
ExperimentList,
|
|
20
|
+
GroupSummary,
|
|
18
21
|
MetricBars,
|
|
19
22
|
MetricLine,
|
|
20
23
|
MetricMatrix,
|
|
@@ -23,18 +26,19 @@ import {
|
|
|
23
26
|
RunOverview,
|
|
24
27
|
Scoreboard,
|
|
25
28
|
} from "./components.tsx";
|
|
29
|
+
import { reasonFor } from "./compute.ts";
|
|
26
30
|
|
|
27
31
|
// ───────────────────────── fake 数据(按 results 读取契约造)─────────────────────────
|
|
28
32
|
|
|
29
33
|
let seq = 0;
|
|
30
34
|
|
|
31
35
|
/** 造一条结果;默认给每条唯一 startedAt —— 身份键含 startedAt,免得普通样本被去重误伤。 */
|
|
32
|
-
function res(id: string,
|
|
36
|
+
function res(id: string, verdict: Verdict, extra: Partial<EvalResult> = {}): EvalResult {
|
|
33
37
|
seq += 1;
|
|
34
38
|
return {
|
|
35
39
|
id,
|
|
36
40
|
agent: "agent-x",
|
|
37
|
-
|
|
41
|
+
verdict,
|
|
38
42
|
attempt: 0,
|
|
39
43
|
startedAt: `2026-07-01T00:00:00.${String(seq).padStart(6, "0")}Z`,
|
|
40
44
|
durationMs: 1000,
|
|
@@ -47,6 +51,24 @@ function softAssertion(name: string, score: number, extra: Partial<AssertionResu
|
|
|
47
51
|
return { name, severity: "soft", score, passed: true, ...extra };
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
/** 最小合规 O11ySummary,只有 totalTurns 会变;供 turns 指标测试内联挂到 EvalResult.o11y。 */
|
|
55
|
+
function o11ySummary(totalTurns: number): O11ySummary {
|
|
56
|
+
return {
|
|
57
|
+
totalTurns,
|
|
58
|
+
toolCalls: {},
|
|
59
|
+
totalToolCalls: 0,
|
|
60
|
+
filesRead: [],
|
|
61
|
+
filesModified: [],
|
|
62
|
+
shellCommands: [],
|
|
63
|
+
webFetches: [],
|
|
64
|
+
errors: [],
|
|
65
|
+
thinkingBlocks: 0,
|
|
66
|
+
compactions: 0,
|
|
67
|
+
durationMs: 0,
|
|
68
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
interface SnapSpec {
|
|
51
73
|
experimentId: string;
|
|
52
74
|
results: EvalResult[];
|
|
@@ -58,55 +80,46 @@ interface SnapSpec {
|
|
|
58
80
|
|
|
59
81
|
let runSeq = 0;
|
|
60
82
|
|
|
61
|
-
/**
|
|
83
|
+
/** 最小构造:一个快照目录装一个快照。runStartedAt 决定去重时谁是「最新快照」。 */
|
|
62
84
|
function snap(spec: SnapSpec): Snapshot {
|
|
63
85
|
runSeq += 1;
|
|
64
86
|
const startedAt = spec.runStartedAt ?? `2026-06-01T00:00:00.${String(runSeq).padStart(3, "0")}Z`;
|
|
65
|
-
const
|
|
66
|
-
|
|
87
|
+
const dir = `/results/exp/snap-${runSeq}`;
|
|
88
|
+
const snapshot = {
|
|
89
|
+
experimentId: spec.experimentId,
|
|
67
90
|
startedAt,
|
|
68
91
|
completedAt: startedAt,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const runDir: RunDir = { dir: `/results/run-${runSeq}`, summary, attempts: [] };
|
|
77
|
-
const attempts: AttemptHandle[] = spec.results.map((r, i) => ({
|
|
92
|
+
agent: spec.agent ?? "agent-x",
|
|
93
|
+
model: spec.model,
|
|
94
|
+
schemaVersion: 1,
|
|
95
|
+
dir,
|
|
96
|
+
knownEvalIds: spec.knownEvalIds,
|
|
97
|
+
} as Snapshot;
|
|
98
|
+
const attempts: AttemptHandle[] = spec.results.map((r) => ({
|
|
78
99
|
evalId: r.id,
|
|
79
100
|
experimentId: r.experimentId ?? spec.experimentId,
|
|
80
101
|
result: r,
|
|
81
|
-
ref: {
|
|
82
|
-
|
|
102
|
+
ref: { snapshot: `exp/snap-${runSeq}`, attempt: `${r.id}/a${r.attempt}` },
|
|
103
|
+
snapshot,
|
|
83
104
|
events: async () => null,
|
|
84
105
|
trace: async () => null,
|
|
85
|
-
o11y: async () => null,
|
|
106
|
+
o11y: async () => r.o11y ?? null,
|
|
107
|
+
agentSetup: async () => null,
|
|
86
108
|
diff: async () => null,
|
|
87
109
|
sources: async () => null,
|
|
88
110
|
}));
|
|
89
|
-
runDir.attempts = attempts;
|
|
90
111
|
const evals = new Map<string, AttemptHandle[]>();
|
|
91
112
|
for (const attempt of attempts) {
|
|
92
113
|
const list = evals.get(attempt.evalId);
|
|
93
114
|
if (list) list.push(attempt);
|
|
94
115
|
else evals.set(attempt.evalId, [attempt]);
|
|
95
116
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
agent: spec.agent ?? "agent-x",
|
|
100
|
-
model: spec.model,
|
|
101
|
-
schemaVersion: 1,
|
|
102
|
-
evals: [...evals.entries()].map(([id, list]) => ({ id, attempts: list })),
|
|
103
|
-
attempts,
|
|
104
|
-
runDir,
|
|
105
|
-
knownEvalIds: spec.knownEvalIds,
|
|
106
|
-
};
|
|
117
|
+
snapshot.evals = [...evals.entries()].map(([id, list]) => ({ id, attempts: list }));
|
|
118
|
+
snapshot.attempts = attempts;
|
|
119
|
+
return snapshot;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
/**
|
|
122
|
+
/** 手工造一个 Selection(warnings 随行的形状,filter 语义与 results 一致)。 */
|
|
110
123
|
function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selection {
|
|
111
124
|
return {
|
|
112
125
|
snapshots,
|
|
@@ -122,6 +135,187 @@ function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selecti
|
|
|
122
135
|
};
|
|
123
136
|
}
|
|
124
137
|
|
|
138
|
+
describe("ExperimentList.data", () => {
|
|
139
|
+
it("身份、flags、Eval 判定构成、官方两级聚合汇总指标,展开到 Eval 携带 locator 与证据能力", async () => {
|
|
140
|
+
const s = snap({
|
|
141
|
+
experimentId: "group/codex-e2b--mempal",
|
|
142
|
+
agent: "codex",
|
|
143
|
+
model: "gpt-5.4-mini",
|
|
144
|
+
runStartedAt: "2026-07-10T22:44:00.000Z",
|
|
145
|
+
results: [
|
|
146
|
+
res("memory/a", "failed", {
|
|
147
|
+
attempt: 0,
|
|
148
|
+
durationMs: 1_000,
|
|
149
|
+
assertions: [{ name: "gate", severity: "gate", passed: false, score: 0, detail: "wrong" }],
|
|
150
|
+
usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.1 },
|
|
151
|
+
hasEvents: true,
|
|
152
|
+
}),
|
|
153
|
+
res("memory/a", "passed", {
|
|
154
|
+
attempt: 1,
|
|
155
|
+
durationMs: 2_000,
|
|
156
|
+
usage: { inputTokens: 20, outputTokens: 10, costUSD: 0.2 },
|
|
157
|
+
}),
|
|
158
|
+
res("memory/b", "errored", { error: "timeout", durationMs: 3_000 }),
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
s.experiment = { id: s.experimentId, runs: 2, earlyExit: false, sandbox: "e2b:fast", budget: 2, flags: { cache: true } };
|
|
162
|
+
|
|
163
|
+
const items = await ExperimentList.data([s]);
|
|
164
|
+
expect(items).toHaveLength(1);
|
|
165
|
+
const item = items[0]!;
|
|
166
|
+
expect(item).toMatchObject({
|
|
167
|
+
experimentId: "group/codex-e2b--mempal",
|
|
168
|
+
agent: "codex",
|
|
169
|
+
model: "gpt-5.4-mini",
|
|
170
|
+
flags: { cache: true },
|
|
171
|
+
verdicts: { passed: 1, failed: 0, errored: 1, skipped: 0 },
|
|
172
|
+
evals: 2,
|
|
173
|
+
attempts: 3,
|
|
174
|
+
lastRunAt: "2026-07-10T22:44:00.000Z",
|
|
175
|
+
});
|
|
176
|
+
// 官方两级聚合口径:memory/a 题内均值 (0+1)/2=0.5,memory/b 题内 0;跨题均值 0.25
|
|
177
|
+
expect(item.passRate.value).toBeCloseTo(0.25);
|
|
178
|
+
expect(item.cost.samples).toBe(2); // 只有 memory/a 的两次 attempt 报了成本
|
|
179
|
+
|
|
180
|
+
const evalA = item.evalRows.find((e) => e.evalId === "memory/a")!;
|
|
181
|
+
expect(evalA.verdict).toBe("passed"); // 任一轮通过 → 该题通过
|
|
182
|
+
expect(evalA.attempts.map((a) => a.attempt)).toEqual([0, 1]);
|
|
183
|
+
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
|
+
// 每次 attempt 独立编码,即使同一道题也不撞车
|
|
187
|
+
expect(evalA.attempts[0]!.locator).not.toBe(evalA.attempts[1]!.locator);
|
|
188
|
+
|
|
189
|
+
const evalB = item.evalRows.find((e) => e.evalId === "memory/b")!;
|
|
190
|
+
expect(evalB.verdict).toBe("errored");
|
|
191
|
+
expect(evalB.reason).toBe("timeout");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("展开数组按 evalId 升序,顶层数组按 experimentId 升序", async () => {
|
|
195
|
+
const s1 = snap({ experimentId: "exp/b", results: [res("z", "passed"), res("a", "passed")] });
|
|
196
|
+
const s2 = snap({ experimentId: "exp/a", results: [res("x", "passed")] });
|
|
197
|
+
const items = await ExperimentList.data([s1, s2]);
|
|
198
|
+
expect(items.map((i) => i.experimentId)).toEqual(["exp/a", "exp/b"]);
|
|
199
|
+
expect(items.find((i) => i.experimentId === "exp/b")!.evalRows.map((e) => e.evalId)).toEqual(["a", "z"]);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("EvalList.data", () => {
|
|
204
|
+
it("每项一个 experimentId + evalId,判定/分数/成本耗时均值/失败原因与展开到 Attempt 都在", async () => {
|
|
205
|
+
const s = snap({
|
|
206
|
+
experimentId: "compare/codex",
|
|
207
|
+
results: [
|
|
208
|
+
res("weather/brooklyn", "failed", {
|
|
209
|
+
attempt: 0,
|
|
210
|
+
durationMs: 40_000,
|
|
211
|
+
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.02 },
|
|
212
|
+
assertions: [{ name: 'calledTool("get_weather")', severity: "gate", passed: false, score: 0, detail: "tool was never called" }],
|
|
213
|
+
}),
|
|
214
|
+
res("weather/brooklyn", "failed", {
|
|
215
|
+
attempt: 1,
|
|
216
|
+
durationMs: 42_000,
|
|
217
|
+
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.06 },
|
|
218
|
+
assertions: [{ name: 'calledTool("get_weather")', severity: "gate", passed: false, score: 0, detail: "tool was never called" }],
|
|
219
|
+
}),
|
|
220
|
+
],
|
|
221
|
+
});
|
|
222
|
+
const items = await EvalList.data([s]);
|
|
223
|
+
expect(items).toHaveLength(1);
|
|
224
|
+
const item = items[0]!;
|
|
225
|
+
expect(item.evalId).toBe("weather/brooklyn");
|
|
226
|
+
expect(item.experimentId).toBe("compare/codex");
|
|
227
|
+
expect(item.verdict).toBe("failed");
|
|
228
|
+
expect(item.reason).toBe('calledTool("get_weather"): tool was never called');
|
|
229
|
+
expect(item.score.value).toBe(0); // examScore: failed → 0
|
|
230
|
+
expect(item.duration.value).toBeCloseTo(41_000); // 平均耗时
|
|
231
|
+
expect(item.cost.value).toBeCloseTo(0.04); // 平均成本
|
|
232
|
+
expect(item.attempts.map((a) => a.attempt)).toEqual([0, 1]);
|
|
233
|
+
expect(item.attempts[0]!.locator).not.toBe(item.attempts[1]!.locator);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("同一个 eval id 出现在两个 experiment 上是两条不同结果,不合并", async () => {
|
|
237
|
+
const s1 = snap({ experimentId: "exp/a", results: [res("shared", "passed")] });
|
|
238
|
+
const s2 = snap({ experimentId: "exp/b", results: [res("shared", "failed")] });
|
|
239
|
+
const items = await EvalList.data([s1, s2]);
|
|
240
|
+
expect(items).toHaveLength(2);
|
|
241
|
+
expect(items.map((i) => `${i.experimentId}:${i.verdict}`).sort()).toEqual(["exp/a:passed", "exp/b:failed"]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("AttemptList.data", () => {
|
|
246
|
+
it("每项一个 Attempt:身份、判定、assertions、耗时、成本、locator 与证据能力标记", async () => {
|
|
247
|
+
const s = snap({
|
|
248
|
+
experimentId: "exp/x",
|
|
249
|
+
results: [
|
|
250
|
+
res("A", "failed", {
|
|
251
|
+
durationMs: 5_000,
|
|
252
|
+
usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.05 },
|
|
253
|
+
assertions: [{ name: "gate", severity: "gate", passed: false, score: 0, detail: "nope" }],
|
|
254
|
+
hasEvents: true,
|
|
255
|
+
hasSources: true,
|
|
256
|
+
}),
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
const items = await AttemptList.data([s]);
|
|
260
|
+
expect(items).toHaveLength(1);
|
|
261
|
+
const item = items[0]!;
|
|
262
|
+
expect(item).toMatchObject({
|
|
263
|
+
evalId: "A",
|
|
264
|
+
experimentId: "exp/x",
|
|
265
|
+
attempt: 0,
|
|
266
|
+
agent: "agent-x",
|
|
267
|
+
verdict: "failed",
|
|
268
|
+
durationMs: 5_000,
|
|
269
|
+
costUSD: 0.05,
|
|
270
|
+
});
|
|
271
|
+
expect(item.assertions).toHaveLength(1);
|
|
272
|
+
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
|
+
});
|
|
277
|
+
|
|
278
|
+
it("redact 作用于 error 与断言 detail/evidence,不作用于身份字段", async () => {
|
|
279
|
+
const s = snap({
|
|
280
|
+
experimentId: "exp/x",
|
|
281
|
+
results: [
|
|
282
|
+
res("A", "errored", { error: "ENOENT /Users/me/repo/tool" }),
|
|
283
|
+
res("B", "failed", {
|
|
284
|
+
assertions: [
|
|
285
|
+
{
|
|
286
|
+
name: "includes",
|
|
287
|
+
severity: "gate",
|
|
288
|
+
score: 0,
|
|
289
|
+
passed: false,
|
|
290
|
+
detail: "missing text under /Users/me/repo/src",
|
|
291
|
+
evidence: "checked /Users/me/repo/src/app.ts",
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
}),
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
const items = await AttemptList.data([s], { redact: (text) => text.replaceAll("/Users/me/repo", "<repo>") });
|
|
298
|
+
const a = items.find((i) => i.evalId === "A")!;
|
|
299
|
+
expect(a.error).toBe("ENOENT <repo>/tool");
|
|
300
|
+
const b = items.find((i) => i.evalId === "B")!;
|
|
301
|
+
expect(b.assertions[0]!.detail).toBe("missing text under <repo>/src");
|
|
302
|
+
expect(b.assertions[0]!.evidence).toBe("checked <repo>/src/app.ts");
|
|
303
|
+
// 身份字段不经 redact
|
|
304
|
+
expect(items.every((i) => i.experimentId === "exp/x")).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("不预设只看失败:passed 的 attempt 同样列出,原样携带 assertions", async () => {
|
|
308
|
+
const s = snap({
|
|
309
|
+
experimentId: "exp/x",
|
|
310
|
+
results: [res("A", "passed", { assertions: [{ name: "ok", severity: "gate", passed: true, score: 1 }] })],
|
|
311
|
+
});
|
|
312
|
+
const items = await AttemptList.data([s]);
|
|
313
|
+
expect(items).toHaveLength(1);
|
|
314
|
+
expect(items[0]).toMatchObject({ verdict: "passed" });
|
|
315
|
+
expect(items[0]!.assertions).toHaveLength(1);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
125
319
|
// ───────────────────────── 两级聚合 ─────────────────────────
|
|
126
320
|
|
|
127
321
|
describe("两级聚合引擎", () => {
|
|
@@ -146,7 +340,7 @@ describe("两级聚合引擎", () => {
|
|
|
146
340
|
name: "pass@k",
|
|
147
341
|
better: "higher",
|
|
148
342
|
unit: "%",
|
|
149
|
-
value: (a) => (a.result.
|
|
343
|
+
value: (a) => (a.result.verdict === "skipped" ? null : a.result.verdict === "passed" ? 1 : 0),
|
|
150
344
|
aggregate: { perEval: "max", across: "mean" },
|
|
151
345
|
});
|
|
152
346
|
const s = snap({
|
|
@@ -184,7 +378,7 @@ describe("两级聚合引擎", () => {
|
|
|
184
378
|
it("where 不满足 → null,不进聚合", async () => {
|
|
185
379
|
const onlyPassed = defineMetric({
|
|
186
380
|
name: "only-passed",
|
|
187
|
-
where: (a) => a.result.
|
|
381
|
+
where: (a) => a.result.verdict === "passed",
|
|
188
382
|
value: () => 5,
|
|
189
383
|
});
|
|
190
384
|
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
|
|
@@ -339,6 +533,22 @@ describe("内置指标", () => {
|
|
|
339
533
|
const dur = await MetricTable.data([s], { rows: "agent", columns: [durationMs] });
|
|
340
534
|
expect(dur.rows[0].cells["duration"].value).toBe(2000); // (1000 + 3000)/2,errored 实测照算
|
|
341
535
|
});
|
|
536
|
+
|
|
537
|
+
it("turns:读 o11y.totalTurns;o11y 缺失(未随发布带上)→ null,不是 0;skipped 不进聚合", async () => {
|
|
538
|
+
const s = snap({
|
|
539
|
+
experimentId: "exp/x",
|
|
540
|
+
results: [
|
|
541
|
+
res("A", "passed", { o11y: o11ySummary(12) }),
|
|
542
|
+
res("B", "failed"), // 没带 o11y(如 copySnapshots 漏选 artifact)→ null,不稀释成 0
|
|
543
|
+
res("C", "skipped", { o11y: o11ySummary(3) }), // skipped 恒 null,哪怕 o11y 在场
|
|
544
|
+
],
|
|
545
|
+
});
|
|
546
|
+
const data = await MetricTable.data([s], { rows: "agent", columns: [turns] });
|
|
547
|
+
const cell = data.rows[0].cells["turns"];
|
|
548
|
+
expect(cell.value).toBe(12);
|
|
549
|
+
expect(cell.samples).toBe(1); // 只有 A 测得了(B 缺 o11y、C 是 skipped)
|
|
550
|
+
expect(cell.total).toBe(3); // total 是组内全部 attempt 数,不是「本该测得」的分母
|
|
551
|
+
});
|
|
342
552
|
});
|
|
343
553
|
|
|
344
554
|
// ───────────────────────── Scoreboard.data ─────────────────────────
|
|
@@ -468,10 +678,10 @@ describe("MetricScatter.data", () => {
|
|
|
468
678
|
// ───────────────────────── flag():维度与轴 ─────────────────────────
|
|
469
679
|
|
|
470
680
|
describe("flag()", () => {
|
|
471
|
-
const withFlags = (id: string, flags: Record<string, unknown> | undefined,
|
|
681
|
+
const withFlags = (id: string, flags: Record<string, unknown> | undefined, verdict: Verdict) =>
|
|
472
682
|
snap({
|
|
473
683
|
experimentId: id,
|
|
474
|
-
results: [res("A",
|
|
684
|
+
results: [res("A", verdict, { experimentId: id, experiment: { id, flags } })],
|
|
475
685
|
});
|
|
476
686
|
|
|
477
687
|
it("MetricLine.data:x 收 flag、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
|
|
@@ -512,6 +722,63 @@ describe("flag()", () => {
|
|
|
512
722
|
});
|
|
513
723
|
});
|
|
514
724
|
|
|
725
|
+
// ───────────────────────── MetricTable.data:没有实体下钻 ─────────────────────────
|
|
726
|
+
|
|
727
|
+
describe("MetricTable.data", () => {
|
|
728
|
+
it("rows: \"experiment\" 的 meta 不带 subRows —— MetricTable 只表达维度 × 指标,没有 expand", async () => {
|
|
729
|
+
const s = snap({
|
|
730
|
+
experimentId: "exp/x",
|
|
731
|
+
results: [res("A", "passed"), res("B", "failed")],
|
|
732
|
+
});
|
|
733
|
+
const data = await MetricTable.data([s], { rows: "experiment", columns: [passRate] });
|
|
734
|
+
expect(data.rows).toHaveLength(1);
|
|
735
|
+
expect(data.rows[0]!.meta).not.toHaveProperty("subRows");
|
|
736
|
+
// 展开到 Eval 是 ExperimentList 的职责,不在这里验证 —— 见 ExperimentList.data 的测试组。
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// ───────────────────────── reasonFor(原因优先级)─────────────────────────
|
|
741
|
+
// docs/feature/reports/show.md 裸 `show` 的 RESULT 列口径:error → skipReason → 未通过的 gate 断言。
|
|
742
|
+
// ExperimentList / EvalList 的 evalRows.reason、AttemptList 的 attemptItemReason 都port这同一份材料,
|
|
743
|
+
// 这里直接测 reasonFor 本身,不必再经 MetricTable 之类的中间组件绕一圈。
|
|
744
|
+
|
|
745
|
+
describe("reasonFor", () => {
|
|
746
|
+
it("同一 result 同时含 error、失败 gate、失败 soft → 只显示 error", () => {
|
|
747
|
+
const result = res("A", "errored", {
|
|
748
|
+
error: "adapter crashed",
|
|
749
|
+
assertions: [
|
|
750
|
+
{ name: "includes", severity: "gate", score: 0, passed: false, detail: "missing text" },
|
|
751
|
+
softAssertion("judge", 0.2, { passed: false }),
|
|
752
|
+
],
|
|
753
|
+
});
|
|
754
|
+
expect(reasonFor(result)).toBe("adapter crashed");
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("skipReason 优先于 gate 断言,但让位给 error", () => {
|
|
758
|
+
const result = res("A", "skipped", {
|
|
759
|
+
skipReason: "missing fixture",
|
|
760
|
+
assertions: [{ name: "includes", severity: "gate", score: 0, passed: false }],
|
|
761
|
+
});
|
|
762
|
+
expect(reasonFor(result)).toBe("missing fixture");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("多个失败 gate 按原始声明顺序全部保留,拼接成一行;失败 soft 不出现在 reason 里", () => {
|
|
766
|
+
const result = res("A", "failed", {
|
|
767
|
+
assertions: [
|
|
768
|
+
softAssertion("judge-first", 0.1, { passed: false }), // 声明顺序第一,但 soft 不进 reason
|
|
769
|
+
{ name: "includes", severity: "gate", score: 0, passed: false, detail: "missing text" },
|
|
770
|
+
{ name: "matches", severity: "gate", score: 0, passed: false }, // 无 detail,只有 name
|
|
771
|
+
softAssertion("judge-last", 0.3, { passed: false }), // soft 依旧不进 reason
|
|
772
|
+
],
|
|
773
|
+
});
|
|
774
|
+
expect(reasonFor(result)).toBe("includes: missing text, matches");
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it("都缺席 → 无原因", () => {
|
|
778
|
+
expect(reasonFor(res("A", "passed"))).toBeUndefined();
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
515
782
|
// ───────────────────────── RunOverview.data ─────────────────────────
|
|
516
783
|
|
|
517
784
|
describe("RunOverview.data", () => {
|
|
@@ -553,6 +820,103 @@ describe("RunOverview.data", () => {
|
|
|
553
820
|
});
|
|
554
821
|
});
|
|
555
822
|
|
|
823
|
+
// ───────────────────────── GroupSummary.data ─────────────────────────
|
|
824
|
+
|
|
825
|
+
describe("GroupSummary.data", () => {
|
|
826
|
+
it("多 experiment、多 attempt、部分成本缺失、不同 startedAt:eval 级折叠计票(完整身份键)、总成本 null-safe 求和、lastRunAt 取组内最大值", async () => {
|
|
827
|
+
const expA = snap({
|
|
828
|
+
experimentId: "exp/a",
|
|
829
|
+
runStartedAt: "2026-07-01T08:00:00Z",
|
|
830
|
+
results: [
|
|
831
|
+
// "shared" 两轮:一败一过 → 折叠成 passed(与 exp/b 同名的 "shared" 各算各的,见下方独立测试)
|
|
832
|
+
res("shared", "failed", { attempt: 0 }),
|
|
833
|
+
res("shared", "passed", { attempt: 1, usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.5 } }),
|
|
834
|
+
// "only-a" 只有 exp/a 跑过,失败、没报成本
|
|
835
|
+
res("only-a", "failed"),
|
|
836
|
+
],
|
|
837
|
+
});
|
|
838
|
+
const expB = snap({
|
|
839
|
+
experimentId: "exp/b",
|
|
840
|
+
runStartedAt: "2026-07-03T08:00:00Z", // 比 exp/a 晚 —— lastRunAt 应取这个
|
|
841
|
+
results: [
|
|
842
|
+
// 与 exp/a 同名的 "shared":不同 experiment,必须独立计一票,不能被误合并
|
|
843
|
+
res("shared", "errored", { estimatedCostUSD: 0.2 }),
|
|
844
|
+
],
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
const data = await GroupSummary.data([expA, expB]);
|
|
848
|
+
|
|
849
|
+
expect(data.experiments).toBe(2);
|
|
850
|
+
// 3 道题:exp/a 的 shared、exp/a 的 only-a、exp/b 的 shared —— 完整身份键各算各的
|
|
851
|
+
expect(data.evals).toBe(3);
|
|
852
|
+
expect(data.attempts).toBe(4); // 原始 attempt 计数:exp/a 2(shared)+1(only-a)+ exp/b 1(shared)
|
|
853
|
+
expect(data.verdicts).toEqual({ passed: 1, failed: 1, errored: 1, skipped: 0 });
|
|
854
|
+
// eval 级折叠通过率:1 / (1 + 1 + 1) = 1/3,不是 attempt 原始占比(1 passed / 4 attempts = 25%)
|
|
855
|
+
expect(data.passRate.value).toBeCloseTo(1 / 3, 10);
|
|
856
|
+
expect(data.passRate.display).toBe("33.3%");
|
|
857
|
+
expect(data.passRate.samples).toBe(3);
|
|
858
|
+
expect(data.passRate.total).toBe(3);
|
|
859
|
+
// 总成本 null-safe 求和:0.5(实测)+ 0.2(估算)= 0.7;only-a 的失败 attempt 没报成本,不计 0
|
|
860
|
+
expect(data.totalCostUSD).toBeCloseTo(0.7, 10);
|
|
861
|
+
// 最后运行时间:组内两个快照的 startedAt 取最大值(exp/b 更晚)
|
|
862
|
+
expect(data.lastRunAt).toBe(expB.startedAt);
|
|
863
|
+
});
|
|
864
|
+
|
|
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
|
+
it("全组没有任何 attempt 报成本 → totalCostUSD null,不编 0", async () => {
|
|
876
|
+
const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
|
|
877
|
+
const data = await GroupSummary.data([s]);
|
|
878
|
+
expect(data.totalCostUSD).toBeNull();
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it("分母为 0(组内 eval 全部 skipped)→ passRate.value 为 null,不编 0%", async () => {
|
|
882
|
+
const s = snap({ experimentId: "exp/x", results: [res("A", "skipped"), res("B", "skipped")] });
|
|
883
|
+
const data = await GroupSummary.data([s]);
|
|
884
|
+
expect(data.verdicts).toEqual({ passed: 0, failed: 0, errored: 0, skipped: 2 });
|
|
885
|
+
expect(data.passRate.value).toBeNull();
|
|
886
|
+
expect(data.passRate.display).toBe("—");
|
|
887
|
+
expect(data.passRate.samples).toBe(0);
|
|
888
|
+
expect(data.passRate.total).toBe(2);
|
|
889
|
+
});
|
|
890
|
+
|
|
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
|
+
it('MetricTable rows: "experiment" 的 verdicts meta 复用同一份 eval 级统计,行为不变(回归)', async () => {
|
|
910
|
+
const s = snap({
|
|
911
|
+
experimentId: "exp/x",
|
|
912
|
+
results: [res("A", "passed", { attempt: 0 }), res("A", "failed", { attempt: 1 }), res("B", "failed")],
|
|
913
|
+
});
|
|
914
|
+
const data = await MetricTable.data([s], { rows: "experiment", columns: [passRate] });
|
|
915
|
+
// A 两轮折成 passed(任一轮过即过),B 失败:1 passed / 1 failed
|
|
916
|
+
expect(data.rows[0].meta?.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
556
920
|
// ───────────────────────── DeltaTable.data ─────────────────────────
|
|
557
921
|
|
|
558
922
|
describe("DeltaTable.data", () => {
|
|
@@ -622,61 +986,6 @@ describe("DeltaTable.data", () => {
|
|
|
622
986
|
});
|
|
623
987
|
});
|
|
624
988
|
|
|
625
|
-
// ───────────────────────── CaseList.data ─────────────────────────
|
|
626
|
-
|
|
627
|
-
describe("CaseList.data", () => {
|
|
628
|
-
it("默认只列 failed+errored;redact 作用于 error/detail/evidence;truncated 如实", async () => {
|
|
629
|
-
const s = snap({
|
|
630
|
-
experimentId: "exp/x",
|
|
631
|
-
results: [
|
|
632
|
-
res("A", "failed", {
|
|
633
|
-
assertions: [
|
|
634
|
-
{
|
|
635
|
-
name: "includes",
|
|
636
|
-
severity: "gate",
|
|
637
|
-
score: 0,
|
|
638
|
-
passed: false,
|
|
639
|
-
detail: "missing text under /Users/me/repo/src",
|
|
640
|
-
evidence: "checked /Users/me/repo/src/app.ts",
|
|
641
|
-
},
|
|
642
|
-
{ name: "ok", severity: "gate", score: 1, passed: true }, // 通过的断言不列
|
|
643
|
-
],
|
|
644
|
-
}),
|
|
645
|
-
res("B", "errored", { error: "ENOENT /Users/me/repo/tool" }),
|
|
646
|
-
res("C", "failed"),
|
|
647
|
-
res("D", "passed"),
|
|
648
|
-
res("E", "skipped"),
|
|
649
|
-
],
|
|
650
|
-
});
|
|
651
|
-
const data = await CaseList.data([s], {
|
|
652
|
-
limit: 2,
|
|
653
|
-
redact: (text) => text.replaceAll("/Users/me/repo", "<repo>"),
|
|
654
|
-
});
|
|
655
|
-
expect(data.rows).toHaveLength(2);
|
|
656
|
-
expect(data.truncated).toBe(1); // C 被截;D/E 本就不在默认 outcomes 里
|
|
657
|
-
|
|
658
|
-
const [first, second] = data.rows;
|
|
659
|
-
expect(first.eval).toBe("A");
|
|
660
|
-
expect(first.outcome).toBe("failed");
|
|
661
|
-
expect(first.failedAssertions).toHaveLength(1);
|
|
662
|
-
expect(first.failedAssertions[0].detail).toBe("missing text under <repo>/src");
|
|
663
|
-
expect(first.failedAssertions[0].evidence).toBe("checked <repo>/src/app.ts");
|
|
664
|
-
expect(first.ref.result).toBe(0);
|
|
665
|
-
|
|
666
|
-
expect(second.eval).toBe("B");
|
|
667
|
-
expect(second.outcome).toBe("errored");
|
|
668
|
-
expect(second.error).toBe("ENOENT <repo>/tool");
|
|
669
|
-
expect(second.ref.result).toBe(1);
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
it("outcomes 可收窄;不传 limit 不截断", async () => {
|
|
673
|
-
const s = snap({ experimentId: "exp/x", results: [res("A", "failed"), res("B", "errored")] });
|
|
674
|
-
const onlyErrored = await CaseList.data([s], { outcomes: ["errored"] });
|
|
675
|
-
expect(onlyErrored.rows.map((r) => r.eval)).toEqual(["B"]);
|
|
676
|
-
expect(onlyErrored.truncated).toBe(0);
|
|
677
|
-
});
|
|
678
|
-
});
|
|
679
|
-
|
|
680
989
|
// ───────────────────────── 身份键去重 ─────────────────────────
|
|
681
990
|
|
|
682
991
|
describe("身份键去重", () => {
|