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