niceeval 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +321 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/otlp/parse.d.ts +3 -0
  9. package/dist/o11y/otlp/receiver.d.ts +21 -0
  10. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  11. package/dist/o11y/types.d.ts +190 -0
  12. package/dist/report/aggregate.d.ts +57 -0
  13. package/dist/report/aggregate.js +212 -0
  14. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  15. package/dist/report/built-ins/experiment-comparison.js +13 -0
  16. package/dist/report/built-ins/index.d.ts +1 -0
  17. package/dist/report/built-ins/index.js +2 -0
  18. package/dist/report/components.d.ts +180 -0
  19. package/dist/report/components.js +132 -0
  20. package/dist/report/compute.d.ts +106 -0
  21. package/dist/report/compute.js +600 -0
  22. package/dist/report/flag.d.ts +6 -0
  23. package/dist/report/flag.js +8 -0
  24. package/dist/report/format.d.ts +26 -0
  25. package/dist/report/format.js +109 -0
  26. package/dist/report/index.d.ts +19 -0
  27. package/dist/report/index.js +30 -0
  28. package/dist/report/load.d.ts +12 -0
  29. package/dist/report/load.js +56 -0
  30. package/dist/report/locale.d.ts +94 -0
  31. package/dist/report/locale.js +183 -0
  32. package/dist/report/metrics.d.ts +21 -0
  33. package/dist/report/metrics.js +115 -0
  34. package/dist/report/primitives.d.ts +64 -0
  35. package/dist/report/primitives.js +113 -0
  36. package/dist/report/react/AttemptList.d.ts +23 -0
  37. package/dist/report/react/AttemptList.js +19 -0
  38. package/dist/report/react/DeltaTable.d.ts +8 -0
  39. package/dist/report/react/DeltaTable.js +24 -0
  40. package/dist/report/react/EvalList.d.ts +10 -0
  41. package/dist/report/react/EvalList.js +0 -0
  42. package/dist/report/react/ExperimentList.d.ts +11 -0
  43. package/dist/report/react/ExperimentList.js +67 -0
  44. package/dist/report/react/GroupSummary.d.ts +8 -0
  45. package/dist/report/react/GroupSummary.js +8 -0
  46. package/dist/report/react/MetricBars.d.ts +10 -0
  47. package/dist/report/react/MetricBars.js +48 -0
  48. package/dist/report/react/MetricLine.d.ts +9 -0
  49. package/dist/report/react/MetricLine.js +68 -0
  50. package/dist/report/react/MetricMatrix.d.ts +10 -0
  51. package/dist/report/react/MetricMatrix.js +23 -0
  52. package/dist/report/react/MetricScatter.d.ts +9 -0
  53. package/dist/report/react/MetricScatter.js +96 -0
  54. package/dist/report/react/MetricTable.d.ts +11 -0
  55. package/dist/report/react/MetricTable.js +26 -0
  56. package/dist/report/react/RunOverview.d.ts +8 -0
  57. package/dist/report/react/RunOverview.js +12 -0
  58. package/dist/report/react/Scoreboard.d.ts +8 -0
  59. package/dist/report/react/Scoreboard.js +26 -0
  60. package/dist/report/react/cell.d.ts +9 -0
  61. package/dist/report/react/cell.js +9 -0
  62. package/dist/report/react/chart-math.d.ts +14 -0
  63. package/dist/report/react/chart-math.js +68 -0
  64. package/dist/report/react/colors.d.ts +14 -0
  65. package/dist/report/react/colors.js +45 -0
  66. package/dist/report/react/fixtures.d.ts +19 -0
  67. package/dist/report/react/fixtures.js +398 -0
  68. package/dist/report/react/format.d.ts +3 -0
  69. package/dist/report/react/format.js +7 -0
  70. package/dist/report/react/index.d.ts +6 -0
  71. package/dist/report/react/index.js +14 -0
  72. package/dist/report/report.d.ts +24 -0
  73. package/dist/report/report.js +49 -0
  74. package/dist/report/text/faces.d.ts +29 -0
  75. package/dist/report/text/faces.js +541 -0
  76. package/dist/report/text/layout.d.ts +75 -0
  77. package/dist/report/text/layout.js +173 -0
  78. package/dist/report/text/plot.d.ts +37 -0
  79. package/dist/report/text/plot.js +81 -0
  80. package/dist/report/text/table.d.ts +8 -0
  81. package/dist/report/text/table.js +86 -0
  82. package/dist/report/tree.d.ts +99 -0
  83. package/dist/report/tree.js +228 -0
  84. package/dist/report/types.d.ts +408 -0
  85. package/dist/report/types.js +4 -0
  86. package/dist/report/web.d.ts +15 -0
  87. package/dist/report/web.js +37 -0
  88. package/dist/results/locator.d.ts +75 -0
  89. package/dist/results/locator.js +0 -0
  90. package/dist/results/select.d.ts +45 -0
  91. package/dist/results/select.js +230 -0
  92. package/dist/results/source-hash.d.ts +9 -0
  93. package/dist/results/types.d.ts +204 -0
  94. package/dist/runner/eval-source.d.ts +22 -0
  95. package/dist/runner/fingerprint.d.ts +23 -0
  96. package/dist/runner/types.d.ts +332 -0
  97. package/dist/sandbox/docker-stream.d.ts +24 -0
  98. package/dist/sandbox/docker.d.ts +84 -0
  99. package/dist/sandbox/e2b.d.ts +31 -0
  100. package/dist/sandbox/errors.d.ts +4 -0
  101. package/dist/sandbox/local-files.d.ts +6 -0
  102. package/dist/sandbox/paths.d.ts +4 -0
  103. package/dist/sandbox/registry.d.ts +13 -0
  104. package/dist/sandbox/resolve.d.ts +40 -0
  105. package/dist/sandbox/retry.d.ts +10 -0
  106. package/dist/sandbox/shell.d.ts +11 -0
  107. package/dist/sandbox/source-files.d.ts +13 -0
  108. package/dist/sandbox/types.d.ts +203 -0
  109. package/dist/sandbox/vercel.d.ts +37 -0
  110. package/dist/scoring/types.d.ts +70 -0
  111. package/dist/shared/aggregate.d.ts +29 -0
  112. package/dist/shared/aggregate.js +55 -0
  113. package/dist/shared/types.d.ts +27 -0
  114. package/dist/shared/verdict.d.ts +27 -0
  115. package/dist/shared/verdict.js +34 -0
  116. package/dist/tty-line.d.ts +6 -0
  117. package/dist/types.d.ts +7 -0
  118. package/dist/util.d.ts +22 -0
  119. package/docs-site/zh/concepts/adapter.mdx +7 -7
  120. package/docs-site/zh/concepts/assert.mdx +19 -18
  121. package/docs-site/zh/concepts/evals.mdx +12 -11
  122. package/docs-site/zh/concepts/experiment.mdx +2 -2
  123. package/docs-site/zh/concepts/judge.mdx +9 -9
  124. package/docs-site/zh/concepts/overview.mdx +3 -3
  125. package/docs-site/zh/concepts/tier.mdx +6 -6
  126. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  127. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  128. package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
  129. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  130. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  131. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  132. package/docs-site/zh/guides/experiments.mdx +7 -1
  133. package/docs-site/zh/guides/fixtures.mdx +4 -2
  134. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  135. package/docs-site/zh/guides/publish-report.mdx +4 -4
  136. package/docs-site/zh/guides/report-components.mdx +194 -55
  137. package/docs-site/zh/guides/reporters.mdx +2 -2
  138. package/docs-site/zh/guides/results-data.mdx +61 -34
  139. package/docs-site/zh/guides/runner.mdx +2 -2
  140. package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
  141. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
  142. package/docs-site/zh/guides/viewing-results.mdx +200 -79
  143. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  144. package/docs-site/zh/guides/write-send.mdx +4 -3
  145. package/docs-site/zh/index.mdx +4 -3
  146. package/docs-site/zh/introduction.mdx +2 -1
  147. package/docs-site/zh/quickstart.mdx +5 -3
  148. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  149. package/docs-site/zh/reference/capabilities.mdx +1 -1
  150. package/docs-site/zh/reference/cli.mdx +36 -13
  151. package/docs-site/zh/reference/define-agent.mdx +15 -13
  152. package/docs-site/zh/reference/define-config.mdx +1 -1
  153. package/docs-site/zh/reference/define-eval.mdx +3 -3
  154. package/docs-site/zh/reference/events.mdx +9 -3
  155. package/docs-site/zh/reference/expect.mdx +13 -8
  156. package/package.json +10 -6
  157. package/src/agents/bub.ts +129 -42
  158. package/src/agents/claude-code.test.ts +152 -0
  159. package/src/agents/claude-code.ts +127 -13
  160. package/src/agents/codex.test.ts +192 -0
  161. package/src/agents/codex.ts +151 -12
  162. package/src/agents/index.ts +9 -3
  163. package/src/agents/manifest.ts +41 -0
  164. package/src/agents/plugin-config.test.ts +94 -0
  165. package/src/agents/shared.ts +33 -2
  166. package/src/agents/skills.test.ts +214 -0
  167. package/src/agents/skills.ts +304 -0
  168. package/src/agents/streaming.ts +5 -5
  169. package/src/agents/types.ts +59 -3
  170. package/src/cli.ts +74 -47
  171. package/src/context/context.test.ts +20 -2
  172. package/src/context/control-flow.ts +1 -1
  173. package/src/context/session.test.ts +92 -1
  174. package/src/context/session.ts +23 -1
  175. package/src/context/types.ts +4 -4
  176. package/src/define.test.ts +8 -8
  177. package/src/define.ts +11 -11
  178. package/src/expect/index.ts +5 -5
  179. package/src/i18n/en.ts +33 -15
  180. package/src/i18n/zh-CN.ts +37 -21
  181. package/src/index.ts +1 -1
  182. package/src/o11y/execution-tree.test.ts +452 -0
  183. package/src/o11y/execution-tree.ts +367 -0
  184. package/src/o11y/otlp/receiver.ts +1 -1
  185. package/src/o11y/otlp/select.ts +7 -3
  186. package/src/o11y/parsers/claude-code.test.ts +175 -0
  187. package/src/o11y/parsers/claude-code.ts +32 -0
  188. package/src/o11y/types.ts +6 -1
  189. package/src/report/aggregate.ts +26 -20
  190. package/src/report/built-in-user-parity.test.tsx +640 -0
  191. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  192. package/src/report/built-ins/index.ts +2 -0
  193. package/src/report/components.tsx +225 -50
  194. package/src/report/compute.ts +335 -54
  195. package/src/report/dual-render.test.tsx +1129 -0
  196. package/src/report/flag.ts +1 -1
  197. package/src/report/format.ts +35 -2
  198. package/src/report/index.ts +47 -14
  199. package/src/report/load.ts +1 -1
  200. package/src/report/locale.ts +229 -0
  201. package/src/report/metrics.ts +39 -15
  202. package/src/report/primitives.tsx +100 -2
  203. package/src/report/react/AttemptList.tsx +113 -0
  204. package/src/report/react/DeltaTable.tsx +9 -6
  205. package/src/report/react/EvalList.tsx +0 -0
  206. package/src/report/react/ExperimentList.tsx +220 -0
  207. package/src/report/react/GroupSummary.tsx +66 -0
  208. package/src/report/react/MetricBars.tsx +24 -13
  209. package/src/report/react/MetricLine.tsx +21 -21
  210. package/src/report/react/MetricMatrix.tsx +8 -4
  211. package/src/report/react/MetricScatter.tsx +126 -96
  212. package/src/report/react/MetricTable.tsx +115 -15
  213. package/src/report/react/RunOverview.tsx +48 -26
  214. package/src/report/react/Scoreboard.tsx +15 -7
  215. package/src/report/react/cell.tsx +18 -9
  216. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  217. package/src/report/react/colors.ts +24 -15
  218. package/src/report/react/enhance.js +168 -0
  219. package/src/report/react/fixtures.ts +169 -34
  220. package/src/report/react/format.ts +1 -1
  221. package/src/report/react/index.tsx +21 -6
  222. package/src/report/react/render.test.tsx +180 -31
  223. package/src/report/react/styles.css +432 -213
  224. package/src/report/report.test.ts +398 -94
  225. package/src/report/report.ts +29 -11
  226. package/src/report/text/faces.ts +344 -96
  227. package/src/report/text/layout.ts +76 -11
  228. package/src/report/text/table.ts +98 -0
  229. package/src/report/tree.ts +103 -20
  230. package/src/report/types.ts +184 -25
  231. package/src/report/web.ts +46 -19
  232. package/src/results/annotated-source.test.ts +146 -0
  233. package/src/results/annotated-source.ts +128 -0
  234. package/src/results/attempt-evidence.test.ts +158 -0
  235. package/src/results/attempt-evidence.ts +158 -0
  236. package/src/results/attempt-source.ts +48 -0
  237. package/src/results/copy.ts +154 -139
  238. package/src/results/format.ts +54 -47
  239. package/src/results/host-equivalence.test.ts +527 -0
  240. package/src/results/index.ts +56 -14
  241. package/src/results/locator.test.ts +198 -0
  242. package/src/results/locator.ts +0 -0
  243. package/src/results/open.ts +392 -175
  244. package/src/results/results.test.ts +943 -494
  245. package/src/results/select.ts +129 -23
  246. package/src/results/skipped-notice.ts +0 -0
  247. package/src/results/source-hash.ts +28 -0
  248. package/src/results/types.ts +98 -62
  249. package/src/results/writer.ts +304 -176
  250. package/src/runner/attempt.test.ts +170 -0
  251. package/src/runner/attempt.ts +48 -13
  252. package/src/runner/discover.test.ts +65 -0
  253. package/src/runner/discover.ts +6 -2
  254. package/src/runner/eval-source.test.ts +121 -0
  255. package/src/runner/eval-source.ts +45 -0
  256. package/src/runner/fingerprint.ts +54 -1
  257. package/src/runner/remote-sandbox.ts +1 -1
  258. package/src/runner/report.test.ts +6 -8
  259. package/src/runner/report.ts +5 -4
  260. package/src/runner/reporters/artifacts.ts +27 -41
  261. package/src/runner/reporters/braintrust.test.ts +3 -3
  262. package/src/runner/reporters/braintrust.ts +1 -1
  263. package/src/runner/reporters/console.ts +6 -6
  264. package/src/runner/reporters/json.ts +4 -4
  265. package/src/runner/reporters/live.test.ts +56 -0
  266. package/src/runner/reporters/live.ts +36 -10
  267. package/src/runner/reporters/quiet.test.ts +66 -0
  268. package/src/runner/reporters/quiet.ts +49 -0
  269. package/src/runner/reporters/shared.ts +5 -5
  270. package/src/runner/reporters/table.ts +17 -17
  271. package/src/runner/run.ts +61 -102
  272. package/src/runner/types.ts +43 -32
  273. package/src/sandbox/checkpoint.ts +1 -1
  274. package/src/sandbox/docker.ts +15 -2
  275. package/src/sandbox/e2b.ts +11 -5
  276. package/src/sandbox/errors.ts +13 -0
  277. package/src/sandbox/index.ts +3 -3
  278. package/src/sandbox/local-files.ts +1 -1
  279. package/src/sandbox/registry.ts +6 -5
  280. package/src/sandbox/resolve.ts +38 -23
  281. package/src/sandbox/retry.test.ts +68 -0
  282. package/src/sandbox/retry.ts +44 -0
  283. package/src/sandbox/types.ts +24 -24
  284. package/src/sandbox/vercel.ts +17 -3
  285. package/src/scoring/collector.ts +3 -3
  286. package/src/scoring/judge.ts +2 -2
  287. package/src/scoring/scoped.ts +13 -1
  288. package/src/scoring/types.ts +7 -7
  289. package/src/scoring/verdict.ts +4 -4
  290. package/src/shared/aggregate.ts +23 -3
  291. package/src/shared/types.ts +2 -2
  292. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  293. package/src/show/compose.ts +12 -132
  294. package/src/show/index.ts +119 -95
  295. package/src/show/render.ts +462 -159
  296. package/src/show/show.test.ts +471 -253
  297. package/src/source-loc.ts +1 -1
  298. package/src/tty-line.ts +32 -0
  299. package/src/types.ts +2 -2
  300. package/src/util.test.ts +21 -1
  301. package/src/util.ts +5 -1
  302. package/src/view/app/App.tsx +33 -198
  303. package/src/view/app/components/AttemptModal.tsx +6 -6
  304. package/src/view/app/components/CodeView.tsx +2 -2
  305. package/src/view/app/components/CopyControls.tsx +32 -36
  306. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  307. package/src/view/app/components/ui/badge.tsx +1 -1
  308. package/src/view/app/components/ui/dialog.tsx +2 -2
  309. package/src/view/app/i18n.ts +40 -31
  310. package/src/view/app/lib/artifact-url.ts +1 -1
  311. package/src/view/app/lib/attempt-route.test.ts +37 -47
  312. package/src/view/app/lib/attempt-route.ts +26 -26
  313. package/src/view/app/lib/rows.ts +9 -112
  314. package/src/view/app/lib/verdict.ts +25 -0
  315. package/src/view/app/main.tsx +9 -12
  316. package/src/view/app/pages/RunsPage.tsx +4 -4
  317. package/src/view/app/pages/TracesPage.tsx +3 -3
  318. package/src/view/app/shared.ts +0 -1
  319. package/src/view/app/types.ts +6 -51
  320. package/src/view/artifact-serving.test.ts +136 -0
  321. package/src/view/client-dist/app.css +1 -1
  322. package/src/view/client-dist/app.js +21 -21
  323. package/src/view/data.test.ts +214 -143
  324. package/src/view/data.ts +166 -120
  325. package/src/view/index.ts +23 -11
  326. package/src/view/server.ts +52 -17
  327. package/src/view/shared/types.ts +22 -31
  328. package/src/view/styles.css +117 -327
  329. package/src/view/view-report.test.ts +146 -98
  330. package/src/report/default-report.tsx +0 -222
  331. package/src/report/dual-face.test.tsx +0 -527
  332. package/src/report/react/CaseList.tsx +0 -70
  333. package/src/view/app/components/CostScoreChart.tsx +0 -147
  334. package/src/view/app/components/ExperimentTable.tsx +0 -284
  335. package/src/view/app/components/GroupSelector.tsx +0 -61
  336. package/src/view/app/components/primitives.tsx +0 -43
  337. 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, ResultOutcome, RunSummary } from "../types.ts";
10
- import type { AttemptHandle, RunDir, Selection, SelectionWarning, Snapshot } from "../results/index.ts";
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
- CaseList,
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, outcome: ResultOutcome, extra: Partial<EvalResult> = {}): EvalResult {
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
- outcome,
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
- /** 最小构造:一个 run 装一个快照。runStartedAt 决定去重时谁是「最新 run」。 */
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 summary: RunSummary = {
66
- agent: spec.agent ?? "agent-x",
87
+ const dir = `/results/exp/snap-${runSeq}`;
88
+ const snapshot = {
89
+ experimentId: spec.experimentId,
67
90
  startedAt,
68
91
  completedAt: startedAt,
69
- passed: 0,
70
- failed: 0,
71
- skipped: 0,
72
- errored: 0,
73
- durationMs: 0,
74
- results: spec.results,
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: { run: `run-${runSeq}`, result: i },
82
- runDir,
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
- return {
97
- experimentId: spec.experimentId,
98
- startedAt,
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
- /** 手工造一个选集(warnings 随行的形状,filter 语义与 results 一致)。 */
122
+ /** 手工造一个 Selection(warnings 随行的形状,filter 语义与 results 一致)。 */
110
123
  function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selection {
111
124
  return {
112
125
  snapshots,
@@ -122,6 +135,182 @@ function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selecti
122
135
  };
123
136
  }
124
137
 
138
+ describe("ExperimentList.data", () => {
139
+ it("身份、flags、Eval 判定构成、官方两级聚合汇总指标,展开到 Eval 携带全部 Attempt 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
+ // 每次 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");
190
+ });
191
+
192
+ it("展开数组按 evalId 升序,顶层数组按 experimentId 升序", async () => {
193
+ const s1 = snap({ experimentId: "exp/b", results: [res("z", "passed"), res("a", "passed")] });
194
+ const s2 = snap({ experimentId: "exp/a", results: [res("x", "passed")] });
195
+ const items = await ExperimentList.data([s1, s2]);
196
+ expect(items.map((i) => i.experimentId)).toEqual(["exp/a", "exp/b"]);
197
+ expect(items.find((i) => i.experimentId === "exp/b")!.evalRows.map((e) => e.evalId)).toEqual(["a", "z"]);
198
+ });
199
+ });
200
+
201
+ describe("EvalList.data", () => {
202
+ it("每项一个 experimentId + evalId,判定/分数/成本耗时均值/失败原因与展开到 Attempt 都在", async () => {
203
+ const s = snap({
204
+ experimentId: "compare/codex",
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
+ ],
219
+ });
220
+ const items = await EvalList.data([s]);
221
+ expect(items).toHaveLength(1);
222
+ const item = items[0]!;
223
+ expect(item.evalId).toBe("weather/brooklyn");
224
+ expect(item.experimentId).toBe("compare/codex");
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"]);
240
+ });
241
+ });
242
+
243
+ describe("AttemptList.data", () => {
244
+ it("每项一个 Attempt:身份、判定、assertions、耗时、成本与 locator", async () => {
245
+ const s = snap({
246
+ experimentId: "exp/x",
247
+ results: [
248
+ res("A", "failed", {
249
+ durationMs: 5_000,
250
+ usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.05 },
251
+ assertions: [{ name: "gate", severity: "gate", passed: false, score: 0, detail: "nope" }],
252
+ hasEvents: true,
253
+ hasSources: true,
254
+ }),
255
+ ],
256
+ });
257
+ const items = await AttemptList.data([s]);
258
+ expect(items).toHaveLength(1);
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}$/);
271
+ });
272
+
273
+ it("redact 作用于 error 与断言 detail/evidence,不作用于身份字段", async () => {
274
+ const s = snap({
275
+ experimentId: "exp/x",
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
+ ],
291
+ });
292
+ const items = await AttemptList.data([s], { redact: (text) => text.replaceAll("/Users/me/repo", "<repo>") });
293
+ const a = items.find((i) => i.evalId === "A")!;
294
+ expect(a.error).toBe("ENOENT <repo>/tool");
295
+ const b = items.find((i) => i.evalId === "B")!;
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);
300
+ });
301
+
302
+ it("不预设只看失败:passed 的 attempt 同样列出,原样携带 assertions", async () => {
303
+ const s = snap({
304
+ experimentId: "exp/x",
305
+ results: [res("A", "passed", { assertions: [{ name: "ok", severity: "gate", passed: true, score: 1 }] })],
306
+ });
307
+ const items = await AttemptList.data([s]);
308
+ expect(items).toHaveLength(1);
309
+ expect(items[0]).toMatchObject({ verdict: "passed" });
310
+ expect(items[0]!.assertions).toHaveLength(1);
311
+ });
312
+ });
313
+
125
314
  // ───────────────────────── 两级聚合 ─────────────────────────
126
315
 
127
316
  describe("两级聚合引擎", () => {
@@ -146,7 +335,7 @@ describe("两级聚合引擎", () => {
146
335
  name: "pass@k",
147
336
  better: "higher",
148
337
  unit: "%",
149
- value: (a) => (a.result.outcome === "skipped" ? null : a.result.outcome === "passed" ? 1 : 0),
338
+ value: (a) => (a.result.verdict === "skipped" ? null : a.result.verdict === "passed" ? 1 : 0),
150
339
  aggregate: { perEval: "max", across: "mean" },
151
340
  });
152
341
  const s = snap({
@@ -184,7 +373,7 @@ describe("两级聚合引擎", () => {
184
373
  it("where 不满足 → null,不进聚合", async () => {
185
374
  const onlyPassed = defineMetric({
186
375
  name: "only-passed",
187
- where: (a) => a.result.outcome === "passed",
376
+ where: (a) => a.result.verdict === "passed",
188
377
  value: () => 5,
189
378
  });
190
379
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
@@ -339,6 +528,22 @@ describe("内置指标", () => {
339
528
  const dur = await MetricTable.data([s], { rows: "agent", columns: [durationMs] });
340
529
  expect(dur.rows[0].cells["duration"].value).toBe(2000); // (1000 + 3000)/2,errored 实测照算
341
530
  });
531
+
532
+ it("turns:读 o11y.totalTurns;o11y 缺失(未随发布带上)→ null,不是 0;skipped 不进聚合", async () => {
533
+ const s = snap({
534
+ experimentId: "exp/x",
535
+ results: [
536
+ res("A", "passed", { o11y: o11ySummary(12) }),
537
+ res("B", "failed"), // 没带 o11y(如 copySnapshots 漏选 artifact)→ null,不稀释成 0
538
+ res("C", "skipped", { o11y: o11ySummary(3) }), // skipped 恒 null,哪怕 o11y 在场
539
+ ],
540
+ });
541
+ const data = await MetricTable.data([s], { rows: "agent", columns: [turns] });
542
+ const cell = data.rows[0].cells["turns"];
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 数,不是「本该测得」的分母
546
+ });
342
547
  });
343
548
 
344
549
  // ───────────────────────── Scoreboard.data ─────────────────────────
@@ -468,10 +673,10 @@ describe("MetricScatter.data", () => {
468
673
  // ───────────────────────── flag():维度与轴 ─────────────────────────
469
674
 
470
675
  describe("flag()", () => {
471
- const withFlags = (id: string, flags: Record<string, unknown> | undefined, outcome: ResultOutcome) =>
676
+ const withFlags = (id: string, flags: Record<string, unknown> | undefined, verdict: Verdict) =>
472
677
  snap({
473
678
  experimentId: id,
474
- results: [res("A", outcome, { experimentId: id, experiment: { id, flags } })],
679
+ results: [res("A", verdict, { experimentId: id, experiment: { id, flags } })],
475
680
  });
476
681
 
477
682
  it("MetricLine.data:x 收 flag、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
@@ -512,6 +717,63 @@ describe("flag()", () => {
512
717
  });
513
718
  });
514
719
 
720
+ // ───────────────────────── MetricTable.data:没有实体下钻 ─────────────────────────
721
+
722
+ describe("MetricTable.data", () => {
723
+ it("rows: \"experiment\" 的 meta 不带 subRows —— MetricTable 只表达维度 × 指标,没有 expand", async () => {
724
+ const s = snap({
725
+ experimentId: "exp/x",
726
+ results: [res("A", "passed"), res("B", "failed")],
727
+ });
728
+ const data = await MetricTable.data([s], { rows: "experiment", columns: [passRate] });
729
+ expect(data.rows).toHaveLength(1);
730
+ expect(data.rows[0]!.meta).not.toHaveProperty("subRows");
731
+ // 展开到 Eval 是 ExperimentList 的职责,不在这里验证 —— 见 ExperimentList.data 的测试组。
732
+ });
733
+ });
734
+
735
+ // ───────────────────────── reasonFor(原因优先级)─────────────────────────
736
+ // docs/feature/reports/show.md 裸 `show` 的 RESULT 列口径:error → skipReason → 未通过的 gate 断言。
737
+ // ExperimentList / EvalList 的 evalRows.reason、AttemptList 的 attemptItemReason 都port这同一份材料,
738
+ // 这里直接测 reasonFor 本身,不必再经 MetricTable 之类的中间组件绕一圈。
739
+
740
+ describe("reasonFor", () => {
741
+ it("同一 result 同时含 error、失败 gate、失败 soft → 只显示 error", () => {
742
+ const result = res("A", "errored", {
743
+ error: "adapter crashed",
744
+ assertions: [
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");
750
+ });
751
+
752
+ it("skipReason 优先于 gate 断言,但让位给 error", () => {
753
+ const result = res("A", "skipped", {
754
+ skipReason: "missing fixture",
755
+ assertions: [{ name: "includes", severity: "gate", score: 0, passed: false }],
756
+ });
757
+ expect(reasonFor(result)).toBe("missing fixture");
758
+ });
759
+
760
+ it("多个失败 gate 按原始声明顺序全部保留,拼接成一行;失败 soft 不出现在 reason 里", () => {
761
+ const result = res("A", "failed", {
762
+ assertions: [
763
+ softAssertion("judge-first", 0.1, { passed: false }), // 声明顺序第一,但 soft 不进 reason
764
+ { name: "includes", severity: "gate", score: 0, passed: false, detail: "missing text" },
765
+ { name: "matches", severity: "gate", score: 0, passed: false }, // 无 detail,只有 name
766
+ softAssertion("judge-last", 0.3, { passed: false }), // soft 依旧不进 reason
767
+ ],
768
+ });
769
+ expect(reasonFor(result)).toBe("includes: missing text, matches");
770
+ });
771
+
772
+ it("都缺席 → 无原因", () => {
773
+ expect(reasonFor(res("A", "passed"))).toBeUndefined();
774
+ });
775
+ });
776
+
515
777
  // ───────────────────────── RunOverview.data ─────────────────────────
516
778
 
517
779
  describe("RunOverview.data", () => {
@@ -553,6 +815,103 @@ describe("RunOverview.data", () => {
553
815
  });
554
816
  });
555
817
 
818
+ // ───────────────────────── GroupSummary.data ─────────────────────────
819
+
820
+ describe("GroupSummary.data", () => {
821
+ it("多 experiment、多 attempt、部分成本缺失、不同 startedAt:eval 级折叠计票(完整身份键)、总成本 null-safe 求和、lastRunAt 取组内最大值", async () => {
822
+ const expA = snap({
823
+ experimentId: "exp/a",
824
+ runStartedAt: "2026-07-01T08:00:00Z",
825
+ results: [
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
+ });
841
+
842
+ const data = await GroupSummary.data([expA, expB]);
843
+
844
+ expect(data.experiments).toBe(2);
845
+ // 3 道题:exp/a 的 shared、exp/a 的 only-a、exp/b 的 shared —— 完整身份键各算各的
846
+ expect(data.evals).toBe(3);
847
+ expect(data.attempts).toBe(4); // 原始 attempt 计数:exp/a 2(shared)+1(only-a)+ exp/b 1(shared)
848
+ expect(data.verdicts).toEqual({ passed: 1, failed: 1, errored: 1, skipped: 0 });
849
+ // eval 级折叠通过率:1 / (1 + 1 + 1) = 1/3,不是 attempt 原始占比(1 passed / 4 attempts = 25%)
850
+ expect(data.passRate.value).toBeCloseTo(1 / 3, 10);
851
+ expect(data.passRate.display).toBe("33.3%");
852
+ expect(data.passRate.samples).toBe(3);
853
+ expect(data.passRate.total).toBe(3);
854
+ // 总成本 null-safe 求和:0.5(实测)+ 0.2(估算)= 0.7;only-a 的失败 attempt 没报成本,不计 0
855
+ expect(data.totalCostUSD).toBeCloseTo(0.7, 10);
856
+ // 最后运行时间:组内两个快照的 startedAt 取最大值(exp/b 更晚)
857
+ expect(data.lastRunAt).toBe(expB.startedAt);
858
+ });
859
+
860
+ it('跨 experiment 同名 eval 不合并:两个 experiment 各自的 "x" 独立计一票,不是被折成一票', async () => {
861
+ const a = snap({ experimentId: "exp/a", results: [res("x", "passed")] });
862
+ const b = snap({ experimentId: "exp/b", results: [res("x", "failed")] });
863
+ const data = await GroupSummary.data([a, b]);
864
+ expect(data.experiments).toBe(2);
865
+ expect(data.evals).toBe(2); // 只按 eval id 折叠(误把两个 experiment 的 "x" 当一道题)会变成 1
866
+ expect(data.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
867
+ expect(data.passRate.value).toBeCloseTo(0.5, 10); // 误合并会因为「任一轮过即过」变成 100%
868
+ });
869
+
870
+ it("全组没有任何 attempt 报成本 → totalCostUSD null,不编 0", async () => {
871
+ const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
872
+ const data = await GroupSummary.data([s]);
873
+ expect(data.totalCostUSD).toBeNull();
874
+ });
875
+
876
+ it("分母为 0(组内 eval 全部 skipped)→ passRate.value 为 null,不编 0%", async () => {
877
+ const s = snap({ experimentId: "exp/x", results: [res("A", "skipped"), res("B", "skipped")] });
878
+ const data = await GroupSummary.data([s]);
879
+ expect(data.verdicts).toEqual({ passed: 0, failed: 0, errored: 0, skipped: 2 });
880
+ expect(data.passRate.value).toBeNull();
881
+ expect(data.passRate.display).toBe("—");
882
+ expect(data.passRate.samples).toBe(0);
883
+ expect(data.passRate.total).toBe(2);
884
+ });
885
+
886
+ it("lastRunAt 只从传入的组 Selection 取值,不读全局最新 —— 组外快照更晚也不影响", async () => {
887
+ const inGroup = snap({
888
+ experimentId: "exp/a",
889
+ runStartedAt: "2026-07-01T08:00:00Z",
890
+ results: [res("A", "passed")],
891
+ });
892
+ const outOfGroup = snap({
893
+ experimentId: "exp/b",
894
+ runStartedAt: "2026-07-09T08:00:00Z",
895
+ results: [res("B", "passed")],
896
+ });
897
+ const sel = selection([inGroup, outOfGroup], []);
898
+ const scoped = sel.filter((s) => s.experimentId === "exp/a"); // 只留组内快照,模拟按实验组收窄的 Selection
899
+ const data = await GroupSummary.data(scoped);
900
+ expect(data.lastRunAt).toBe(inGroup.startedAt);
901
+ expect(data.lastRunAt).not.toBe(outOfGroup.startedAt);
902
+ });
903
+
904
+ it('MetricTable rows: "experiment" 的 verdicts meta 复用同一份 eval 级统计,行为不变(回归)', async () => {
905
+ const s = snap({
906
+ experimentId: "exp/x",
907
+ results: [res("A", "passed", { attempt: 0 }), res("A", "failed", { attempt: 1 }), res("B", "failed")],
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 });
912
+ });
913
+ });
914
+
556
915
  // ───────────────────────── DeltaTable.data ─────────────────────────
557
916
 
558
917
  describe("DeltaTable.data", () => {
@@ -622,61 +981,6 @@ describe("DeltaTable.data", () => {
622
981
  });
623
982
  });
624
983
 
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
984
  // ───────────────────────── 身份键去重 ─────────────────────────
681
985
 
682
986
  describe("身份键去重", () => {