niceeval 0.9.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/INDEX.md +31 -33
  2. package/dist/agents/types.d.ts +20 -17
  3. package/dist/context/types.d.ts +21 -21
  4. package/dist/i18n/en.d.ts +10 -2
  5. package/dist/i18n/zh-CN.d.ts +14 -6
  6. package/dist/o11y/execution-tree.d.ts +103 -0
  7. package/dist/o11y/otlp/select.d.ts +22 -0
  8. package/dist/report/{react → assets}/colors.d.ts +8 -0
  9. package/dist/report/{react → assets}/colors.js +23 -0
  10. package/dist/report/built-in/index.d.ts +2 -2
  11. package/dist/report/built-in/index.js +2 -2
  12. package/dist/report/built-in/standard.d.ts +8 -1
  13. package/dist/report/built-in/standard.js +17 -7
  14. package/dist/report/components/attempt-detail/AttemptAssertions.d.ts +6 -0
  15. package/dist/report/components/attempt-detail/AttemptAssertions.js +17 -0
  16. package/dist/report/components/attempt-detail/AttemptConversation.d.ts +10 -0
  17. package/dist/report/components/attempt-detail/AttemptConversation.js +74 -0
  18. package/dist/report/components/attempt-detail/AttemptDiagnostics.d.ts +6 -0
  19. package/dist/report/components/attempt-detail/AttemptDiagnostics.js +7 -0
  20. package/dist/report/components/attempt-detail/AttemptDiff.d.ts +6 -0
  21. package/dist/report/components/attempt-detail/AttemptDiff.js +11 -0
  22. package/dist/report/components/attempt-detail/AttemptError.d.ts +6 -0
  23. package/dist/report/components/attempt-detail/AttemptError.js +16 -0
  24. package/dist/report/components/attempt-detail/AttemptFixPrompt.d.ts +6 -0
  25. package/dist/report/components/attempt-detail/AttemptFixPrompt.js +7 -0
  26. package/dist/report/components/attempt-detail/AttemptSource.d.ts +6 -0
  27. package/dist/report/components/attempt-detail/AttemptSource.js +97 -0
  28. package/dist/report/components/attempt-detail/AttemptSummary.d.ts +8 -0
  29. package/dist/report/components/attempt-detail/AttemptSummary.js +20 -0
  30. package/dist/report/components/attempt-detail/AttemptTimeline.d.ts +6 -0
  31. package/dist/report/components/attempt-detail/AttemptTimeline.js +28 -0
  32. package/dist/report/components/attempt-detail/AttemptTrace.d.ts +6 -0
  33. package/dist/report/components/attempt-detail/AttemptTrace.js +29 -0
  34. package/dist/report/components/attempt-detail/AttemptUsage.d.ts +6 -0
  35. package/dist/report/components/attempt-detail/AttemptUsage.js +20 -0
  36. package/dist/report/components/attempt-detail/compute.d.ts +24 -0
  37. package/dist/report/components/attempt-detail/compute.js +297 -0
  38. package/dist/report/components/attempt-detail/faces.d.ts +14 -0
  39. package/dist/report/components/attempt-detail/faces.js +239 -0
  40. package/dist/report/components/attempt-detail/index.d.ts +38 -0
  41. package/dist/report/components/attempt-detail/index.js +547 -0
  42. package/dist/report/{react → components}/cell.d.ts +5 -3
  43. package/dist/report/{react → components}/cell.js +3 -3
  44. package/dist/report/{react → components/entity-lists}/AttemptList.d.ts +8 -4
  45. package/dist/report/{react → components/entity-lists}/AttemptList.js +16 -10
  46. package/dist/report/{react → components/entity-lists}/EvalList.d.ts +3 -3
  47. package/dist/report/{react → components/entity-lists}/EvalList.js +0 -0
  48. package/dist/report/{react → components/entity-lists}/ExperimentList.d.ts +4 -5
  49. package/dist/report/{react → components/entity-lists}/ExperimentList.js +12 -11
  50. package/dist/report/components/entity-lists/compute.d.ts +23 -0
  51. package/dist/report/components/entity-lists/compute.js +171 -0
  52. package/dist/report/components/entity-lists/faces.d.ts +5 -0
  53. package/dist/report/components/entity-lists/faces.js +175 -0
  54. package/dist/report/components/entity-lists/index.d.ts +49 -0
  55. package/dist/report/components/entity-lists/index.js +169 -0
  56. package/dist/report/{react → components}/fixtures.d.ts +2 -2
  57. package/dist/report/{react → components}/fixtures.js +1 -1
  58. package/dist/report/{react → components/metric-views}/DeltaTable.d.ts +2 -2
  59. package/dist/report/{react → components/metric-views}/DeltaTable.js +4 -4
  60. package/dist/report/{react → components/metric-views}/MetricBars.d.ts +3 -3
  61. package/dist/report/{react → components/metric-views}/MetricBars.js +3 -3
  62. package/dist/report/{react → components/metric-views}/MetricLine.d.ts +2 -2
  63. package/dist/report/{react → components/metric-views}/MetricLine.js +8 -5
  64. package/dist/report/{react → components/metric-views}/MetricMatrix.d.ts +3 -3
  65. package/dist/report/{react → components/metric-views}/MetricMatrix.js +4 -4
  66. package/dist/report/components/metric-views/MetricScatter.d.ts +11 -0
  67. package/dist/report/{react → components/metric-views}/MetricScatter.js +27 -60
  68. package/dist/report/{react → components/metric-views}/MetricTable.d.ts +3 -3
  69. package/dist/report/{react → components/metric-views}/MetricTable.js +4 -4
  70. package/dist/report/{react → components/metric-views}/Scoreboard.d.ts +2 -2
  71. package/dist/report/{react → components/metric-views}/Scoreboard.js +3 -3
  72. package/dist/report/components/metric-views/compute.d.ts +89 -0
  73. package/dist/report/{compute.js → components/metric-views/compute.js} +32 -415
  74. package/dist/report/components/metric-views/faces.d.ts +13 -0
  75. package/dist/report/components/metric-views/faces.js +381 -0
  76. package/dist/report/components/metric-views/index.d.ts +50 -0
  77. package/dist/report/components/metric-views/index.js +273 -0
  78. package/dist/report/{text → components/metric-views}/plot.js +1 -1
  79. package/dist/report/components/shared-compute.d.ts +22 -0
  80. package/dist/report/components/shared-compute.js +47 -0
  81. package/dist/report/components/shared-faces.d.ts +9 -0
  82. package/dist/report/components/shared-faces.js +26 -0
  83. package/dist/report/components/shared.d.ts +68 -0
  84. package/dist/report/components/shared.js +125 -0
  85. package/dist/report/{react → components/site-components}/CopyFixPrompt.d.ts +2 -2
  86. package/dist/report/{react → components/site-components}/CopyFixPrompt.js +2 -2
  87. package/dist/report/{react → components/site-components}/HeroCard.d.ts +2 -2
  88. package/dist/report/{react → components/site-components}/HeroCard.js +2 -2
  89. package/dist/report/{react → components/site-components}/ScopeWarnings.d.ts +2 -2
  90. package/dist/report/{react → components/site-components}/ScopeWarnings.js +3 -3
  91. package/dist/report/{react → components/site-components}/TraceWaterfall.d.ts +3 -3
  92. package/dist/report/{react → components/site-components}/TraceWaterfall.js +5 -5
  93. package/dist/report/components/site-components/compute.d.ts +28 -0
  94. package/dist/report/components/site-components/compute.js +132 -0
  95. package/dist/report/components/site-components/faces.d.ts +21 -0
  96. package/dist/report/components/site-components/faces.js +75 -0
  97. package/dist/report/components/site-components/index.d.ts +74 -0
  98. package/dist/report/components/site-components/index.js +220 -0
  99. package/dist/report/{scope-warnings.d.ts → components/site-components/scope-warnings.d.ts} +2 -2
  100. package/dist/report/{scope-warnings.js → components/site-components/scope-warnings.js} +2 -2
  101. package/dist/report/{react → components/summaries}/ScopeSummary.d.ts +2 -2
  102. package/dist/report/{react → components/summaries}/ScopeSummary.js +17 -8
  103. package/dist/report/components/summaries/compute.d.ts +7 -0
  104. package/dist/report/components/summaries/compute.js +51 -0
  105. package/dist/report/components/summaries/faces.d.ts +7 -0
  106. package/dist/report/components/summaries/faces.js +38 -0
  107. package/dist/report/components/summaries/index.d.ts +27 -0
  108. package/dist/report/components/summaries/index.js +78 -0
  109. package/dist/report/definition/grid-layout.d.ts +50 -0
  110. package/dist/report/definition/grid-layout.js +89 -0
  111. package/dist/report/{primitives.d.ts → definition/primitives.d.ts} +34 -4
  112. package/dist/report/{primitives.js → definition/primitives.js} +101 -10
  113. package/dist/report/{report.d.ts → definition/report.d.ts} +36 -65
  114. package/dist/report/{report.js → definition/report.js} +58 -90
  115. package/dist/report/{text/table.d.ts → definition/table-text.d.ts} +2 -2
  116. package/dist/report/{text/table.js → definition/table-text.js} +2 -2
  117. package/dist/report/{tree.d.ts → definition/tree.d.ts} +43 -11
  118. package/dist/report/{tree.js → definition/tree.js} +9 -11
  119. package/dist/report/index.d.ts +33 -18
  120. package/dist/report/index.js +23 -14
  121. package/dist/report/{aggregate.d.ts → model/aggregate.d.ts} +12 -4
  122. package/dist/report/{aggregate.js → model/aggregate.js} +28 -8
  123. package/dist/report/{flag.d.ts → model/flag.d.ts} +14 -1
  124. package/dist/report/{flag.js → model/flag.js} +35 -0
  125. package/dist/report/{format.d.ts → model/format.d.ts} +13 -7
  126. package/dist/report/{format.js → model/format.js} +79 -9
  127. package/dist/report/{locale.d.ts → model/locale.d.ts} +17 -17
  128. package/dist/report/{locale.js → model/locale.js} +31 -33
  129. package/dist/report/{metrics.d.ts → model/metrics.d.ts} +2 -2
  130. package/dist/report/{metrics.js → model/metrics.js} +2 -2
  131. package/dist/report/{types.d.ts → model/types.d.ts} +154 -20
  132. package/dist/report/react/index.d.ts +32 -21
  133. package/dist/report/react/index.js +33 -21
  134. package/dist/report/{load.d.ts → runtime/load.d.ts} +1 -1
  135. package/dist/report/{load.js → runtime/load.js} +1 -1
  136. package/dist/report/runtime/text.d.ts +67 -0
  137. package/dist/report/runtime/text.js +114 -0
  138. package/dist/report/runtime/web.d.ts +35 -0
  139. package/dist/report/runtime/web.js +70 -0
  140. package/dist/results/annotated-source.d.ts +87 -0
  141. package/dist/results/attempt-evidence.d.ts +74 -0
  142. package/dist/results/attempt-source.d.ts +15 -0
  143. package/dist/results/select.d.ts +11 -1
  144. package/dist/results/select.js +27 -5
  145. package/dist/runner/eval-selection.d.ts +26 -0
  146. package/dist/runner/feedback/sink.d.ts +26 -1
  147. package/dist/runner/types.d.ts +151 -24
  148. package/dist/sandbox/docker.d.ts +6 -0
  149. package/dist/sandbox/e2b.d.ts +0 -1
  150. package/dist/sandbox/types.d.ts +23 -21
  151. package/dist/scoring/display.d.ts +7 -2
  152. package/dist/scoring/display.js +42 -4
  153. package/dist/scoring/types.d.ts +3 -3
  154. package/dist/shared/aggregate.d.ts +14 -0
  155. package/dist/shared/aggregate.js +31 -0
  156. package/dist/shared/types.d.ts +6 -1
  157. package/docs-site/images/logo-dark.svg +7 -0
  158. package/docs-site/images/logo.svg +6 -5
  159. package/docs-site/zh/README.md +8 -7
  160. package/docs-site/zh/examples/ai-agent-application.mdx +5 -5
  161. package/docs-site/zh/examples/coding-agent-extensions.mdx +4 -4
  162. package/docs-site/zh/examples/index.mdx +3 -3
  163. package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +6 -6
  164. package/docs-site/zh/examples/integrations/claude-sdk.mdx +3 -3
  165. package/docs-site/zh/examples/integrations/codex-sdk.mdx +3 -3
  166. package/docs-site/zh/examples/integrations/langgraph.mdx +3 -3
  167. package/docs-site/zh/examples/integrations/pi-sdk.mdx +2 -2
  168. package/docs-site/zh/explanation/adapter.mdx +19 -19
  169. package/docs-site/zh/explanation/assert.mdx +9 -9
  170. package/docs-site/zh/explanation/drive.mdx +4 -4
  171. package/docs-site/zh/explanation/evals.mdx +8 -8
  172. package/docs-site/zh/explanation/experiment.mdx +8 -8
  173. package/docs-site/zh/explanation/hitl.mdx +13 -13
  174. package/docs-site/zh/explanation/judge.mdx +3 -3
  175. package/docs-site/zh/explanation/overview.mdx +7 -7
  176. package/docs-site/zh/explanation/runner.mdx +17 -13
  177. package/docs-site/zh/explanation/tier.mdx +6 -6
  178. package/docs-site/zh/index.mdx +14 -14
  179. package/docs-site/zh/introduction.mdx +13 -16
  180. package/docs-site/zh/reference/builtin-agents.mdx +61 -28
  181. package/docs-site/zh/reference/capabilities.mdx +7 -27
  182. package/docs-site/zh/reference/cli.mdx +36 -33
  183. package/docs-site/zh/reference/define-agent.mdx +32 -31
  184. package/docs-site/zh/reference/define-config.mdx +6 -6
  185. package/docs-site/zh/reference/define-eval.mdx +33 -20
  186. package/docs-site/zh/reference/events.mdx +4 -4
  187. package/docs-site/zh/reference/expect.mdx +3 -3
  188. package/docs-site/zh/reference/official-adapters.mdx +17 -17
  189. package/docs-site/zh/reference/report-components.mdx +195 -121
  190. package/docs-site/zh/reference/results-data.mdx +13 -13
  191. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +11 -11
  192. package/docs-site/zh/troubleshooting/debugging.mdx +40 -16
  193. package/docs-site/zh/{how-to → tutorials}/agent-feedback-loop.mdx +38 -38
  194. package/docs-site/zh/tutorials/agent-onboarding.mdx +100 -0
  195. package/docs-site/zh/tutorials/authoring.mdx +211 -0
  196. package/docs-site/zh/{how-to → tutorials}/ci-integration.mdx +8 -8
  197. package/docs-site/zh/{how-to → tutorials}/connect-otel.mdx +22 -22
  198. package/docs-site/zh/{how-to → tutorials}/connect-your-agent.mdx +58 -63
  199. package/docs-site/zh/tutorials/custom-reports.mdx +453 -0
  200. package/docs-site/zh/{how-to → tutorials}/dataset-fanout.mdx +4 -4
  201. package/docs-site/zh/tutorials/experiments.mdx +102 -0
  202. package/docs-site/zh/{how-to → tutorials}/fixtures.mdx +9 -9
  203. package/docs-site/zh/{how-to → tutorials}/publish-report.mdx +6 -6
  204. package/docs-site/zh/tutorials/quickstart.mdx +18 -18
  205. package/docs-site/zh/{how-to → tutorials}/reporters.mdx +6 -6
  206. package/docs-site/zh/{how-to → tutorials}/sandbox-agent.mdx +6 -6
  207. package/docs-site/zh/{how-to → tutorials}/sandbox-providers.mdx +41 -19
  208. package/docs-site/zh/{how-to → tutorials}/scoring-guide.mdx +4 -4
  209. package/docs-site/zh/{how-to → tutorials}/viewing-results.mdx +60 -57
  210. package/docs-site/zh/tutorials/write-experiment.mdx +353 -0
  211. package/docs-site/zh/{how-to → tutorials}/write-send.mdx +40 -40
  212. package/package.json +10 -7
  213. package/src/agents/bub.ts +21 -8
  214. package/src/agents/claude-code.ts +23 -10
  215. package/src/agents/codex.test.ts +22 -7
  216. package/src/agents/codex.ts +22 -9
  217. package/src/agents/index.ts +2 -2
  218. package/src/agents/openai-compat.ts +1 -1
  219. package/src/agents/post-setup.ts +45 -22
  220. package/src/agents/streaming.ts +1 -1
  221. package/src/agents/types.ts +20 -17
  222. package/src/agents/ui-message-stream.test.ts +10 -0
  223. package/src/agents/ui-message-stream.ts +12 -1
  224. package/src/cli.ts +75 -59
  225. package/src/context/types.ts +21 -21
  226. package/src/define.ts +13 -0
  227. package/src/i18n/en.ts +27 -12
  228. package/src/i18n/zh-CN.ts +27 -12
  229. package/src/index.ts +2 -0
  230. package/src/o11y/prices.json +176 -99
  231. package/src/report/{react → assets}/colors.ts +22 -0
  232. package/src/report/{react → assets}/enhance.js +1 -33
  233. package/src/report/assets/styles.css +1785 -0
  234. package/src/report/built-in/index.tsx +2 -2
  235. package/src/report/built-in/standard.tsx +18 -6
  236. package/src/report/components/attempt-detail/AttemptAssertions.tsx +67 -0
  237. package/src/report/components/attempt-detail/AttemptConversation.tsx +158 -0
  238. package/src/report/components/attempt-detail/AttemptDiagnostics.tsx +38 -0
  239. package/src/report/components/attempt-detail/AttemptDiff.tsx +35 -0
  240. package/src/report/components/attempt-detail/AttemptError.tsx +31 -0
  241. package/src/report/components/attempt-detail/AttemptFixPrompt.tsx +26 -0
  242. package/src/report/components/attempt-detail/AttemptSource.tsx +224 -0
  243. package/src/report/components/attempt-detail/AttemptSummary.tsx +74 -0
  244. package/src/report/components/attempt-detail/AttemptTimeline.tsx +108 -0
  245. package/src/report/components/attempt-detail/AttemptTrace.tsx +63 -0
  246. package/src/report/components/attempt-detail/AttemptUsage.tsx +29 -0
  247. package/src/report/components/attempt-detail/attempt-components.test.tsx +758 -0
  248. package/src/report/components/attempt-detail/compute.ts +336 -0
  249. package/src/report/components/attempt-detail/faces.ts +260 -0
  250. package/src/report/components/attempt-detail/index.tsx +600 -0
  251. package/src/report/components/attempt-detail/validate.test.ts +252 -0
  252. package/src/report/{react → components}/cell.tsx +6 -3
  253. package/src/report/{report.test.ts → components/compute.test.ts} +316 -46
  254. package/src/report/{react → components/entity-lists}/AttemptList.tsx +24 -16
  255. package/src/report/{react → components/entity-lists}/EvalList.tsx +0 -0
  256. package/src/report/{react → components/entity-lists}/ExperimentList.tsx +33 -27
  257. package/src/report/components/entity-lists/compute.ts +196 -0
  258. package/src/report/components/entity-lists/faces.ts +208 -0
  259. package/src/report/components/entity-lists/index.tsx +238 -0
  260. package/src/report/components/entity-lists/validate.test.ts +114 -0
  261. package/src/report/{react → components}/fixtures.ts +2 -2
  262. package/src/report/{react → components/metric-views}/DeltaTable.tsx +5 -5
  263. package/src/report/{react → components/metric-views}/MetricBars.tsx +5 -5
  264. package/src/report/{react → components/metric-views}/MetricLine.tsx +9 -6
  265. package/src/report/{react → components/metric-views}/MetricMatrix.tsx +6 -6
  266. package/src/report/{react → components/metric-views}/MetricScatter.tsx +42 -70
  267. package/src/report/{react → components/metric-views}/MetricTable.tsx +6 -6
  268. package/src/report/{react → components/metric-views}/Scoreboard.tsx +4 -4
  269. package/src/report/{compute.ts → components/metric-views/compute.ts} +51 -477
  270. package/src/report/components/metric-views/faces.ts +423 -0
  271. package/src/report/components/metric-views/index.tsx +352 -0
  272. package/src/report/{text → components/metric-views}/plot.ts +1 -1
  273. package/src/report/components/metric-views/validate.test.ts +211 -0
  274. package/src/report/{react → components}/render.test.tsx +66 -26
  275. package/src/report/components/shared-compute.ts +59 -0
  276. package/src/report/components/shared-faces.ts +30 -0
  277. package/src/report/components/shared.ts +192 -0
  278. package/src/report/{react → components/site-components}/CopyFixPrompt.tsx +3 -3
  279. package/src/report/{react → components/site-components}/HeroCard.tsx +3 -3
  280. package/src/report/{react → components/site-components}/ScopeWarnings.tsx +4 -4
  281. package/src/report/{react → components/site-components}/TraceWaterfall.tsx +13 -10
  282. package/src/report/components/site-components/compute.ts +144 -0
  283. package/src/report/components/site-components/faces.ts +80 -0
  284. package/src/report/components/site-components/index.tsx +265 -0
  285. package/src/report/{scope-warnings.ts → components/site-components/scope-warnings.ts} +3 -3
  286. package/src/report/{site-components.test.tsx → components/site-components/site-components.test.tsx} +23 -21
  287. package/src/report/components/site-components/validate.test.ts +89 -0
  288. package/src/report/{react → components/summaries}/ScopeSummary.tsx +26 -11
  289. package/src/report/components/summaries/compute.ts +56 -0
  290. package/src/report/components/summaries/faces.ts +48 -0
  291. package/src/report/components/summaries/index.tsx +127 -0
  292. package/src/report/components/summaries/validate.test.ts +49 -0
  293. package/src/report/definition/grid-layout.test.ts +124 -0
  294. package/src/report/definition/grid-layout.ts +146 -0
  295. package/src/report/{primitives.tsx → definition/primitives.tsx} +168 -18
  296. package/src/report/{report.ts → definition/report.ts} +98 -165
  297. package/src/report/{shell-head.test.ts → definition/shell-head.test.ts} +2 -2
  298. package/src/report/{text/table.ts → definition/table-text.ts} +4 -4
  299. package/src/report/{tree.ts → definition/tree.ts} +56 -20
  300. package/src/report/index.ts +128 -66
  301. package/src/report/{aggregate.ts → model/aggregate.ts} +31 -9
  302. package/src/report/{flag.ts → model/flag.ts} +39 -1
  303. package/src/report/{format.ts → model/format.ts} +89 -10
  304. package/src/report/{locale.ts → model/locale.ts} +34 -36
  305. package/src/report/{metrics.ts → model/metrics.ts} +3 -3
  306. package/src/report/{types.ts → model/types.ts} +159 -22
  307. package/src/report/react/index.tsx +54 -26
  308. package/src/report/{dual-render.test.tsx → runtime/dual-render.test.tsx} +601 -53
  309. package/src/report/runtime/host.test.ts +44 -0
  310. package/src/report/runtime/host.ts +138 -0
  311. package/src/report/{load.ts → runtime/load.ts} +1 -1
  312. package/src/report/runtime/text.ts +192 -0
  313. package/src/report/runtime/web.ts +106 -0
  314. package/src/results/attempt-evidence.ts +5 -1
  315. package/src/results/host-equivalence.test.ts +53 -2
  316. package/src/results/select.ts +29 -5
  317. package/src/runner/attempt.test.ts +48 -1
  318. package/src/runner/attempt.ts +62 -42
  319. package/src/runner/cleanup-timeout.test.ts +16 -0
  320. package/src/runner/cleanup-timeout.ts +23 -0
  321. package/src/runner/discover.ts +1 -2
  322. package/src/runner/eval-selection.test.ts +187 -0
  323. package/src/runner/eval-selection.ts +109 -0
  324. package/src/runner/experiment-cleanup-registry.test.ts +89 -0
  325. package/src/runner/experiment-cleanup-registry.ts +39 -0
  326. package/src/runner/experiment-labels.test.ts +71 -0
  327. package/src/runner/feedback/agent.test.ts +33 -1
  328. package/src/runner/feedback/agent.ts +12 -16
  329. package/src/runner/feedback/ci.test.ts +35 -1
  330. package/src/runner/feedback/ci.ts +16 -18
  331. package/src/runner/feedback/coordinator.ts +28 -0
  332. package/src/runner/feedback/human.test.ts +108 -1
  333. package/src/runner/feedback/human.ts +71 -21
  334. package/src/runner/feedback/reducer.test.ts +64 -0
  335. package/src/runner/feedback/reducer.ts +26 -0
  336. package/src/runner/feedback/sink.ts +40 -1
  337. package/src/runner/fingerprint.ts +2 -1
  338. package/src/runner/report.test.ts +14 -0
  339. package/src/runner/report.ts +3 -1
  340. package/src/runner/run.test.ts +390 -12
  341. package/src/runner/run.ts +259 -26
  342. package/src/runner/sandbox-selection.test.ts +13 -3
  343. package/src/runner/sandbox-selection.ts +4 -2
  344. package/src/runner/types.ts +160 -24
  345. package/src/sandbox/docker.ts +7 -0
  346. package/src/sandbox/e2b-reconcile.test.ts +125 -0
  347. package/src/sandbox/e2b.ts +47 -12
  348. package/src/sandbox/types.ts +23 -21
  349. package/src/scoring/display.test.ts +28 -2
  350. package/src/scoring/display.ts +38 -5
  351. package/src/scoring/types.ts +3 -3
  352. package/src/shared/aggregate.test.ts +52 -0
  353. package/src/shared/aggregate.ts +29 -0
  354. package/src/shared/types.ts +6 -1
  355. package/src/show/command.test.ts +34 -0
  356. package/src/show/command.ts +16 -0
  357. package/src/show/compose.ts +1 -1
  358. package/src/show/index.ts +56 -20
  359. package/src/show/render.ts +15 -239
  360. package/src/show/show.test.ts +127 -34
  361. package/src/view/app/App.test.tsx +1 -1
  362. package/src/view/app/App.tsx +119 -47
  363. package/src/view/app/i18n.ts +7 -147
  364. package/src/view/app/lib/attempt-dialog.test.ts +65 -0
  365. package/src/view/app/lib/attempt-dialog.ts +69 -0
  366. package/src/view/app/main.tsx +0 -1
  367. package/src/view/app/types.ts +3 -86
  368. package/src/view/artifact-serving.test.ts +22 -38
  369. package/src/view/client-dist/app.css +1 -1
  370. package/src/view/client-dist/app.js +15 -22
  371. package/src/view/data.test.ts +48 -36
  372. package/src/view/data.ts +105 -83
  373. package/src/view/index.ts +1 -1
  374. package/src/view/server.ts +32 -2
  375. package/src/view/shared/types.ts +6 -31
  376. package/src/view/site-parity.test.ts +24 -1
  377. package/src/view/site.ts +144 -17
  378. package/src/view/styles.css +0 -805
  379. package/src/view/view-report.test.ts +143 -22
  380. package/dist/report/components.d.ts +0 -179
  381. package/dist/report/components.js +0 -544
  382. package/dist/report/compute.d.ts +0 -139
  383. package/dist/report/react/ExperimentComparison.d.ts +0 -10
  384. package/dist/report/react/ExperimentComparison.js +0 -12
  385. package/dist/report/react/MetricScatter.d.ts +0 -9
  386. package/dist/report/react/format.d.ts +0 -3
  387. package/dist/report/react/format.js +0 -7
  388. package/dist/report/text/faces.d.ts +0 -45
  389. package/dist/report/text/faces.js +0 -671
  390. package/dist/report/web.d.ts +0 -32
  391. package/dist/report/web.js +0 -48
  392. package/docs-site/zh/how-to/authoring.mdx +0 -162
  393. package/docs-site/zh/how-to/custom-reports.mdx +0 -414
  394. package/docs-site/zh/how-to/experiments.mdx +0 -86
  395. package/docs-site/zh/how-to/write-experiment.mdx +0 -164
  396. package/src/report/components.tsx +0 -925
  397. package/src/report/react/ExperimentComparison.tsx +0 -73
  398. package/src/report/react/format.ts +0 -9
  399. package/src/report/react/styles.css +0 -945
  400. package/src/report/text/faces.ts +0 -767
  401. package/src/report/web.ts +0 -77
  402. package/src/show/report-host.test.ts +0 -205
  403. package/src/show/report-host.ts +0 -389
  404. package/src/view/app/components/AttemptModal.tsx +0 -496
  405. package/src/view/app/components/CodeView.test.tsx +0 -142
  406. package/src/view/app/components/CodeView.tsx +0 -310
  407. package/src/view/app/components/CopyControls.tsx +0 -106
  408. package/src/view/app/components/Trace.tsx +0 -100
  409. package/src/view/app/components/Transcript.tsx +0 -157
  410. package/src/view/app/components/ui/badge.tsx +0 -21
  411. package/src/view/app/lib/artifact-url.ts +0 -17
  412. package/src/view/app/lib/attempt-route.test.ts +0 -80
  413. package/src/view/app/lib/attempt-route.ts +0 -52
  414. package/src/view/app/lib/format.ts +0 -70
  415. package/src/view/app/lib/guards.test.ts +0 -108
  416. package/src/view/app/lib/guards.ts +0 -71
  417. package/src/view/app/lib/rows.ts +0 -22
  418. package/src/view/app/lib/transcript-data.tsx +0 -151
  419. package/src/view/app/lib/verdict.ts +0 -23
  420. package/src/view/app/shared.ts +0 -8
  421. /package/dist/report/{react → components/metric-views}/chart-math.d.ts +0 -0
  422. /package/dist/report/{react → components/metric-views}/chart-math.js +0 -0
  423. /package/dist/report/{text → components/metric-views}/plot.d.ts +0 -0
  424. /package/dist/report/{react → components/site-components}/PoweredBy.d.ts +0 -0
  425. /package/dist/report/{react → components/site-components}/PoweredBy.js +0 -0
  426. /package/dist/report/{text/layout.d.ts → model/text-layout.d.ts} +0 -0
  427. /package/dist/report/{text/layout.js → model/text-layout.js} +0 -0
  428. /package/dist/report/{types.js → model/types.js} +0 -0
  429. /package/src/report/{react → components/metric-views}/chart-math.test.ts +0 -0
  430. /package/src/report/{react → components/metric-views}/chart-math.ts +0 -0
  431. /package/src/report/{react → components/site-components}/PoweredBy.tsx +0 -0
  432. /package/src/report/{text/layout.ts → model/text-layout.ts} +0 -0
@@ -0,0 +1,758 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
2
+ // Attempt 详情组件族的单元测试:11 个叶子的非空/空证据矩阵、AttemptAssessment 的
3
+ // source/assertions fallback、AttemptDetail 的内建顺序、spec/data 等价与 scope-input page
4
+ // 报错、AttemptConversation 的 loc 分轮、attemptSourceData 的 loc 投影、AttemptTimeline 的默认折叠。
5
+ // 纯渲染,注入数据:直接构造 AttemptEvidence fixture,不 mock fetch(这些组件从不 fetch)。
6
+ // 样式与视觉交互(染色、布局、展开观感)不在本层:归 E2E 报告域(docs/engineering/e2e-ci/results.md)。
7
+
8
+ import { renderToStaticMarkup } from "react-dom/server";
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import type { AssertionResult, EvalResult, StreamEvent, Verdict } from "../../../types.ts";
12
+ import type { Results, Scope } from "../../../results/index.ts";
13
+ import { makeScope } from "../../../results/select.ts";
14
+ import type { AttemptEvidence, AttemptEvidenceCapabilities } from "../../../results/attempt-evidence.ts";
15
+ import { encodeAttemptLocator, type AttemptIdentity } from "../../../results/locator.ts";
16
+ import { composeOf, createTextContext, renderNodeToText, resolveReportTree, ResolveMemo, type ReportNode } from "../../definition/tree.ts";
17
+ import { buildReportMeta, defineReport } from "../../definition/report.ts";
18
+ import {
19
+ attemptAssertionsData,
20
+ attemptConversationData,
21
+ attemptDiagnosticsData,
22
+ attemptDiffData,
23
+ attemptErrorData,
24
+ attemptFixPromptData,
25
+ attemptSourceData,
26
+ attemptSummaryData,
27
+ attemptTimelineData,
28
+ attemptTraceData,
29
+ attemptUsageData,
30
+ } from "./compute.ts";
31
+ import {
32
+ AttemptAssertions,
33
+ AttemptAssessment,
34
+ AttemptConversation,
35
+ AttemptDetail,
36
+ AttemptDiagnostics,
37
+ AttemptDiff,
38
+ AttemptError,
39
+ AttemptFixPrompt,
40
+ AttemptSource,
41
+ AttemptSummary,
42
+ AttemptTimeline,
43
+ AttemptTrace,
44
+ AttemptUsage,
45
+ validateAssertionsData,
46
+ validateConversationData,
47
+ validateDiagnosticsData,
48
+ validateDiffData,
49
+ validateErrorData,
50
+ validateFixPromptData,
51
+ validateSourceData,
52
+ validateSummaryData,
53
+ validateTimelineData,
54
+ validateTraceData,
55
+ validateUsageData,
56
+ } from "./index.tsx";
57
+
58
+ // ───────────────────────── fixture ─────────────────────────
59
+
60
+ function identityOf(overrides: Partial<AttemptIdentity> = {}): AttemptIdentity {
61
+ return { experimentId: "exp/a", snapshotStartedAt: "2026-07-01T00:00:00.000Z", evalId: "eval/one", attempt: 0, ...overrides };
62
+ }
63
+
64
+ function resultOf(overrides: Partial<EvalResult> = {}): EvalResult {
65
+ return {
66
+ id: "eval/one",
67
+ agent: "agent-x",
68
+ verdict: "passed" as Verdict,
69
+ attempt: 0,
70
+ durationMs: 1000,
71
+ assertions: [],
72
+ ...overrides,
73
+ };
74
+ }
75
+
76
+ const FULL_CAPS: AttemptEvidenceCapabilities = { source: true, execution: true, timing: true, diff: true };
77
+ const NO_CAPS: AttemptEvidenceCapabilities = { source: false, execution: false, timing: false, diff: false };
78
+
79
+ function evidenceOf(overrides: Partial<AttemptEvidence> = {}): AttemptEvidence {
80
+ const identity = overrides.identity ?? identityOf();
81
+ return {
82
+ locator: overrides.locator ?? encodeAttemptLocator(identity),
83
+ identity,
84
+ result: overrides.result ?? resultOf(),
85
+ events: overrides.events ?? null,
86
+ evalSource: overrides.evalSource ?? null,
87
+ execution: overrides.execution ?? null,
88
+ diff: overrides.diff ?? null,
89
+ trace: overrides.trace ?? null,
90
+ artifactPaths: overrides.artifactPaths ?? { dir: "/results/exp/a/eval-one/a0" },
91
+ capabilities: overrides.capabilities ?? NO_CAPS,
92
+ };
93
+ }
94
+
95
+ function scopeAndResults(): { scope: Scope; results: Results } {
96
+ const scope = makeScope("current-evals", [], []);
97
+ const results = { experiments: [], skipped: [], latest: () => scope, current: () => scope } as unknown as Results;
98
+ return { scope, results };
99
+ }
100
+
101
+ /** resolve 单个 attempt-input page 节点,注入给定的 evidence。 */
102
+ async function resolveOnAttemptPage(node: ReportNode, evidence: AttemptEvidence): Promise<unknown> {
103
+ const { scope, results } = scopeAndResults();
104
+ const page = { id: "attempt", input: "attempt" as const, locator: evidence.locator, evidence };
105
+ return resolveReportTree(node, {
106
+ scope,
107
+ results,
108
+ report: buildReportMeta(defineReport(node), scope),
109
+ page,
110
+ memo: new ResolveMemo(),
111
+ });
112
+ }
113
+
114
+ /** resolve 一份放在 scope-input page 上的节点(默认 report 页,没有 attempt evidence)。 */
115
+ async function resolveOnScopePage(node: ReportNode): Promise<unknown> {
116
+ const { scope, results } = scopeAndResults();
117
+ return resolveReportTree(node, {
118
+ scope,
119
+ results,
120
+ report: buildReportMeta(defineReport(node), scope),
121
+ page: { id: "report", input: "scope" },
122
+ memo: new ResolveMemo(),
123
+ });
124
+ }
125
+
126
+ // ───────────────────────── 11 个叶子的非空/空证据矩阵 ─────────────────────────
127
+
128
+ describe("Attempt 详情组件族:非空/空证据矩阵", () => {
129
+ it("AttemptSummary 恒非空", () => {
130
+ const evidence = evidenceOf({ capabilities: FULL_CAPS });
131
+ const data = attemptSummaryData(evidence);
132
+ expect(data.locator).toBe(evidence.locator);
133
+ expect(data.verdict).toBe("passed");
134
+ expect(validateSummaryData(data)).toBeNull();
135
+ });
136
+
137
+ it("AttemptSummary 的 startedAt 与 identity.attempt 两面都可见,startedAt 缺失时两面都不产生该字段", () => {
138
+ const withBoth = evidenceOf({
139
+ identity: identityOf({ attempt: 2 }),
140
+ result: resultOf({ attempt: 2, startedAt: "2026-01-01T12:34:00.000Z" }),
141
+ });
142
+ const dataWithBoth = attemptSummaryData(withBoth);
143
+ const htmlWithBoth = renderToStaticMarkup(<AttemptSummary data={dataWithBoth} /> as never);
144
+ const textWithBoth = renderNodeToText(<AttemptSummary data={dataWithBoth} /> as never, createTextContext({ width: 100 }));
145
+ expect(htmlWithBoth).toContain("2026");
146
+ expect(textWithBoth).toContain("2026");
147
+ expect(htmlWithBoth).toContain("3"); // identity.attempt = 2,显示前 +1
148
+ expect(textWithBoth).toContain("3");
149
+
150
+ const withoutStartedAt = evidenceOf({ result: resultOf({ startedAt: undefined }) });
151
+ const dataWithoutStartedAt = attemptSummaryData(withoutStartedAt);
152
+ expect(dataWithoutStartedAt.startedAt).toBeUndefined();
153
+ const htmlWithoutStartedAt = renderToStaticMarkup(<AttemptSummary data={dataWithoutStartedAt} /> as never);
154
+ const textWithoutStartedAt = renderNodeToText(
155
+ <AttemptSummary data={dataWithoutStartedAt} /> as never,
156
+ createTextContext({ width: 100 }),
157
+ );
158
+ expect(htmlWithoutStartedAt).not.toContain("Started");
159
+ expect(textWithoutStartedAt).not.toContain("undefined");
160
+ });
161
+
162
+ it("AttemptError:没有 error 时 null,有 error 时结构化字段齐全", () => {
163
+ expect(attemptErrorData(evidenceOf())).toBeNull();
164
+ const withError = evidenceOf({
165
+ result: resultOf({ verdict: "errored", error: { code: "timeout", message: "boom", phase: "eval.run" } }),
166
+ });
167
+ expect(attemptErrorData(withError)).toEqual({ code: "timeout", message: "boom", phase: "eval.run" });
168
+ expect(validateErrorData(attemptErrorData(withError))).toBeNull();
169
+ });
170
+
171
+ it("AttemptAssertions:没有 assertion 时 null,有时按 attention/passedGroups 分桶", () => {
172
+ expect(attemptAssertionsData(evidenceOf())).toBeNull();
173
+ const assertions: AssertionResult[] = [
174
+ { name: "a", severity: "gate", outcome: "failed", score: 0 },
175
+ { name: "b", severity: "gate", outcome: "passed", score: 1, groupPath: ["g1"] },
176
+ { name: "c", severity: "gate", outcome: "passed", score: 1, groupPath: ["g1"] },
177
+ ];
178
+ const data = attemptAssertionsData(evidenceOf({ result: resultOf({ verdict: "failed", assertions }) }))!;
179
+ expect(data.attention.map((a) => a.name)).toEqual(["a"]);
180
+ expect(data.passedGroups).toEqual([{ group: "g1", items: [assertions[1], assertions[2]] }]);
181
+ expect(validateAssertionsData(data)).toBeNull();
182
+ });
183
+
184
+ it("AttemptSource:没有 source 时 null(evalSource null 或 capability 假)", () => {
185
+ expect(attemptSourceData(evidenceOf())).toBeNull();
186
+ const withSource = evidenceOf({
187
+ capabilities: { ...NO_CAPS, source: true },
188
+ evalSource: {
189
+ sourcePath: "evals/a.ts",
190
+ sourceSha256: "x",
191
+ lines: [{ line: 1, text: "t.expect(1).toBe(1)", assertions: [], sends: [] }],
192
+ unmapped: [],
193
+ summary: {
194
+ totalAssertions: 0,
195
+ mappedAssertions: 0,
196
+ unmappedAssertions: 0,
197
+ passed: 0,
198
+ failed: 0,
199
+ gate: 0,
200
+ soft: 0,
201
+ totalLines: 1,
202
+ annotatedLines: 0,
203
+ },
204
+ },
205
+ });
206
+ expect(attemptSourceData(withSource)?.sourcePath).toBe("evals/a.ts");
207
+ expect(validateSourceData(attemptSourceData(withSource))).toBeNull();
208
+ });
209
+
210
+ it("AttemptFixPrompt:passed 时 null,failed 且有可归因原因时给出可复制 prompt", () => {
211
+ expect(attemptFixPromptData(evidenceOf())).toBeNull();
212
+ const failed = evidenceOf({
213
+ result: resultOf({
214
+ verdict: "failed",
215
+ assertions: [{ name: "check", severity: "gate", outcome: "failed", score: 0, detail: "expected true" }],
216
+ }),
217
+ });
218
+ const data = attemptFixPromptData(failed);
219
+ expect(data?.prompt).toContain("exp/a");
220
+ expect(data?.prompt).toContain(`niceeval show ${failed.locator}`);
221
+ expect(validateFixPromptData(data)).toBeNull();
222
+ });
223
+
224
+ it("AttemptTimeline:没有 phase 时 null", () => {
225
+ expect(attemptTimelineData(evidenceOf())).toBeNull();
226
+ const withPhases = evidenceOf({
227
+ result: resultOf({ phases: [{ name: "eval.run", durationMs: 10 }] }),
228
+ trace: [{ traceId: "t1", spanId: "s1", name: "model-call", startMs: 0, endMs: 100 }],
229
+ });
230
+ const data = attemptTimelineData(withPhases);
231
+ expect(data?.phases).toHaveLength(1);
232
+ expect(validateTimelineData(data)).toBeNull();
233
+ });
234
+
235
+ it("AttemptConversation:没有 events 时 null", () => {
236
+ expect(attemptConversationData(evidenceOf())).toBeNull();
237
+ expect(attemptConversationData(evidenceOf({ events: [] }))).toBeNull();
238
+ });
239
+
240
+ it("AttemptDiagnostics:没有 diagnostics 时 null", () => {
241
+ expect(attemptDiagnosticsData(evidenceOf())).toBeNull();
242
+ const withDiag = evidenceOf({
243
+ result: resultOf({ diagnostics: [{ code: "cleanup-failed", level: "warning", message: "m", phase: "eval.teardown" }] }),
244
+ });
245
+ const data = attemptDiagnosticsData(withDiag);
246
+ expect(data?.groups).toEqual([
247
+ { phase: "eval.teardown", items: [{ code: "cleanup-failed", level: "warning", message: "m", phase: "eval.teardown" }] },
248
+ ]);
249
+ expect(validateDiagnosticsData(data)).toBeNull();
250
+ });
251
+
252
+ it("AttemptUsage:没有 usage 时 null", () => {
253
+ expect(attemptUsageData(evidenceOf())).toBeNull();
254
+ const withUsage = evidenceOf({ result: resultOf({ usage: { inputTokens: 10, outputTokens: 5 } }) });
255
+ const data = attemptUsageData(withUsage);
256
+ expect(data?.usage.inputTokens).toBe(10);
257
+ expect(validateUsageData(data)).toBeNull();
258
+ });
259
+
260
+ it("AttemptTrace:没有 trace 时 null", () => {
261
+ expect(attemptTraceData(evidenceOf())).toBeNull();
262
+ const withTrace = evidenceOf({ trace: [{ traceId: "t1", spanId: "s1", name: "model-call", startMs: 0, endMs: 100 }] });
263
+ const data = attemptTraceData(withTrace);
264
+ expect(data?.spans).toHaveLength(1);
265
+ expect(validateTraceData(data)).toBeNull();
266
+ });
267
+
268
+ it("AttemptDiff:没有变更时 null,net:none 的触碰不进列表", () => {
269
+ expect(attemptDiffData(evidenceOf())).toBeNull();
270
+ const diff = {
271
+ windows: [
272
+ {
273
+ window: "s1/t1",
274
+ changes: {
275
+ "a.ts": { status: "modified" as const, before: "1\n2", after: "1\n3" },
276
+ "b.ts": { status: "modified" as const, before: "x", after: "x" },
277
+ },
278
+ },
279
+ ],
280
+ files: {
281
+ "a.ts": { net: "modified" as const, windows: ["s1/t1"] },
282
+ "b.ts": { net: "none" as const, windows: ["s1/t1"] },
283
+ },
284
+ get: (path: string) => (path === "a.ts" ? "1\n3" : "x"),
285
+ };
286
+ const withDiff = evidenceOf({ capabilities: { ...NO_CAPS, diff: true }, diff });
287
+ const data = attemptDiffData(withDiff);
288
+ expect(data?.files.map((f) => f.path)).toEqual(["a.ts"]);
289
+ expect(data?.files[0]!.lines).toEqual({ added: 1, deleted: 1 });
290
+ expect(validateDiffData(data)).toBeNull();
291
+ });
292
+ });
293
+
294
+ // ───────────────────────── AttemptAssessment / AttemptDetail ─────────────────────────
295
+
296
+ describe("AttemptAssessment / AttemptDetail(组合组件)", () => {
297
+ it("有 source 时展开树含 AttemptSource 不含 AttemptAssertions;无 source 时相反", async () => {
298
+ for (const [evidence, expectSource] of [
299
+ [evidenceOf({ capabilities: { ...NO_CAPS, source: true } }), true],
300
+ [evidenceOf({ capabilities: NO_CAPS }), false],
301
+ ] as const) {
302
+ const resolved = (await resolveOnAttemptPage(<AttemptAssessment />, evidence)) as { props: { children: Array<{ type: unknown }> } };
303
+ const types = resolved.props.children.map((c) => c.type);
304
+ expect(types).toContain(AttemptError);
305
+ expect(types.includes(AttemptSource)).toBe(expectSource);
306
+ expect(types.includes(AttemptAssertions)).toBe(!expectSource);
307
+ }
308
+ });
309
+
310
+ it("在 scope-input page 之外调用时 resolve 报完整用户反馈", async () => {
311
+ await expect(resolveOnScopePage(<AttemptAssessment />)).rejects.toThrow(/attempt-input page/);
312
+ });
313
+
314
+ it("AttemptDetail:有 source 时不重复 Conversation,无 source 时在 usage 后保留 fallback", () => {
315
+ // AttemptDetail 自己是组合组件:resolve 会把它(以及嵌套的 AttemptAssessment)递归展开,
316
+ // 所以这里直接检查它的 compose 函数产出的原始树(与「内建报告」测试检查 standard.tsx
317
+ // 原始声明同一手法),不走完整 resolve——那样 AttemptAssessment 会被替换成它自己展开出的
318
+ // <Col> 而不再是 AttemptAssessment 这个类型。
319
+ const compose = composeOf(AttemptDetail)!;
320
+ const childTypes = (evidence: AttemptEvidence): unknown[] => {
321
+ const tree = compose({}, { page: { input: "attempt", evidence } } as never) as unknown as {
322
+ props: { children: Array<{ type: unknown } | null> };
323
+ };
324
+ return tree.props.children.filter((child): child is { type: unknown } => child !== null).map((child) => child.type);
325
+ };
326
+ const withoutSource = childTypes(evidenceOf());
327
+ expect(withoutSource).toEqual([
328
+ AttemptSummary,
329
+ AttemptAssessment,
330
+ AttemptFixPrompt,
331
+ AttemptTimeline,
332
+ AttemptDiagnostics,
333
+ AttemptUsage,
334
+ AttemptConversation,
335
+ AttemptTrace,
336
+ AttemptDiff,
337
+ ]);
338
+
339
+ const withSource = childTypes(
340
+ evidenceOf({
341
+ capabilities: { ...NO_CAPS, source: true },
342
+ evalSource: {
343
+ sourcePath: "evals/a.ts",
344
+ sourceSha256: "x",
345
+ lines: [{ line: 1, text: "", assertions: [], sends: [] }],
346
+ unmapped: [],
347
+ summary: {
348
+ totalAssertions: 0,
349
+ mappedAssertions: 0,
350
+ unmappedAssertions: 0,
351
+ passed: 0,
352
+ failed: 0,
353
+ gate: 0,
354
+ soft: 0,
355
+ totalLines: 1,
356
+ annotatedLines: 0,
357
+ },
358
+ },
359
+ }),
360
+ );
361
+ expect(withSource).toEqual(withoutSource.filter((type) => type !== AttemptConversation));
362
+ });
363
+ });
364
+
365
+ // ───────────────────────── spec/data 等价与 scope-input page 报错 ─────────────────────────
366
+
367
+ describe("叶子组件的 spec/data 形态", () => {
368
+ it("<AttemptSummary /> 在 attempt page 内的 spec 结果与手工 attemptSummaryData(evidence) 深等", async () => {
369
+ const evidence = evidenceOf({ capabilities: FULL_CAPS });
370
+ const resolved = (await resolveOnAttemptPage(<AttemptSummary />, evidence)) as { props: { data: unknown } };
371
+ expect(resolved.props.data).toEqual(attemptSummaryData(evidence));
372
+ });
373
+
374
+ it("<AttemptSummary /> 放进 scope-input page 报错,文案含移到 attempt-input page 或传入 evidence", async () => {
375
+ await expect(resolveOnScopePage(<AttemptSummary />)).rejects.toThrow(/attempt-input page/);
376
+ });
377
+
378
+ it("显式传 data 时不再取当前 page 的 evidence(scope-input page 上也能直接渲染)", async () => {
379
+ const data = attemptSummaryData(evidenceOf());
380
+ const resolved = (await resolveOnScopePage(<AttemptSummary data={data} />)) as { props: { data: unknown } };
381
+ expect(resolved.props.data).toEqual(data);
382
+ });
383
+
384
+ it("同时传 data 与 input 报完整用户反馈,不静默取一边", async () => {
385
+ const evidence = evidenceOf();
386
+ const data = attemptSummaryData(evidence);
387
+ await expect(
388
+ resolveOnScopePage(
389
+ // @ts-expect-error data 与 input 字段互斥,类型层已拒绝;这里模拟无类型 JS 输入
390
+ <AttemptSummary data={data} input={evidence} />,
391
+ ),
392
+ ).rejects.toThrow(/both `data` and `input`/);
393
+ });
394
+ });
395
+
396
+ // ───────────────────────── text/web 共享同一份 data ─────────────────────────
397
+
398
+ describe("text/web 共享同一份 data,不逐字比较", () => {
399
+ it("AttemptSummary 两面都显示相同 verdict 与 locator", () => {
400
+ const evidence = evidenceOf({ result: resultOf({ verdict: "failed" }) });
401
+ const data = attemptSummaryData(evidence);
402
+ const html = renderToStaticMarkup(<AttemptSummary data={data} /> as never);
403
+ const text = renderNodeToText(<AttemptSummary data={data} /> as never, createTextContext({ width: 100 }));
404
+ expect(html).toContain(evidence.locator);
405
+ expect(text).toContain(evidence.locator);
406
+ expect(html).toContain("failed");
407
+ expect(text).toContain("failed");
408
+ });
409
+
410
+ it("失败断言的 expected/received 两面都可见(docs/feature/reports/library/attempt-detail.md「在 show 与 view 怎样渲染」表)", () => {
411
+ const assertions: AssertionResult[] = [
412
+ { name: "check", severity: "gate", outcome: "failed", score: 0, detail: "equals(4)", expected: "4", received: "3" },
413
+ ];
414
+ const data = attemptAssertionsData(evidenceOf({ result: resultOf({ verdict: "failed", assertions }) }))!;
415
+ const html = renderToStaticMarkup(<AttemptAssertions data={data} /> as never);
416
+ const text = renderNodeToText(<AttemptAssertions data={data} /> as never, createTextContext({ width: 100 }));
417
+ for (const face of [html, text]) {
418
+ expect(face).toContain("expected: 4");
419
+ expect(face).toContain("received: 3");
420
+ }
421
+ });
422
+ });
423
+
424
+ // ───────────────────────── AttemptConversation:loc 分轮 ─────────────────────────
425
+
426
+ describe("AttemptConversation:标准事件流按 loc 分轮", () => {
427
+ it("send(带 loc)后紧跟同文本无 loc 回显,回复仍全部聚到 send 行", () => {
428
+ const loc = { file: "evals/a.ts", line: 5 };
429
+ const events: StreamEvent[] = [
430
+ { type: "message", role: "user", text: "hello", loc },
431
+ { type: "message", role: "user", text: "hello" }, // 原生 transcript 回显,无 loc
432
+ { type: "message", role: "assistant", text: "hi there" },
433
+ ];
434
+ const data = attemptConversationData(evidenceOf({ events }))!;
435
+ expect(data.rounds).toHaveLength(1);
436
+ expect(data.rounds[0]!.loc).toEqual(loc);
437
+ expect(data.rounds[0]!.replies).toEqual([{ kind: "assistant", text: "hi there" }]);
438
+ expect(validateConversationData(data)).toBeNull();
439
+ });
440
+
441
+ it("混入完全未知的事件类型时该条目原始 JSON 保留,不吞没其余事件", () => {
442
+ const loc = { file: "evals/a.ts", line: 1 };
443
+ const events = [
444
+ { type: "message", role: "user", text: "go", loc },
445
+ { type: "future.thing", weird: true },
446
+ { type: "message", role: "assistant", text: "ok" },
447
+ ] as unknown as StreamEvent[];
448
+ const data = attemptConversationData(evidenceOf({ events }))!;
449
+ expect(data.rounds[0]!.replies.map((r) => r.kind)).toEqual(["raw", "assistant"]);
450
+ expect(data.rounds[0]!.replies[0]).toEqual({ kind: "raw", raw: { type: "future.thing", weird: true } });
451
+ expect(validateConversationData(data)).toBeNull();
452
+ });
453
+
454
+ it("skill.loaded 显示 Skill 名,不伪装成工具调用", () => {
455
+ const loc = { file: "evals/a.ts", line: 1 };
456
+ const events: StreamEvent[] = [
457
+ { type: "message", role: "user", text: "go", loc },
458
+ { type: "skill.loaded", skill: "pdf-tools" },
459
+ ];
460
+ const data = attemptConversationData(evidenceOf({ events }))!;
461
+ expect(data.rounds[0]!.replies).toEqual([{ kind: "skill", skill: "pdf-tools" }]);
462
+ expect(validateConversationData(data)).toBeNull();
463
+ });
464
+
465
+ it("流首无 loc 的 user 消息(旧 artifact)仍开 noloc 兜底轮", () => {
466
+ const events: StreamEvent[] = [{ type: "message", role: "assistant", text: "orphan reply" }];
467
+ const data = attemptConversationData(evidenceOf({ events }))!;
468
+ expect(data.rounds).toHaveLength(1);
469
+ expect(data.rounds[0]!.loc).toBeUndefined();
470
+ expect(data.rounds[0]!.replies).toEqual([{ kind: "assistant", text: "orphan reply" }]);
471
+ expect(validateConversationData(data)).toBeNull();
472
+ });
473
+
474
+ it("action.called + action.result 按 callId 合并成一条 tool 回复", () => {
475
+ const loc = { file: "evals/a.ts", line: 1 };
476
+ const events: StreamEvent[] = [
477
+ { type: "message", role: "user", text: "go", loc },
478
+ { type: "action.called", callId: "c1", name: "bash", input: { command: "ls" }, tool: "shell" },
479
+ { type: "action.result", callId: "c1", output: "file.txt", status: "completed" },
480
+ ];
481
+ const data = attemptConversationData(evidenceOf({ events }))!;
482
+ expect(data.rounds[0]!.replies).toEqual([
483
+ { kind: "tool", callId: "c1", name: "bash", tool: "shell", input: { command: "ls" }, output: "file.txt", status: "completed" },
484
+ ]);
485
+ expect(validateConversationData(data)).toBeNull();
486
+ });
487
+ });
488
+
489
+ // bug: memory/attempt-detail-components-shipped-without-styles.md
490
+ describe("attemptSourceData:标准事件流按 loc 投影回 send 行", () => {
491
+ it("send 行的 turns 携带 sentText 与按序归并的完整回复", () => {
492
+ const sourcePath = "evals/a.ts";
493
+ const data = attemptSourceData(
494
+ evidenceOf({
495
+ capabilities: { ...NO_CAPS, source: true, execution: true },
496
+ evalSource: {
497
+ sourcePath,
498
+ sourceSha256: "x",
499
+ lines: [
500
+ { line: 1, text: 'import { defineEval } from "niceeval";', assertions: [], sends: [] },
501
+ {
502
+ line: 2,
503
+ text: 'const reply = await t.send("hello");',
504
+ assertions: [],
505
+ sends: [{ label: "s1/t1", status: "completed" as const, durationMs: 120, loc: { file: sourcePath, line: 2 } }],
506
+ },
507
+ ],
508
+ unmapped: [],
509
+ summary: {
510
+ totalAssertions: 0,
511
+ mappedAssertions: 0,
512
+ unmappedAssertions: 0,
513
+ passed: 0,
514
+ failed: 0,
515
+ gate: 0,
516
+ soft: 0,
517
+ totalLines: 2,
518
+ annotatedLines: 1,
519
+ },
520
+ },
521
+ events: [
522
+ { type: "message", role: "user", text: "hello", loc: { file: sourcePath, line: 2 } },
523
+ { type: "message", role: "assistant", text: "assistant reply attached to the source line" },
524
+ ],
525
+ }),
526
+ )!;
527
+
528
+ expect(data.lines[1]!.turns[0]).toMatchObject({ label: "s1/t1", sentText: "hello" });
529
+ expect(data.lines[1]!.turns[0]!.replies).toEqual([
530
+ { kind: "assistant", text: "assistant reply attached to the source line" },
531
+ ]);
532
+ expect(data.lines[0]!.turns).toEqual([]);
533
+ });
534
+ });
535
+
536
+ // ───────────────────────── AttemptTimeline:默认折叠 ─────────────────────────
537
+
538
+ describe("AttemptTimeline:默认只显示主链,children 收合", () => {
539
+ it("失败最深节点默认展开(HTML 含 open 属性)并带失败标记,祖先不重复标记", () => {
540
+ const evidence = evidenceOf({
541
+ result: resultOf({
542
+ phases: [
543
+ {
544
+ name: "eval.run",
545
+ durationMs: 100,
546
+ children: [
547
+ {
548
+ id: "t1",
549
+ kind: "turn",
550
+ label: "s1/t1",
551
+ startOffsetMs: 0,
552
+ durationMs: 50,
553
+ failed: true,
554
+ children: [{ id: "h1", kind: "hook", label: "hook#1", startOffsetMs: 0, durationMs: 10 }],
555
+ },
556
+ ],
557
+ },
558
+ ],
559
+ }),
560
+ });
561
+ const data = attemptTimelineData(evidence)!;
562
+ const html = renderToStaticMarkup(<AttemptTimeline data={data} /> as never);
563
+ // 失败最深节点(t1,带子节点故渲染为 <details>)默认展开;它的祖先 phase 自己没有 failed 标记
564
+ // (只有真正失败的最深节点才有),所以只有这一处 open,不是逐层重复标记。
565
+ expect(html).toContain('open=""');
566
+ expect(html).toContain("nre-timeline-failed");
567
+ expect((html.match(/nre-timeline-failed/g) ?? [])).toHaveLength(1);
568
+ });
569
+
570
+ it("默认(全部通过)不展开:HTML 不含 open 属性", () => {
571
+ const evidence = evidenceOf({
572
+ result: resultOf({
573
+ phases: [{ name: "eval.run", durationMs: 100, children: [{ id: "t1", kind: "turn", label: "s1/t1", startOffsetMs: 0, durationMs: 50 }] }],
574
+ }),
575
+ });
576
+ const data = attemptTimelineData(evidence)!;
577
+ const html = renderToStaticMarkup(<AttemptTimeline data={data} /> as never);
578
+ expect(html).not.toContain('open=""');
579
+ });
580
+ });
581
+
582
+ // ───────────────────────── 渲染矩阵:两面在空/非空两态下都真正渲染一次 ─────────────────────────
583
+ // 上面的矩阵只断言了 data 层(null vs 结构化字段);没有一处真正调用大多数叶子的 web/text 渲染
584
+ // 函数——一次 .map / JSON.stringify / 递归写错都能在那一层蒙混过关(typecheck 与 build:report
585
+ // 只证明能编译、能打包,不证明能跑)。这里对每个叶子直接以 data 形态渲染(不经 resolve,构造
586
+ // 方式与「text/web 共享同一份 data」一致):空态两面零可见输出,非空态两面都不抛错且各含一项
587
+ // 该叶子独有的标志性字段。AttemptFixPrompt 的 text 面是故意恒为空串(见 attempt-faces.ts 注释:
588
+ // 终端已有 locator,不重复整段 prompt),因此单列 textMarker: null 断言「恒空串」而不是子串。
589
+ describe("Attempt 详情组件族:渲染矩阵(空/非空两态,两面都真正渲染)", () => {
590
+ const richEvidence = evidenceOf({
591
+ capabilities: FULL_CAPS,
592
+ result: resultOf({
593
+ verdict: "failed",
594
+ error: { code: "timeout", message: "boom", phase: "eval.run" },
595
+ assertions: [
596
+ { name: "a", severity: "gate", outcome: "failed", score: 0, detail: "expected true" },
597
+ { name: "b", severity: "gate", outcome: "passed", score: 1, groupPath: ["g1"] },
598
+ ],
599
+ phases: [
600
+ {
601
+ name: "eval.run",
602
+ durationMs: 100,
603
+ children: [{ id: "t1", kind: "turn", label: "s1/t1", startOffsetMs: 0, durationMs: 50 }],
604
+ },
605
+ ],
606
+ usage: { inputTokens: 10, outputTokens: 5, cacheReadTokens: 2 },
607
+ diagnostics: [{ code: "cleanup-failed", level: "warning", message: "m", phase: "eval.teardown" }],
608
+ }),
609
+ evalSource: {
610
+ sourcePath: "evals/a.ts",
611
+ sourceSha256: "x",
612
+ lines: [
613
+ {
614
+ line: 1,
615
+ text: "t.expect(1).toBe(1)",
616
+ assertions: [
617
+ {
618
+ name: "a",
619
+ severity: "gate" as const,
620
+ outcome: "failed" as const,
621
+ score: 0,
622
+ detail: "expected true",
623
+ loc: { file: "evals/a.ts", line: 1 },
624
+ },
625
+ ],
626
+ sends: [],
627
+ },
628
+ ],
629
+ unmapped: [],
630
+ summary: {
631
+ totalAssertions: 2,
632
+ mappedAssertions: 1,
633
+ unmappedAssertions: 1,
634
+ passed: 1,
635
+ failed: 1,
636
+ gate: 2,
637
+ soft: 0,
638
+ totalLines: 1,
639
+ annotatedLines: 1,
640
+ },
641
+ },
642
+ events: [
643
+ { type: "message", role: "user", text: "go", loc: { file: "evals/a.ts", line: 1 } },
644
+ { type: "message", role: "assistant", text: "hi there" },
645
+ { type: "action.called", callId: "c1", name: "bash", input: { command: "ls" }, tool: "shell" },
646
+ { type: "action.result", callId: "c1", output: "file.txt", status: "completed" },
647
+ ],
648
+ trace: [
649
+ { traceId: "t1", spanId: "root", name: "attempt", startMs: 0, endMs: 100 },
650
+ { traceId: "t1", spanId: "child", parentSpanId: "root", name: "model-call", startMs: 0, endMs: 50 },
651
+ ],
652
+ diff: {
653
+ windows: [{ window: "s1/t1", changes: { "a.ts": { status: "modified" as const, before: "1\n2", after: "1\n3" } } }],
654
+ files: { "a.ts": { net: "modified" as const, windows: ["s1/t1"] } },
655
+ get: () => "1\n3",
656
+ },
657
+ });
658
+
659
+ const emptyEvidence = evidenceOf();
660
+
661
+ const LEAVES = [
662
+ { name: "AttemptError", Component: AttemptError, computeData: attemptErrorData, htmlMarker: "boom", textMarker: "boom" },
663
+ {
664
+ name: "AttemptAssertions",
665
+ Component: AttemptAssertions,
666
+ computeData: attemptAssertionsData,
667
+ htmlMarker: "expected true",
668
+ textMarker: "expected true",
669
+ },
670
+ { name: "AttemptSource", Component: AttemptSource, computeData: attemptSourceData, htmlMarker: "evals/a.ts", textMarker: "evals/a.ts" },
671
+ { name: "AttemptFixPrompt", Component: AttemptFixPrompt, computeData: attemptFixPromptData, htmlMarker: "exp/a", textMarker: null },
672
+ { name: "AttemptTimeline", Component: AttemptTimeline, computeData: attemptTimelineData, htmlMarker: "eval.run", textMarker: "eval.run" },
673
+ {
674
+ name: "AttemptConversation",
675
+ Component: AttemptConversation,
676
+ computeData: attemptConversationData,
677
+ htmlMarker: "hi there",
678
+ textMarker: "hi there",
679
+ },
680
+ {
681
+ name: "AttemptDiagnostics",
682
+ Component: AttemptDiagnostics,
683
+ computeData: attemptDiagnosticsData,
684
+ htmlMarker: "cleanup-failed",
685
+ textMarker: "cleanup-failed",
686
+ },
687
+ { name: "AttemptUsage", Component: AttemptUsage, computeData: attemptUsageData, htmlMarker: "input tokens", textMarker: "tokens" },
688
+ {
689
+ name: "AttemptTrace",
690
+ Component: AttemptTrace,
691
+ computeData: attemptTraceData,
692
+ htmlMarker: "model-call",
693
+ textMarker: "trace:",
694
+ },
695
+ { name: "AttemptDiff", Component: AttemptDiff, computeData: attemptDiffData, htmlMarker: "a.ts", textMarker: "a.ts" },
696
+ ] as const;
697
+
698
+ it.each(LEAVES)("$name:空态两面零输出,非空态两面都渲染且各含标志字段", ({ Component, computeData, htmlMarker, textMarker }) => {
699
+ const emptyData = computeData(emptyEvidence);
700
+ expect(emptyData).toBeNull();
701
+ expect(renderToStaticMarkup(<Component data={emptyData as never} /> as never)).toBe("");
702
+ expect(renderNodeToText(<Component data={emptyData as never} /> as never, createTextContext({ width: 100 }))).toBe("");
703
+
704
+ const loadedData = computeData(richEvidence);
705
+ expect(loadedData).not.toBeNull();
706
+ const html = renderToStaticMarkup(<Component data={loadedData as never} /> as never);
707
+ const text = renderNodeToText(<Component data={loadedData as never} /> as never, createTextContext({ width: 100 }));
708
+ expect(html).toContain(htmlMarker);
709
+ if (textMarker === null) expect(text).toBe("");
710
+ else expect(text).toContain(textMarker);
711
+ });
712
+
713
+ it("AttemptSummary(恒非空)两面都渲染完整证据且含 locator/verdict", () => {
714
+ const data = attemptSummaryData(richEvidence);
715
+ const html = renderToStaticMarkup(<AttemptSummary data={data} /> as never);
716
+ const text = renderNodeToText(<AttemptSummary data={data} /> as never, createTextContext({ width: 100 }));
717
+ expect(html).toContain(richEvidence.locator);
718
+ expect(text).toContain(richEvidence.locator);
719
+ expect(html).toContain("failed");
720
+ expect(text).toContain("failed");
721
+ });
722
+
723
+ // 五个证据组件(AttemptSource/AttemptTimeline/AttemptConversation/AttemptTrace/AttemptDiff)的
724
+ // text 面都拼 `niceeval show <locator> --X` 下钻命令;docs/feature/reports/library/
725
+ // attempt-detail.md「在 show 与 view 怎样渲染」要求"保留完整 locator"——没有 locator 的裸
726
+ // `niceeval show --X` 不可执行,不能算满足契约。命令由宿主经 ctx.attemptCommand 注入
727
+ // (report.ts DEFAULT_ATTEMPT_COMMAND),没有 attempt-input page 时不存在,行退化为纯文本
728
+ // (与 traceWaterfallText 同一套退化规则),不生成假命令。
729
+ describe("证据组件的 text 面下钻命令携带完整 locator", () => {
730
+ const attemptCommand = (locator: string) => `niceeval show ${locator}`;
731
+ const withCommand = createTextContext({ width: 100, attemptCommand });
732
+ const withoutCommand = createTextContext({ width: 100 });
733
+
734
+ const COMMANDED = [
735
+ { name: "AttemptSource", computeData: attemptSourceData, Component: AttemptSource, flag: "--source" },
736
+ { name: "AttemptTimeline", computeData: attemptTimelineData, Component: AttemptTimeline, flag: "--timing" },
737
+ { name: "AttemptConversation", computeData: attemptConversationData, Component: AttemptConversation, flag: "--execution" },
738
+ { name: "AttemptTrace", computeData: attemptTraceData, Component: AttemptTrace, flag: "--timing" },
739
+ { name: "AttemptDiff", computeData: attemptDiffData, Component: AttemptDiff, flag: "--diff" },
740
+ ] as const;
741
+
742
+ it.each(COMMANDED)("$name:ctx.attemptCommand 存在时命令携带 locator,不存在时优雅退化(不生成假命令)", ({ computeData, Component, flag }) => {
743
+ const data = computeData(richEvidence);
744
+ expect(data).not.toBeNull();
745
+ const withText = renderNodeToText(<Component data={data as never} /> as never, withCommand);
746
+ expect(withText).toContain(`niceeval show ${richEvidence.locator} ${flag}`);
747
+ const withoutText = renderNodeToText(<Component data={data as never} /> as never, withoutCommand);
748
+ expect(withoutText).not.toContain("niceeval show");
749
+ expect(withoutText).not.toContain(flag);
750
+ });
751
+ });
752
+
753
+ it("AttemptSource:失败断言的 text 面锚点含完整 file:line:col,可直接复制定位", () => {
754
+ const data = attemptSourceData(richEvidence)!;
755
+ const text = renderNodeToText(<AttemptSource data={data} /> as never, createTextContext({ width: 100 }));
756
+ expect(text).toContain("source: evals/a.ts:1");
757
+ });
758
+ });