niceeval 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +69 -7
  4. package/dist/context/types.d.ts +32 -12
  5. package/dist/i18n/en.d.ts +54 -0
  6. package/dist/i18n/zh-CN.d.ts +57 -3
  7. package/dist/o11y/types.d.ts +16 -2
  8. package/dist/report/aggregate.d.ts +32 -24
  9. package/dist/report/aggregate.js +158 -50
  10. package/dist/report/built-in/index.d.ts +2 -0
  11. package/dist/report/built-in/index.js +8 -0
  12. package/dist/report/components.d.ts +93 -160
  13. package/dist/report/components.js +377 -114
  14. package/dist/report/compute.d.ts +87 -81
  15. package/dist/report/compute.js +597 -417
  16. package/dist/report/flag.d.ts +32 -6
  17. package/dist/report/flag.js +92 -4
  18. package/dist/report/format.d.ts +19 -11
  19. package/dist/report/format.js +30 -13
  20. package/dist/report/index.d.ts +16 -16
  21. package/dist/report/index.js +20 -21
  22. package/dist/report/load.js +3 -2
  23. package/dist/report/locale.d.ts +57 -33
  24. package/dist/report/locale.js +122 -56
  25. package/dist/report/metrics.d.ts +23 -4
  26. package/dist/report/metrics.js +110 -25
  27. package/dist/report/primitives.d.ts +48 -15
  28. package/dist/report/primitives.js +135 -26
  29. package/dist/report/react/AttemptList.d.ts +9 -7
  30. package/dist/report/react/AttemptList.js +17 -10
  31. package/dist/report/react/DeltaTable.js +19 -18
  32. package/dist/report/react/EvalList.d.ts +4 -4
  33. package/dist/report/react/EvalList.js +0 -0
  34. package/dist/report/react/ExperimentComparison.d.ts +10 -0
  35. package/dist/report/react/ExperimentComparison.js +12 -0
  36. package/dist/report/react/ExperimentList.d.ts +4 -3
  37. package/dist/report/react/ExperimentList.js +17 -18
  38. package/dist/report/react/MetricBars.js +5 -4
  39. package/dist/report/react/MetricLine.js +12 -5
  40. package/dist/report/react/MetricMatrix.js +1 -1
  41. package/dist/report/react/MetricScatter.js +59 -28
  42. package/dist/report/react/MetricTable.js +2 -12
  43. package/dist/report/react/ScopeSummary.d.ts +10 -0
  44. package/dist/report/react/ScopeSummary.js +17 -0
  45. package/dist/report/react/Scoreboard.js +6 -6
  46. package/dist/report/react/cell.js +2 -2
  47. package/dist/report/react/chart-math.d.ts +23 -6
  48. package/dist/report/react/chart-math.js +71 -19
  49. package/dist/report/react/fixtures.d.ts +5 -9
  50. package/dist/report/react/fixtures.js +110 -147
  51. package/dist/report/react/index.d.ts +15 -5
  52. package/dist/report/react/index.js +18 -7
  53. package/dist/report/report.d.ts +137 -16
  54. package/dist/report/report.js +259 -28
  55. package/dist/report/text/faces.d.ts +17 -19
  56. package/dist/report/text/faces.js +253 -184
  57. package/dist/report/text/plot.js +1 -1
  58. package/dist/report/text/table.js +38 -7
  59. package/dist/report/tree.d.ts +90 -40
  60. package/dist/report/tree.js +252 -94
  61. package/dist/report/types.d.ts +247 -284
  62. package/dist/report/types.js +4 -3
  63. package/dist/report/web.d.ts +21 -5
  64. package/dist/report/web.js +42 -16
  65. package/dist/results/select.d.ts +38 -16
  66. package/dist/results/select.js +73 -25
  67. package/dist/results/types.d.ts +49 -14
  68. package/dist/runner/feedback/sink.d.ts +110 -0
  69. package/dist/runner/types.d.ts +513 -22
  70. package/dist/sandbox/docker.d.ts +23 -2
  71. package/dist/sandbox/e2b.d.ts +15 -1
  72. package/dist/sandbox/errors.d.ts +30 -3
  73. package/dist/sandbox/io-retry.d.ts +17 -0
  74. package/dist/sandbox/registry.d.ts +2 -0
  75. package/dist/sandbox/resolve.d.ts +18 -5
  76. package/dist/sandbox/retry.d.ts +11 -1
  77. package/dist/sandbox/types.d.ts +39 -5
  78. package/dist/sandbox/vercel.d.ts +7 -1
  79. package/dist/scoring/coverage.d.ts +30 -0
  80. package/dist/scoring/display.d.ts +21 -0
  81. package/dist/scoring/display.js +120 -0
  82. package/dist/scoring/types.d.ts +103 -20
  83. package/dist/shared/aggregate.d.ts +4 -2
  84. package/dist/shared/aggregate.js +8 -7
  85. package/dist/shared/types.d.ts +28 -0
  86. package/dist/tty-line.d.ts +0 -4
  87. package/dist/util.d.ts +23 -0
  88. package/docs-site/zh/README.md +44 -0
  89. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  90. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  91. package/docs-site/zh/examples/index.mdx +50 -0
  92. package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
  93. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  94. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  95. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  96. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  97. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  98. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  99. package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
  100. package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
  101. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  102. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
  103. package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
  104. package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
  105. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  106. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
  107. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
  108. package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
  109. package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
  110. package/docs-site/zh/how-to/publish-report.mdx +105 -0
  111. package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
  112. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
  113. package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
  114. package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
  115. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
  116. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
  117. package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
  118. package/docs-site/zh/index.mdx +24 -26
  119. package/docs-site/zh/introduction.mdx +8 -8
  120. package/docs-site/zh/reference/builtin-agents.mdx +32 -5
  121. package/docs-site/zh/reference/capabilities.mdx +8 -8
  122. package/docs-site/zh/reference/cli.mdx +40 -12
  123. package/docs-site/zh/reference/define-agent.mdx +58 -5
  124. package/docs-site/zh/reference/define-config.mdx +1 -1
  125. package/docs-site/zh/reference/define-eval.mdx +42 -9
  126. package/docs-site/zh/reference/events.mdx +3 -3
  127. package/docs-site/zh/reference/expect.mdx +26 -1
  128. package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
  129. package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
  130. package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
  131. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
  132. package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
  133. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  134. package/package.json +10 -2
  135. package/src/agents/ai-sdk-otel.test.ts +1 -0
  136. package/src/agents/ai-sdk.test.ts +3 -0
  137. package/src/agents/ai-sdk.ts +3 -0
  138. package/src/agents/bub-install-spec.test.ts +34 -0
  139. package/src/agents/bub-install-spec.ts +32 -0
  140. package/src/agents/bub.ts +31 -32
  141. package/src/agents/claude-code.test.ts +130 -9
  142. package/src/agents/claude-code.ts +76 -4
  143. package/src/agents/codex.test.ts +189 -40
  144. package/src/agents/codex.ts +155 -14
  145. package/src/agents/coding-cli-versions.test.ts +15 -0
  146. package/src/agents/coding-cli-versions.ts +3 -0
  147. package/src/agents/index.ts +13 -2
  148. package/src/agents/langgraph.test.ts +204 -0
  149. package/src/agents/langgraph.ts +495 -0
  150. package/src/agents/marketplace.ts +85 -0
  151. package/src/agents/native-config.test.ts +179 -0
  152. package/src/agents/native-config.ts +267 -0
  153. package/src/agents/openai-compat.test.ts +1 -0
  154. package/src/agents/openai-compat.ts +1 -1
  155. package/src/agents/openclaw.test.ts +31 -0
  156. package/src/agents/openclaw.ts +171 -0
  157. package/src/agents/plugin-config.test.ts +1 -0
  158. package/src/agents/sdk-streams.test.ts +79 -0
  159. package/src/agents/sdk-streams.ts +55 -10
  160. package/src/agents/skills.test.ts +1 -0
  161. package/src/agents/streaming.test.ts +3 -9
  162. package/src/agents/streaming.ts +2 -2
  163. package/src/agents/types.ts +71 -8
  164. package/src/agents/ui-message-stream.test.ts +3 -0
  165. package/src/cli.ts +446 -124
  166. package/src/context/context.test.ts +51 -12
  167. package/src/context/context.ts +162 -30
  168. package/src/context/session.test.ts +2 -1
  169. package/src/context/session.ts +115 -7
  170. package/src/context/types.ts +30 -12
  171. package/src/define.test.ts +13 -8
  172. package/src/define.ts +25 -4
  173. package/src/expect/index.ts +53 -23
  174. package/src/i18n/en.ts +81 -17
  175. package/src/i18n/zh-CN.ts +80 -17
  176. package/src/o11y/cost.test.ts +1 -0
  177. package/src/o11y/execution-tree.test.ts +1 -20
  178. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  179. package/src/o11y/otlp/parse.test.ts +1 -0
  180. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  181. package/src/o11y/parsers/bub.test.ts +1 -0
  182. package/src/o11y/parsers/claude-code.test.ts +1 -34
  183. package/src/o11y/parsers/openclaw.test.ts +154 -0
  184. package/src/o11y/parsers/openclaw.ts +310 -0
  185. package/src/o11y/prices.json +746 -311
  186. package/src/o11y/tool-names.test.ts +1 -0
  187. package/src/o11y/types.ts +16 -2
  188. package/src/report/aggregate.ts +178 -61
  189. package/src/report/built-in/index.tsx +9 -0
  190. package/src/report/components.tsx +625 -279
  191. package/src/report/compute.ts +723 -491
  192. package/src/report/dual-render.test.tsx +741 -1024
  193. package/src/report/flag.ts +104 -12
  194. package/src/report/format.ts +32 -12
  195. package/src/report/index.ts +119 -46
  196. package/src/report/load.ts +3 -2
  197. package/src/report/locale.ts +136 -65
  198. package/src/report/metrics.ts +108 -25
  199. package/src/report/primitives.tsx +196 -45
  200. package/src/report/react/AttemptList.tsx +30 -43
  201. package/src/report/react/DeltaTable.tsx +63 -45
  202. package/src/report/react/EvalList.tsx +0 -0
  203. package/src/report/react/ExperimentComparison.tsx +73 -0
  204. package/src/report/react/ExperimentList.tsx +50 -32
  205. package/src/report/react/MetricBars.tsx +5 -4
  206. package/src/report/react/MetricLine.tsx +13 -8
  207. package/src/report/react/MetricMatrix.tsx +2 -2
  208. package/src/report/react/MetricScatter.tsx +86 -34
  209. package/src/report/react/MetricTable.tsx +4 -76
  210. package/src/report/react/ScopeSummary.tsx +86 -0
  211. package/src/report/react/Scoreboard.tsx +28 -10
  212. package/src/report/react/cell.tsx +2 -2
  213. package/src/report/react/chart-math.test.ts +85 -0
  214. package/src/report/react/chart-math.ts +101 -22
  215. package/src/report/react/enhance.js +89 -5
  216. package/src/report/react/fixtures.ts +114 -154
  217. package/src/report/react/index.tsx +24 -39
  218. package/src/report/react/render.test.tsx +138 -158
  219. package/src/report/react/styles.css +243 -82
  220. package/src/report/report.test.ts +779 -841
  221. package/src/report/report.ts +423 -41
  222. package/src/report/text/faces.ts +290 -193
  223. package/src/report/text/plot.ts +1 -1
  224. package/src/report/text/table.ts +44 -7
  225. package/src/report/tree.ts +362 -104
  226. package/src/report/types.ts +261 -271
  227. package/src/report/web.ts +63 -20
  228. package/src/results/annotated-source.test.ts +62 -9
  229. package/src/results/annotated-source.ts +64 -6
  230. package/src/results/attempt-evidence.test.ts +13 -11
  231. package/src/results/attempt-evidence.ts +20 -13
  232. package/src/results/attempt-source.ts +6 -3
  233. package/src/results/copy.ts +150 -60
  234. package/src/results/host-equivalence.test.ts +34 -20
  235. package/src/results/index.ts +12 -4
  236. package/src/results/locator.test.ts +1 -22
  237. package/src/results/open.ts +15 -5
  238. package/src/results/publish.ts +149 -0
  239. package/src/results/results.test.ts +89 -54
  240. package/src/results/select.ts +104 -34
  241. package/src/results/truncate.ts +90 -0
  242. package/src/results/types.ts +43 -14
  243. package/src/results/writer.ts +31 -13
  244. package/src/runner/attempt.test.ts +138 -7
  245. package/src/runner/attempt.ts +603 -104
  246. package/src/runner/discover.test.ts +47 -0
  247. package/src/runner/discover.ts +36 -2
  248. package/src/runner/eval-source.test.ts +1 -27
  249. package/src/runner/feedback/agent.test.ts +504 -0
  250. package/src/runner/feedback/agent.ts +409 -0
  251. package/src/runner/feedback/ci.test.ts +562 -0
  252. package/src/runner/feedback/ci.ts +401 -0
  253. package/src/runner/feedback/coordinator.test.ts +317 -0
  254. package/src/runner/feedback/coordinator.ts +397 -0
  255. package/src/runner/feedback/failure.ts +40 -0
  256. package/src/runner/feedback/human.test.ts +616 -0
  257. package/src/runner/feedback/human.ts +535 -0
  258. package/src/runner/feedback/index.ts +66 -0
  259. package/src/runner/feedback/io.ts +78 -0
  260. package/src/runner/feedback/profile.test.ts +50 -0
  261. package/src/runner/feedback/profile.ts +58 -0
  262. package/src/runner/feedback/reducer.test.ts +395 -0
  263. package/src/runner/feedback/reducer.ts +260 -0
  264. package/src/runner/feedback/renderer.ts +82 -0
  265. package/src/runner/feedback/sink.ts +203 -0
  266. package/src/runner/feedback/testing.ts +106 -0
  267. package/src/runner/ledger.test.ts +230 -0
  268. package/src/runner/ledger.ts +329 -0
  269. package/src/runner/report.test.ts +128 -3
  270. package/src/runner/report.ts +33 -9
  271. package/src/runner/reporters/artifacts.ts +8 -2
  272. package/src/runner/reporters/braintrust.test.ts +8 -7
  273. package/src/runner/reporters/braintrust.ts +9 -2
  274. package/src/runner/reporters/index.ts +2 -2
  275. package/src/runner/reporters/json.test.ts +162 -0
  276. package/src/runner/reporters/json.ts +35 -8
  277. package/src/runner/reporters/shared.ts +1 -5
  278. package/src/runner/run.test.ts +760 -3
  279. package/src/runner/run.ts +243 -37
  280. package/src/runner/sandbox-prep.ts +3 -42
  281. package/src/runner/timing.ts +158 -0
  282. package/src/runner/types.ts +518 -22
  283. package/src/sandbox/checkpoint.test.ts +55 -0
  284. package/src/sandbox/checkpoint.ts +29 -8
  285. package/src/sandbox/cli-commands.ts +407 -0
  286. package/src/sandbox/docker.ts +115 -16
  287. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  288. package/src/sandbox/e2b-agent-template.ts +94 -0
  289. package/src/sandbox/e2b.ts +74 -9
  290. package/src/sandbox/errors.ts +111 -4
  291. package/src/sandbox/index.ts +2 -0
  292. package/src/sandbox/io-retry.test.ts +58 -0
  293. package/src/sandbox/io-retry.ts +45 -0
  294. package/src/sandbox/keep-registry.test.ts +86 -0
  295. package/src/sandbox/keep-registry.ts +142 -0
  296. package/src/sandbox/keep.ts +178 -0
  297. package/src/sandbox/paths.test.ts +1 -0
  298. package/src/sandbox/paths.ts +19 -8
  299. package/src/sandbox/registry.ts +20 -3
  300. package/src/sandbox/resolve.ts +76 -11
  301. package/src/sandbox/retry.test.ts +70 -0
  302. package/src/sandbox/retry.ts +46 -4
  303. package/src/sandbox/types.ts +44 -6
  304. package/src/sandbox/vercel.ts +43 -20
  305. package/src/scoring/collector.ts +60 -17
  306. package/src/scoring/coverage.ts +95 -0
  307. package/src/scoring/diff.ts +81 -0
  308. package/src/scoring/display.test.ts +121 -0
  309. package/src/scoring/display.ts +133 -0
  310. package/src/scoring/evidence.test.ts +189 -0
  311. package/src/scoring/judge.test.ts +142 -0
  312. package/src/scoring/judge.ts +15 -18
  313. package/src/scoring/scoped.ts +217 -50
  314. package/src/scoring/types.ts +117 -20
  315. package/src/scoring/verdict.ts +16 -4
  316. package/src/shared/aggregate.ts +8 -6
  317. package/src/shared/types.ts +31 -0
  318. package/src/show/compose.ts +50 -67
  319. package/src/show/index.ts +127 -56
  320. package/src/show/render.ts +662 -131
  321. package/src/show/report-host.test.ts +188 -0
  322. package/src/show/report-host.ts +375 -0
  323. package/src/show/show.test.ts +320 -54
  324. package/src/tty-line.ts +8 -26
  325. package/src/util.test.ts +1 -0
  326. package/src/util.ts +41 -0
  327. package/src/view/app/App.test.tsx +69 -0
  328. package/src/view/app/App.tsx +144 -48
  329. package/src/view/app/components/AttemptModal.tsx +423 -11
  330. package/src/view/app/components/CodeView.tsx +41 -14
  331. package/src/view/app/components/CopyControls.tsx +2 -2
  332. package/src/view/app/i18n.ts +37 -17
  333. package/src/view/app/lib/attempt-route.test.ts +1 -0
  334. package/src/view/app/lib/verdict.ts +7 -9
  335. package/src/view/app/main.tsx +13 -8
  336. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  337. package/src/view/app/types.ts +4 -1
  338. package/src/view/artifact-serving.test.ts +2 -1
  339. package/src/view/client-dist/app.css +1 -1
  340. package/src/view/client-dist/app.js +17 -17
  341. package/src/view/data.test.ts +10 -3
  342. package/src/view/data.ts +155 -49
  343. package/src/view/index.ts +56 -41
  344. package/src/view/server.ts +37 -15
  345. package/src/view/shared/types.ts +34 -5
  346. package/src/view/styles.css +227 -0
  347. package/src/view/view-report.test.ts +167 -62
  348. package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
  349. package/dist/report/built-ins/experiment-comparison.js +0 -13
  350. package/dist/report/built-ins/index.d.ts +0 -1
  351. package/dist/report/built-ins/index.js +0 -2
  352. package/dist/report/react/GroupSummary.d.ts +0 -8
  353. package/dist/report/react/GroupSummary.js +0 -8
  354. package/dist/report/react/RunOverview.d.ts +0 -8
  355. package/dist/report/react/RunOverview.js +0 -12
  356. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  357. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  358. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  359. package/docs-site/zh/example/showcase.mdx +0 -39
  360. package/docs-site/zh/guides/publish-report.mdx +0 -91
  361. package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
  362. package/src/report/built-in-user-parity.test.tsx +0 -640
  363. package/src/report/built-ins/experiment-comparison.tsx +0 -19
  364. package/src/report/built-ins/index.ts +0 -2
  365. package/src/report/react/GroupSummary.tsx +0 -66
  366. package/src/report/react/RunOverview.tsx +0 -109
  367. package/src/runner/reporters/console.ts +0 -70
  368. package/src/runner/reporters/live.test.ts +0 -56
  369. package/src/runner/reporters/live.ts +0 -247
  370. package/src/runner/reporters/quiet.test.ts +0 -66
  371. package/src/runner/reporters/quiet.ts +0 -49
  372. package/src/runner/reporters/table.ts +0 -277
  373. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  374. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  375. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  376. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  377. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  378. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
@@ -1,10 +1,38 @@
1
- import { describe, expect, it } from "vitest";
2
- import { judgeProbeTargets } from "./run.ts";
3
- import type { JudgeConfig } from "../types.ts";
1
+ // cases: docs/engineering/unit-tests/experiments-runner/cases.md
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { judgeProbeTargets, runEvals } from "./run.ts";
8
+ import { defineSandbox, defineSandboxAgent } from "../define.ts";
9
+ import { Artifacts } from "./reporters/artifacts.ts";
10
+ import { openResults } from "../results/open.ts";
11
+ import { encodeAttemptLocator } from "../results/locator.ts";
12
+ import { equals } from "../expect/index.ts";
13
+ import { createFeedbackCoordinator, type FeedbackCoordinator } from "./feedback/coordinator.ts";
14
+ import { createFakeFeedbackIO } from "./feedback/testing.ts";
15
+ import { activeFeedbackSinkCount } from "./feedback/sink.ts";
16
+ import type { CapturedEvalSource } from "./eval-source.ts";
17
+ import type { CarryPlan } from "./fingerprint.ts";
18
+ import type { AgentRun, RunFeedbackPlan, RunOptions } from "./types.ts";
19
+ import type {
20
+ Agent,
21
+ CommandResult,
22
+ Config,
23
+ DiscoveredEval,
24
+ EvalResult,
25
+ JudgeConfig,
26
+ Reporter,
27
+ ReporterRegistration,
28
+ Sandbox,
29
+ SandboxFile,
30
+ } from "../types.ts";
4
31
 
5
32
  // judge 预检的目标收敛:只探测「实际要跑、且源码里出现 judge 字样」的 eval 的生效配置。
6
33
  // 这是对 memory/judge-config-precheck-hard-fails-without-key 的修复守护——
7
34
  // 全局配了 judge 但选中的 eval 都不用时,不能再因 judge key / 端点问题拦下整次运行。
35
+ // bug: memory/judge-config-precheck-hard-fails-without-key.md
8
36
  describe("judgeProbeTargets", () => {
9
37
  const configJudge: JudgeConfig = { model: "gpt-5.4" };
10
38
 
@@ -48,3 +76,732 @@ describe("judgeProbeTargets", () => {
48
76
  expect(judgeProbeTargets(evals, configJudge)).toEqual([]);
49
77
  });
50
78
  });
79
+
80
+ // ───────────────────────── locator identity 集成测试的 fixture ─────────────────────────
81
+ // 沙箱是内存 fake(记文件,不起容器/不联网)——与 attempt.test.ts 同一种 recipe,这里额外
82
+ // 驱动完整 runEvals() 调度(而不是单个 runAttemptEffect),覆盖 locator 在 reporter 回调 /
83
+ // 事件与落盘 result.json 之间必须完全一致的不变量(见 docs/feature/experiments/cli.md
84
+ // 「Locator 必须在 result 发布前确定」)。
85
+
86
+ class FakeSandbox implements Partial<Sandbox> {
87
+ readonly workdir = "/workspace";
88
+ readonly sandboxId = "fake";
89
+ readonly otlpHost = null;
90
+ readonly files = new Map<string, string>();
91
+
92
+ async runShell(): Promise<CommandResult> {
93
+ return { stdout: "", stderr: "", exitCode: 0 };
94
+ }
95
+ async runCommand(): Promise<CommandResult> {
96
+ return { stdout: "", stderr: "", exitCode: 0 };
97
+ }
98
+ async writeFiles(files: Record<string, string>, targetDir?: string): Promise<void> {
99
+ for (const [path, content] of Object.entries(files)) {
100
+ this.files.set(targetDir ? `${targetDir}/${path}` : path, content);
101
+ }
102
+ }
103
+ async uploadFiles(files: SandboxFile[], targetDir?: string): Promise<void> {
104
+ for (const f of files) {
105
+ this.files.set(targetDir ? `${targetDir}/${f.path}` : f.path, f.content.toString());
106
+ }
107
+ }
108
+ async uploadFile(path: string, content: Buffer): Promise<void> {
109
+ this.files.set(path, content.toString());
110
+ }
111
+ async uploadDirectory(): Promise<void> {}
112
+ async downloadFile(path: string): Promise<Buffer> {
113
+ return Buffer.from(this.files.get(path) ?? "");
114
+ }
115
+ async fileExists(path: string): Promise<boolean> {
116
+ return this.files.has(path);
117
+ }
118
+ async readFile(path: string): Promise<string> {
119
+ const hit = this.files.get(path);
120
+ if (hit === undefined) throw new Error(`no such file: ${path}`);
121
+ return hit;
122
+ }
123
+ async readSourceFiles(): Promise<never> {
124
+ throw new Error("not implemented");
125
+ }
126
+ async stop(): Promise<void> {}
127
+ }
128
+
129
+ const asSandbox = (box: FakeSandbox): Sandbox => box as unknown as Sandbox;
130
+
131
+ // judge 预检需要一个真实可读的文件(runEvals 无条件 readFile(evalDef.sourcePath));
132
+ // 内容无所谓(这些测试都不配置 judge),直接指向本测试文件自己,永远存在。
133
+ const sourcePath = fileURLToPath(import.meta.url);
134
+ const source: CapturedEvalSource = { path: "fake.eval.ts", content: "", sha256: "0".repeat(64) };
135
+
136
+ function makeAgent(name: string): Agent {
137
+ return defineSandboxAgent({ name, send: async () => ({ events: [], status: "completed" }) });
138
+ }
139
+
140
+ function fakeSandboxSpec() {
141
+ // 自定义 provider:create() 直接返回内存 fake,绕开真实沙箱 provider;每次调用给一个
142
+ // 全新实例,并发 attempt 之间不共享可变文件状态。
143
+ return defineSandbox({ name: "fake-provider", create: async () => asSandbox(new FakeSandbox()) });
144
+ }
145
+
146
+ function makeEval(id: string, test: DiscoveredEval["test"]): DiscoveredEval {
147
+ return { id, baseDir: "/project", sourcePath, source, test };
148
+ }
149
+
150
+ const roots: string[] = [];
151
+ async function makeRoot(): Promise<string> {
152
+ const root = await mkdtemp(join(tmpdir(), "niceeval-run-locator-"));
153
+ roots.push(root);
154
+ return root;
155
+ }
156
+ afterEach(async () => {
157
+ await Promise.all(roots.splice(0).map((r) => rm(r, { recursive: true, force: true })));
158
+ });
159
+
160
+ function resultKey(r: { experimentId?: string; id: string; attempt: number }): string {
161
+ return `${r.experimentId ?? ""}|${r.id}|${r.attempt}`;
162
+ }
163
+
164
+ /**
165
+ * 跑一次完整 runEvals():自带一个捕获 reporter(记录 onEvalComplete / eval:complete 事件
166
+ * 观察到的 locator,按 `experimentId|evalId|attempt` 建索引)与真实 Artifacts reporter
167
+ * (落盘到临时目录,供事后用 openResults() 核对与 reporter 观察到的值是否一致)。
168
+ */
169
+ async function run(
170
+ evals: DiscoveredEval[],
171
+ agentRuns: AgentRun[],
172
+ opts: {
173
+ extraReporters?: ReporterRegistration[];
174
+ carryPlan?: CarryPlan;
175
+ maxConcurrency?: number;
176
+ signal?: AbortSignal;
177
+ } = {},
178
+ ): Promise<{
179
+ summary: Awaited<ReturnType<typeof runEvals>>;
180
+ root: string;
181
+ onEvalComplete: Map<string, string | undefined>;
182
+ onEventComplete: Map<string, string | undefined>;
183
+ }> {
184
+ const root = await makeRoot();
185
+ const onEvalComplete = new Map<string, string | undefined>();
186
+ const onEventComplete = new Map<string, string | undefined>();
187
+ const capture: Reporter = {
188
+ onEvalComplete(result) {
189
+ onEvalComplete.set(resultKey(result), result.locator);
190
+ },
191
+ onEvent(event) {
192
+ if (event.type === "eval:complete") {
193
+ onEventComplete.set(resultKey(event.result), event.result.locator);
194
+ }
195
+ },
196
+ };
197
+ const config: Config = {};
198
+ const runOpts: RunOptions = {
199
+ config,
200
+ evals,
201
+ agentRuns,
202
+ reporters: [
203
+ { reporter: capture, name: "capture", required: false },
204
+ { reporter: Artifacts(root), name: "artifacts", required: false },
205
+ ...(opts.extraReporters ?? []),
206
+ ],
207
+ maxConcurrency: opts.maxConcurrency ?? 3,
208
+ ...(opts.carryPlan ? { carryPlan: opts.carryPlan } : {}),
209
+ ...(opts.signal ? { signal: opts.signal } : {}),
210
+ };
211
+ const summary = await runEvals(runOpts);
212
+ return { summary, root, onEvalComplete, onEventComplete };
213
+ }
214
+
215
+ async function diskSnapshotStartedAt(root: string, experimentId: string): Promise<string> {
216
+ const results = await openResults(root);
217
+ const exp = results.experiments.find((e) => e.id === experimentId);
218
+ if (!exp) throw new Error(`no snapshot written for experiment ${experimentId}`);
219
+ return exp.latest.startedAt;
220
+ }
221
+
222
+ async function diskLocator(
223
+ root: string,
224
+ experimentId: string,
225
+ evalId: string,
226
+ attempt: number,
227
+ ): Promise<string | undefined> {
228
+ const results = await openResults(root);
229
+ const exp = results.experiments.find((e) => e.id === experimentId);
230
+ const ev = exp?.latest.evals.find((e) => e.id === evalId);
231
+ return ev?.attempts.find((a) => a.result.attempt === attempt)?.locator;
232
+ }
233
+
234
+ // runner 只是「展开、调度、串行化 reporter 回调」——locator 的确定性完全来自
235
+ // encodeAttemptLocator 自己(已有 src/results/locator.test.ts 与
236
+ // src/results/results.test.ts 的 AttemptLocator 套件覆盖)。这里要守的不变量是编排层的:
237
+ // fresh result 的 locator 必须在任何 reporter 看到它之前就已经"是最终值",且与落盘
238
+ // result.json 完全相同;carry result 必须原样透传,不能被本次 invocation 的
239
+ // snapshotStartedAt 悄悄重算成另一个身份。
240
+ describe("runEvals · fresh EvalResult.locator 在 reporter 观察到之前已经确定", () => {
241
+ it("onEvalComplete / eval:complete 观察到的 locator 与落盘 result.json 完全相同(passed 与 errored 各一次)", async () => {
242
+ const experimentId = "locator-exp";
243
+ const evalOk = makeEval("ok", () => {});
244
+ const evalBoom = makeEval("boom", () => {
245
+ throw new Error("boom");
246
+ });
247
+ const agentRun: AgentRun = {
248
+ agent: makeAgent("agent-a"),
249
+ flags: {},
250
+ runs: 1,
251
+ earlyExit: true,
252
+ sandbox: fakeSandboxSpec(),
253
+ timeoutMs: 5_000,
254
+ evalFilter: () => true,
255
+ experimentId,
256
+ };
257
+
258
+ const { summary, root, onEvalComplete, onEventComplete } = await run([evalOk, evalBoom], [agentRun]);
259
+
260
+ expect(summary.results).toHaveLength(2);
261
+ expect(summary.results.find((r) => r.id === "ok")!.verdict).toBe("passed");
262
+ expect(summary.results.find((r) => r.id === "boom")!.verdict).toBe("errored");
263
+
264
+ const snapStartedAt = await diskSnapshotStartedAt(root, experimentId);
265
+ for (const evalId of ["ok", "boom"]) {
266
+ const key = `${experimentId}|${evalId}|0`;
267
+ const expected = encodeAttemptLocator({ experimentId, snapshotStartedAt: snapStartedAt, evalId, attempt: 0 });
268
+ expect(onEvalComplete.get(key)).toBe(expected);
269
+ expect(onEventComplete.get(key)).toBe(expected);
270
+ expect(await diskLocator(root, experimentId, evalId, 0)).toBe(expected);
271
+ expect(summary.results.find((r) => r.id === evalId)!.locator).toBe(expected);
272
+ }
273
+ });
274
+
275
+ it("多 experiment 共享同一次 invocation 的 snapshotStartedAt,不因此碰撞", async () => {
276
+ const eval1 = makeEval("algebra/q1", () => {});
277
+ const runFor = (experimentId: string): AgentRun => ({
278
+ agent: makeAgent(experimentId),
279
+ flags: {},
280
+ runs: 1,
281
+ earlyExit: true,
282
+ sandbox: fakeSandboxSpec(),
283
+ timeoutMs: 5_000,
284
+ evalFilter: () => true,
285
+ experimentId,
286
+ });
287
+
288
+ const { root, onEvalComplete } = await run([eval1], [runFor("exp/a"), runFor("exp/b")]);
289
+
290
+ const startedA = await diskSnapshotStartedAt(root, "exp/a");
291
+ const startedB = await diskSnapshotStartedAt(root, "exp/b");
292
+ expect(startedA).toBe(startedB); // 共享同一个 invocation 锚点(Artifacts writer 与 runner 用同一个值)
293
+
294
+ const locatorA = onEvalComplete.get("exp/a|algebra/q1|0");
295
+ const locatorB = onEvalComplete.get("exp/b|algebra/q1|0");
296
+ expect(locatorA).toBeDefined();
297
+ expect(locatorB).toBeDefined();
298
+ expect(locatorA).not.toBe(locatorB); // experimentId 参与身份元组,不因共享锚点而碰撞
299
+ expect(await diskLocator(root, "exp/a", "algebra/q1", 0)).toBe(locatorA);
300
+ expect(await diskLocator(root, "exp/b", "algebra/q1", 0)).toBe(locatorB);
301
+ });
302
+
303
+ it("同一 eval 的多次 attempt(runs > 1)各自拿到不同且稳定的 locator", async () => {
304
+ const experimentId = "retry-exp";
305
+ const evalDef = makeEval("flaky", () => {});
306
+ const agentRun: AgentRun = {
307
+ agent: makeAgent("agent-retry"),
308
+ flags: {},
309
+ runs: 2,
310
+ earlyExit: false, // 两次都要真的跑,不能被首过即停吞掉其中一次
311
+ sandbox: fakeSandboxSpec(),
312
+ timeoutMs: 5_000,
313
+ evalFilter: () => true,
314
+ experimentId,
315
+ };
316
+
317
+ const { root, onEvalComplete } = await run([evalDef], [agentRun]);
318
+
319
+ const locator0 = onEvalComplete.get(`${experimentId}|flaky|0`);
320
+ const locator1 = onEvalComplete.get(`${experimentId}|flaky|1`);
321
+ expect(locator0).toBeDefined();
322
+ expect(locator1).toBeDefined();
323
+ expect(locator0).not.toBe(locator1);
324
+ expect(await diskLocator(root, experimentId, "flaky", 0)).toBe(locator0);
325
+ expect(await diskLocator(root, experimentId, "flaky", 1)).toBe(locator1);
326
+ });
327
+
328
+ it("carry 结果的 locator 原样透传,不按本次 invocation 的 snapshotStartedAt 重算", async () => {
329
+ const experimentId = "carry-exp";
330
+ const evalId = "carried-eval";
331
+ const staleLocator = encodeAttemptLocator({
332
+ experimentId,
333
+ snapshotStartedAt: "2020-01-01T00:00:00.000Z", // 明确不同于本次 invocation 的锚点
334
+ evalId,
335
+ attempt: 0,
336
+ });
337
+ const carried: EvalResult = {
338
+ id: evalId,
339
+ experimentId,
340
+ agent: "agent-carried",
341
+ verdict: "passed",
342
+ attempt: 0,
343
+ startedAt: "2020-01-01T00:00:00.000Z",
344
+ durationMs: 1,
345
+ assertions: [],
346
+ locator: staleLocator,
347
+ artifactBase: `${experimentId}/some-old-snapshot/${evalId}/a0`,
348
+ };
349
+ // eval 的 test() 会抛错——如果携带 / 首过即停判断漏了这条、真的调度了一次新 attempt,
350
+ // 这里会产出一条 errored 的重复结果,而不是静默漏测。
351
+ const evalDef = makeEval(evalId, () => {
352
+ throw new Error("carried result should have skipped scheduling a fresh attempt");
353
+ });
354
+ const agentRun: AgentRun = {
355
+ agent: makeAgent("agent-carried"),
356
+ flags: {},
357
+ runs: 1,
358
+ earlyExit: true,
359
+ sandbox: fakeSandboxSpec(),
360
+ timeoutMs: 5_000,
361
+ evalFilter: () => true,
362
+ experimentId,
363
+ };
364
+
365
+ const { summary, root } = await run([evalDef], [agentRun], {
366
+ carryPlan: {
367
+ plannedFingerprints: new Map(),
368
+ priorRunKeys: new Set([`${experimentId}|${evalId}`]),
369
+ carriedResults: [carried],
370
+ },
371
+ });
372
+
373
+ const matches = summary.results.filter((r) => r.id === evalId);
374
+ expect(matches).toHaveLength(1); // 没有额外调度出一条新 attempt
375
+ expect(matches[0]!.verdict).toBe("passed"); // 是携带的那份,不是抛错的新跑
376
+ expect(matches[0]!.locator).toBe(staleLocator); // 原样透传,run.ts 没有碰过它
377
+
378
+ // 反证:如果按本次 invocation 的 snapshotStartedAt 重算,会得到不同的字符串——
379
+ // 证明确实是原样透传,不是巧合相等。
380
+ const snapStartedAt = await diskSnapshotStartedAt(root, experimentId);
381
+ const wronglyRecomputed = encodeAttemptLocator({
382
+ experimentId,
383
+ snapshotStartedAt: snapStartedAt,
384
+ evalId,
385
+ attempt: 0,
386
+ });
387
+ expect(staleLocator).not.toBe(wronglyRecomputed);
388
+
389
+ // Artifacts.onRunComplete 把携带条目落盘时,同样原样保留 locator。
390
+ expect(await diskLocator(root, experimentId, evalId, 0)).toBe(staleLocator);
391
+ });
392
+
393
+ it("并发完成顺序打乱不影响各自 attempt 的 locator 与身份的对应关系", async () => {
394
+ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
395
+ const experimentId = "concurrent-exp";
396
+ // 刻意让"先派发"的反而"最后完成",验证完成顺序打乱不影响身份对应关系。
397
+ const evalSlow = makeEval("c-slow", async () => {
398
+ await sleep(60);
399
+ });
400
+ const evalMid = makeEval("c-mid", async () => {
401
+ await sleep(30);
402
+ });
403
+ const evalFast = makeEval("c-fast", async () => {
404
+ await sleep(5);
405
+ });
406
+ const agentRun: AgentRun = {
407
+ agent: makeAgent("agent-concurrent"),
408
+ flags: {},
409
+ runs: 1,
410
+ earlyExit: true,
411
+ sandbox: fakeSandboxSpec(),
412
+ timeoutMs: 5_000,
413
+ evalFilter: () => true,
414
+ experimentId,
415
+ };
416
+ const completionOrder: string[] = [];
417
+ const orderReporter: Reporter = {
418
+ onEvalComplete(result) {
419
+ completionOrder.push(result.id);
420
+ },
421
+ };
422
+
423
+ const { summary, root, onEvalComplete, onEventComplete } = await run(
424
+ [evalSlow, evalMid, evalFast],
425
+ [agentRun],
426
+ { extraReporters: [{ reporter: orderReporter, name: "order", required: false }], maxConcurrency: 3 },
427
+ );
428
+
429
+ expect(summary.results).toHaveLength(3);
430
+ // sanity:确实是并发乱序完成,不是退化成串行(否则这条测试没有真正测到并发路径)。
431
+ expect(completionOrder.indexOf("c-fast")).toBeLessThan(completionOrder.indexOf("c-slow"));
432
+
433
+ const snapStartedAt = await diskSnapshotStartedAt(root, experimentId);
434
+ for (const evalId of ["c-slow", "c-mid", "c-fast"]) {
435
+ const key = `${experimentId}|${evalId}|0`;
436
+ const expected = encodeAttemptLocator({ experimentId, snapshotStartedAt: snapStartedAt, evalId, attempt: 0 });
437
+ expect(onEvalComplete.get(key)).toBe(expected);
438
+ expect(onEventComplete.get(key)).toBe(expected);
439
+ expect(await diskLocator(root, experimentId, evalId, 0)).toBe(expected);
440
+ }
441
+ });
442
+ });
443
+
444
+ // ───────────────────────── 反馈层永久事件集成测试 ─────────────────────────
445
+ // 驱动一个真实 FeedbackCoordinator(而不是手写的假 sink),覆盖 run.ts 是否真的把 failure /
446
+ // budget-exhausted 这两类永久事件送进去——而不是只在 renderer 单测里喂合成事件(见
447
+ // docs/feature/experiments/cli.md「什么动态更新,什么逐条追加」表的对应行)。
448
+
449
+ /** 建一个真实 coordinator,跑完 fn 后无条件 finish() 收尾——保证测试之间不会因为忘记退出
450
+ * 而互相污染 sink.ts 的活跃栈(与 report.test.ts 的 withFakeSink 同一个目的)。 */
451
+ async function withCoordinator<T>(
452
+ plan: RunFeedbackPlan,
453
+ fn: (coordinator: FeedbackCoordinator) => Promise<T>,
454
+ ): Promise<T> {
455
+ const fakeIO = createFakeFeedbackIO();
456
+ const coordinator = createFeedbackCoordinator({ profile: "ci", renderer: { appendDurable() {} }, io: fakeIO.io });
457
+ coordinator.start(plan);
458
+ try {
459
+ return await fn(coordinator);
460
+ } finally {
461
+ await coordinator.finish({
462
+ summary: { agent: "", startedAt: "", completedAt: "", passed: 0, failed: 0, skipped: 0, errored: 0, durationMs: 0, results: [] },
463
+ completion: { status: "complete", unstarted: 0, earlyExitUnstarted: 0, reporterErrors: [] },
464
+ paths: [],
465
+ });
466
+ }
467
+ }
468
+
469
+ describe("runEvals · failure 永久事件在真实失败/errored attempt 上被发出", () => {
470
+ afterEach(() => {
471
+ expect(activeFeedbackSinkCount()).toBe(0);
472
+ });
473
+
474
+ it("errored 与 failed 各触发一次、locator 与落盘结果一致,passed 不触发", async () => {
475
+ const experimentId = "failure-exp";
476
+ const evalOk = makeEval("ok", () => {});
477
+ const evalErrored = makeEval("boom", () => {
478
+ throw new Error("boom");
479
+ });
480
+ const evalFailed = makeEval("gate-fail", (t) => {
481
+ t.check("actual", equals("expected"));
482
+ });
483
+ const agentRun: AgentRun = {
484
+ agent: makeAgent("agent-a"),
485
+ flags: {},
486
+ runs: 1,
487
+ earlyExit: true,
488
+ sandbox: fakeSandboxSpec(),
489
+ timeoutMs: 5_000,
490
+ evalFilter: () => true,
491
+ experimentId,
492
+ };
493
+ const plan: RunFeedbackPlan = {
494
+ shape: { evals: 3, configs: 1, totalRuns: 3, maxConcurrency: 3 },
495
+ reused: 0,
496
+ reusedFailures: [],
497
+ };
498
+
499
+ await withCoordinator(plan, async (coordinator) => {
500
+ const { summary } = await run([evalOk, evalErrored, evalFailed], [agentRun]);
501
+
502
+ expect(summary.results).toHaveLength(3);
503
+ expect(summary.results.find((r) => r.id === "ok")!.verdict).toBe("passed");
504
+
505
+ // passed 不产出 failure 通知:只有 errored + failed 两条。
506
+ expect(coordinator.state.failures).toHaveLength(2);
507
+ const byLocator = new Map(coordinator.state.failures.map((f) => [String(f.locator), f]));
508
+
509
+ const erroredResult = summary.results.find((r) => r.id === "boom")!;
510
+ const erroredNotice = byLocator.get(String(erroredResult.locator));
511
+ expect(erroredNotice).toBeDefined();
512
+ expect(erroredNotice).toMatchObject({
513
+ verdict: "errored",
514
+ who: experimentId, // runWho():有 experimentId 时用它的最后一段(这里没有 "/",就是整段)
515
+ identity: { experimentId, evalId: "boom", attempt: 0 },
516
+ // evalDef.test() 抛的是普通 Error;即使 attempt 随后仍进入 diff/scoring,永久错误通知
517
+ // 也必须使用结构化 error.phase,报告错误真正发生的 eval.run,而不是最后经过的阶段。
518
+ phase: "eval.run",
519
+ });
520
+ expect(erroredNotice?.reason).toContain("boom");
521
+
522
+ const failedResult = summary.results.find((r) => r.id === "gate-fail")!;
523
+ const failedNotice = byLocator.get(String(failedResult.locator));
524
+ expect(failedNotice).toBeDefined();
525
+ expect(failedNotice).toMatchObject({
526
+ verdict: "failed",
527
+ identity: { experimentId, evalId: "gate-fail", attempt: 0 },
528
+ assertion: {
529
+ severity: "gate",
530
+ assertion: 'equals("expected")',
531
+ expected: '"expected"',
532
+ received: "actual",
533
+ additionalFailures: 0,
534
+ },
535
+ });
536
+ // failed 是断言 outcome,不是 lifecycle error;即使 verdict 在 scoring 阶段算出,也不应
537
+ // 把 scoring(更不能把随后可能发生的 telemetry.collect)冒充成「失败发生阶段」。
538
+ expect(failedNotice).not.toHaveProperty("phase");
539
+ });
540
+ });
541
+ });
542
+
543
+ describe("runEvals · budget-exhausted 永久事件按每个被跳过的 attempt 逐条发出", () => {
544
+ afterEach(() => {
545
+ expect(activeFeedbackSinkCount()).toBe(0);
546
+ });
547
+
548
+ it("budget=0 时三个 attempt 全部因预算到顶未派发,queued/completed 与去重诊断 count 都正确折算", async () => {
549
+ const experimentId = "budget-exp";
550
+ const evals = ["a", "b", "c"].map((id) => makeEval(id, () => {}));
551
+ const agentRun: AgentRun = {
552
+ agent: makeAgent("agent-budget"),
553
+ flags: {},
554
+ runs: 1,
555
+ earlyExit: false,
556
+ sandbox: fakeSandboxSpec(),
557
+ timeoutMs: 5_000,
558
+ evalFilter: () => true,
559
+ experimentId,
560
+ budget: 0, // 花费从 0 起算,>= budget 恒成立——每个 attempt 在 preflight 就被跳过。
561
+ };
562
+ const plan: RunFeedbackPlan = {
563
+ shape: { evals: 3, configs: 1, totalRuns: 3, maxConcurrency: 3 },
564
+ reused: 0,
565
+ reusedFailures: [],
566
+ };
567
+
568
+ await withCoordinator(plan, async (coordinator) => {
569
+ const { summary } = await run(evals, [agentRun]);
570
+
571
+ expect(summary.results).toHaveLength(0); // 全部未派发,没有一条真正跑过
572
+
573
+ const diag = coordinator.state.diagnostics.find((d) => d.key === `budget-exhausted:${experimentId}`);
574
+ expect(diag).toBeDefined();
575
+ expect(diag?.count).toBe(3); // 三个 attempt 各发一次,去重折成同一个 key、count 累加到 3
576
+ expect(diag?.data).toMatchObject({ experimentId, spent: 0, unstarted: 3 });
577
+
578
+ // reducer 不变量:每条 budget-exhausted 把一个 attempt 从 queued 挪进 completed
579
+ // (与 assembleRunCompletion() 读取 count 折算 RunCompletion.unstarted 的口径一致)。
580
+ expect(coordinator.state).toMatchObject({ total: 3, reused: 0, running: 0, queued: 0, completed: 3 });
581
+ });
582
+ });
583
+ });
584
+
585
+ // bug: memory/budget-warning-requires-agent-turn.md
586
+ describe("runEvals · budget-unenforceable 只统计真正发起过 agent turn 的 attempt", () => {
587
+ afterEach(() => {
588
+ expect(activeFeedbackSinkCount()).toBe(0);
589
+ });
590
+
591
+ it("三个 attempt 都在 sandbox.create 失败时只保留根因,不误报 budget-unenforceable", async () => {
592
+ const experimentId = "template-missing-exp";
593
+ const evals = ["a", "b", "c"].map((id) => makeEval(id, () => {}));
594
+ const missingTemplate = defineSandbox({
595
+ name: "missing-template",
596
+ create: async () => {
597
+ throw new Error("404: template 'memory-evals-claude-mempal-deadbeef-0-9-0' not found");
598
+ },
599
+ });
600
+ const agentRun: AgentRun = {
601
+ agent: makeAgent("agent-budget"),
602
+ flags: {},
603
+ runs: 1,
604
+ earlyExit: false,
605
+ sandbox: missingTemplate,
606
+ timeoutMs: 5_000,
607
+ evalFilter: () => true,
608
+ experimentId,
609
+ budget: 10,
610
+ };
611
+ const plan: RunFeedbackPlan = {
612
+ shape: { evals: 3, configs: 1, totalRuns: 3, maxConcurrency: 3 },
613
+ reused: 0,
614
+ reusedFailures: [],
615
+ };
616
+
617
+ await withCoordinator(plan, async (coordinator) => {
618
+ const { summary } = await run(evals, [agentRun]);
619
+
620
+ expect(summary.results).toHaveLength(3);
621
+ expect(summary.results.every((result) => result.error?.phase === "sandbox.create")).toBe(true);
622
+ expect(summary.results.every((result) => result.error?.message.includes("template") === true)).toBe(true);
623
+ expect(coordinator.state.failures).toHaveLength(3);
624
+ expect(coordinator.state.diagnostics.some((d) => d.key === `budget-unenforceable:${experimentId}`)).toBe(false);
625
+ });
626
+ });
627
+
628
+ it("三个 agent turn 都没有成本数据时仍只报一次 budget-unenforceable", async () => {
629
+ const experimentId = "missing-cost-exp";
630
+ const evals = ["a", "b", "c"].map((id) => makeEval(id, async (t) => {
631
+ await t.send("hello");
632
+ }));
633
+ const agentRun: AgentRun = {
634
+ agent: makeAgent("agent-budget"),
635
+ flags: {},
636
+ runs: 1,
637
+ earlyExit: false,
638
+ sandbox: fakeSandboxSpec(),
639
+ timeoutMs: 5_000,
640
+ evalFilter: () => true,
641
+ experimentId,
642
+ budget: 10,
643
+ };
644
+ const plan: RunFeedbackPlan = {
645
+ shape: { evals: 3, configs: 1, totalRuns: 3, maxConcurrency: 3 },
646
+ reused: 0,
647
+ reusedFailures: [],
648
+ };
649
+
650
+ await withCoordinator(plan, async (coordinator) => {
651
+ const { summary } = await run(evals, [agentRun]);
652
+
653
+ expect(summary.results).toHaveLength(3);
654
+ expect(summary.results.every((result) => result.phases?.some(
655
+ (phase) => phase.children?.some((child) => child.kind === "turn"),
656
+ ) === true)).toBe(true);
657
+ const diagnostics = coordinator.state.diagnostics.filter((d) => d.key === `budget-unenforceable:${experimentId}`);
658
+ expect(diagnostics).toHaveLength(1);
659
+ expect(diagnostics[0]?.count).toBe(1);
660
+ });
661
+ });
662
+ });
663
+
664
+ // 携入数量不足以覆盖本次请求的 runs(典型触发:上次 runs:1 落了 1 条终态结果,这次把 runs
665
+ // 调大到 3、没有 --force)时,差额必须真正计入调度,不能被 priorRunKeys 的"这个组合有过携入"
666
+ // 整段跳过——那会让 pass@N 的 N 被携入悄悄砍短,运行还照样报 PASSED/exit 0(见
667
+ // docs/runner.md「不能在 CI 里伪装成全绿」)。
668
+ describe("runEvals · 携入数量少于本次请求的 runs 时,差额必须真正计入调度", () => {
669
+ afterEach(() => {
670
+ expect(activeFeedbackSinkCount()).toBe(0);
671
+ });
672
+
673
+ it("携入 1 条 passed、runs 从 1 调到 3:差额通过 earlyExit 早退回填计入 completed,不真的重跑、也不被静默丢弃", async () => {
674
+ const experimentId = "carry-grow-passed-exp";
675
+ const evalId = "grown-eval";
676
+ const staleLocator = encodeAttemptLocator({
677
+ experimentId,
678
+ snapshotStartedAt: "2020-01-01T00:00:00.000Z",
679
+ evalId,
680
+ attempt: 0,
681
+ });
682
+ const carried: EvalResult = {
683
+ id: evalId,
684
+ experimentId,
685
+ agent: "agent-grow",
686
+ verdict: "passed",
687
+ attempt: 0,
688
+ startedAt: "2020-01-01T00:00:00.000Z",
689
+ durationMs: 1,
690
+ assertions: [],
691
+ locator: staleLocator,
692
+ artifactBase: `${experimentId}/some-old-snapshot/${evalId}/a0`,
693
+ };
694
+ // 差额 attempt 如果真的被调度执行,这里会抛错——用它检测「有没有因为回填而多花一次 agent
695
+ // 成本」。earlyExit 下携入的 passed 应该让回填直接早退,不应该走到这里。
696
+ const evalDef = makeEval(evalId, () => {
697
+ throw new Error("backfilled attempt should have been early-exited, not actually run");
698
+ });
699
+ const agentRun: AgentRun = {
700
+ agent: makeAgent("agent-grow"),
701
+ flags: {},
702
+ runs: 3, // 上次只留 1 条(runs:1 时代的携入),这次调大
703
+ earlyExit: true,
704
+ sandbox: fakeSandboxSpec(),
705
+ timeoutMs: 5_000,
706
+ evalFilter: () => true,
707
+ experimentId,
708
+ };
709
+ const plan: RunFeedbackPlan = {
710
+ shape: { evals: 1, configs: 1, totalRuns: 3, maxConcurrency: 3 },
711
+ reused: 1,
712
+ reusedFailures: [],
713
+ };
714
+
715
+ await withCoordinator(plan, async (coordinator) => {
716
+ const { summary } = await run([evalDef], [agentRun], {
717
+ carryPlan: {
718
+ plannedFingerprints: new Map(),
719
+ priorRunKeys: new Set([`${experimentId}|${evalId}`]),
720
+ carriedResults: [carried],
721
+ },
722
+ });
723
+
724
+ const matches = summary.results.filter((r) => r.id === evalId);
725
+ expect(matches).toHaveLength(1); // 没有因为回填而多出真实结果
726
+ expect(matches[0]!.verdict).toBe("passed");
727
+ expect(matches[0]!.locator).toBe(staleLocator); // 携入结果原样透传
728
+
729
+ // 不变量:携入 1 + early-exit 回填 2 == 本次请求的 runs:3,不留没有解释的差额
730
+ // (queued 必须真正归零,不能停在「还差 2 个不知道去哪」)。
731
+ expect(coordinator.state).toMatchObject({ total: 3, reused: 1, running: 0, queued: 0, completed: 2 });
732
+ });
733
+ });
734
+
735
+ it("携入 1 条 failed、runs 从 1 调到 3:failed 不触发 earlyExit,差额两次必须真的重新调度", async () => {
736
+ const experimentId = "carry-grow-failed-exp";
737
+ const evalId = "grown-failed-eval";
738
+ const staleLocator = encodeAttemptLocator({
739
+ experimentId,
740
+ snapshotStartedAt: "2020-01-01T00:00:00.000Z",
741
+ evalId,
742
+ attempt: 0,
743
+ });
744
+ const carried: EvalResult = {
745
+ id: evalId,
746
+ experimentId,
747
+ agent: "agent-grow-failed",
748
+ verdict: "failed",
749
+ attempt: 0,
750
+ startedAt: "2020-01-01T00:00:00.000Z",
751
+ durationMs: 1,
752
+ assertions: [],
753
+ locator: staleLocator,
754
+ artifactBase: `${experimentId}/some-old-snapshot/${evalId}/a0`,
755
+ };
756
+ let calls = 0;
757
+ // 恒定 gate 失败(而不是恒定通过):避免回填的第一次真的跑出 passed 后,靠"这次跑出来的
758
+ // passed"触发 earlyExit 把第二次也提前吞掉——那样测不出"差额是不是真的被调度"这件事本身。
759
+ const evalDef = makeEval(evalId, (t) => {
760
+ calls += 1;
761
+ t.check("actual", equals("expected"));
762
+ });
763
+ const agentRun: AgentRun = {
764
+ agent: makeAgent("agent-grow-failed"),
765
+ flags: {},
766
+ runs: 3,
767
+ earlyExit: true, // failed 不触发 earlyExit(只有 passed/errored 会),回填的两次应该真的跑
768
+ sandbox: fakeSandboxSpec(),
769
+ timeoutMs: 5_000,
770
+ evalFilter: () => true,
771
+ experimentId,
772
+ };
773
+ const plan: RunFeedbackPlan = {
774
+ shape: { evals: 1, configs: 1, totalRuns: 3, maxConcurrency: 3 },
775
+ reused: 1,
776
+ reusedFailures: [{
777
+ locator: staleLocator,
778
+ identity: { experimentId, evalId, attempt: 0 },
779
+ who: `${experimentId}/agent-grow-failed`,
780
+ verdict: "failed",
781
+ reason: "failed",
782
+ }],
783
+ };
784
+
785
+ await withCoordinator(plan, async (coordinator) => {
786
+ const { summary } = await run([evalDef], [agentRun], {
787
+ carryPlan: {
788
+ plannedFingerprints: new Map(),
789
+ priorRunKeys: new Set([`${experimentId}|${evalId}`]),
790
+ carriedResults: [carried],
791
+ },
792
+ });
793
+
794
+ expect(calls).toBe(2); // 差额的两次真的执行了 agent,不是被携入悄悄吞掉
795
+ const matches = summary.results.filter((r) => r.id === evalId);
796
+ expect(matches).toHaveLength(3); // 1 携入 + 2 新跑,凑满本次请求的 runs:3
797
+ expect(matches.map((r) => r.attempt).sort()).toEqual([0, 1, 2]);
798
+ expect(matches.every((r) => r.verdict === "failed")).toBe(true);
799
+
800
+ expect(coordinator.state).toMatchObject({ total: 3, reused: 1, running: 0, queued: 0, completed: 2 });
801
+ // RunSummary 的三条 failed(1 carry + 2 fresh)与终局 handoff 的 FailureNotice 清单同口径。
802
+ // carry 不能只进 summary 计数而从 FAILURES / agent handoff 消失。
803
+ expect(coordinator.state.failures).toHaveLength(3);
804
+ expect(coordinator.state.failures.map((failure) => failure.locator)).toContain(staleLocator);
805
+ });
806
+ });
807
+ });