niceeval 0.9.0 → 0.10.0

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