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
@@ -0,0 +1,55 @@
1
+ // cases: docs/engineering/unit-tests/sandbox/cases.md
2
+ import { describe, expect, it } from "vitest";
3
+ import { createCheckpoint, restoreCheckpoint } from "./checkpoint.ts";
4
+ import type { Sandbox } from "../types.ts";
5
+
6
+ function sandbox(overrides: Partial<Sandbox> = {}): Sandbox {
7
+ return {
8
+ workdir: "/work",
9
+ sandboxId: "fake",
10
+ otlpHost: null,
11
+ runCommand: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
12
+ runShell: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
13
+ readFile: async () => "",
14
+ fileExists: async () => false,
15
+ readSourceFiles: async () => Object.assign([], {
16
+ text: () => "", code: () => "", fileMatching: () => undefined,
17
+ fileMatchingAll: () => undefined, hasPath: () => false,
18
+ }),
19
+ writeFiles: async () => {},
20
+ uploadFiles: async () => {},
21
+ uploadDirectory: async () => {},
22
+ stop: async () => {},
23
+ downloadFile: async () => Buffer.from("archive"),
24
+ uploadFile: async () => {},
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ describe("sandbox checkpoints", () => {
30
+ it("fails when archive creation fails instead of downloading a bogus checkpoint", async () => {
31
+ let downloaded = false;
32
+ const sb = sandbox({
33
+ runShell: async (script) => script.startsWith("tar ")
34
+ ? { stdout: "", stderr: "permission denied", exitCode: 2 }
35
+ : { stdout: "", stderr: "", exitCode: 0 },
36
+ downloadFile: async () => { downloaded = true; return Buffer.from("bad"); },
37
+ });
38
+ await expect(createCheckpoint(sb, ["/cache"])).rejects.toThrow("checkpoint archive failed");
39
+ expect(downloaded).toBe(false);
40
+ });
41
+
42
+ it("fails when restore tar fails even though cleanup succeeds", async () => {
43
+ const calls: string[] = [];
44
+ const sb = sandbox({
45
+ runShell: async (script) => {
46
+ calls.push(script);
47
+ return script.startsWith("tar ")
48
+ ? { stdout: "", stderr: "invalid gzip", exitCode: 2 }
49
+ : { stdout: "", stderr: "", exitCode: 0 };
50
+ },
51
+ });
52
+ await expect(restoreCheckpoint(sb, Buffer.from("bad"))).rejects.toThrow("checkpoint restore failed");
53
+ expect(calls.some((call) => call.startsWith("rm -f "))).toBe(true);
54
+ });
55
+ });
@@ -22,20 +22,41 @@ function tmpTarPath(tag: string): string {
22
22
 
23
23
  /** 把 paths 列出的目录打成 gzip tar,返回 Buffer。 */
24
24
  export async function createCheckpoint(sb: Sandbox, paths: string[]): Promise<Buffer> {
25
+ if (paths.length === 0) throw new Error(t("checkpoint.emptyTar", { paths: "(none)" }));
25
26
  const tmp = tmpTarPath("cp");
26
27
  const quoted = paths.map(shellQuote).join(" ");
27
- // --ignore-failed-read:跳过不存在的路径(如 .cache/uv 可能未建);true 保证 exit 0。
28
- await sb.runShell(`tar czf ${tmp} --ignore-failed-read ${quoted} 2>/dev/null; true`);
29
- const buf = await sb.downloadFile(tmp);
30
- await sb.runShell(`rm -f ${tmp}`);
31
- if (!buf || buf.length === 0) throw new Error(t("checkpoint.emptyTar", { paths: paths.join(", ") }));
32
- return buf;
28
+ try {
29
+ // --ignore-failed-read 允许某个可选路径不存在,但其它 tar 错误必须显式失败。
30
+ const packed = await sb.runShell(`tar czf ${tmp} --ignore-failed-read ${quoted}`);
31
+ if (packed.exitCode !== 0) {
32
+ throw new Error(t("checkpoint.archiveFailed", {
33
+ exitCode: packed.exitCode,
34
+ detail: (packed.stderr || packed.stdout).trim() || "no output",
35
+ }));
36
+ }
37
+ const buf = await sb.downloadFile(tmp);
38
+ if (!buf || buf.length === 0) throw new Error(t("checkpoint.emptyTar", { paths: paths.join(", ") }));
39
+ return buf;
40
+ } finally {
41
+ await sb.runShell(`rm -f ${tmp}`).catch(() => undefined);
42
+ }
33
43
  }
34
44
 
35
45
  /** 把 createCheckpoint 返回的 Buffer 还原到沙箱根目录。 */
36
46
  export async function restoreCheckpoint(sb: Sandbox, data: Buffer): Promise<void> {
37
47
  const tmp = tmpTarPath("rs");
38
48
  await sb.uploadFile(tmp, data);
39
- // -C / 解压到根目录,覆盖同路径文件;rm 用 ; 保证无论成败都清理临时文件。
40
- await sb.runShell(`tar xzf ${tmp} -C / 2>/dev/null; rm -f ${tmp}`);
49
+ try {
50
+ // -C / 解压到根目录并覆盖同路径文件。不能把 rm 拼在同一条脚本末尾,否则 rm 的
51
+ // exit 0 会掩盖 tar 解压失败。
52
+ const restored = await sb.runShell(`tar xzf ${tmp} -C /`);
53
+ if (restored.exitCode !== 0) {
54
+ throw new Error(t("checkpoint.restoreFailed", {
55
+ exitCode: restored.exitCode,
56
+ detail: (restored.stderr || restored.stdout).trim() || "no output",
57
+ }));
58
+ }
59
+ } finally {
60
+ await sb.runShell(`rm -f ${tmp}`).catch(() => undefined);
61
+ }
41
62
  }
@@ -0,0 +1,407 @@
1
+ // `niceeval sandbox` 命令组:查看与销毁留存的沙箱(见 docs/feature/sandbox/cli.md)。
2
+ // 不读 niceeval.config.ts、不发现 eval,只操作留存注册表(.niceeval/sandboxes/ 逐条目文件)
3
+ // 与内置 provider 的 detached 能力;provider 名的路由发生在 CLI / 注册表边界(sandbox/ 域内)。
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { hostname } from "node:os";
7
+ import { resolve } from "node:path";
8
+ import {
9
+ destroyDetached,
10
+ inspectDetached,
11
+ suspendDetached,
12
+ wakeDetached,
13
+ } from "./keep.ts";
14
+ import {
15
+ findNiceevalRoot,
16
+ keptEntryId,
17
+ readKeptEntries,
18
+ removeKeptEntry,
19
+ updateKeptEntry,
20
+ type KeptSandboxEntry,
21
+ } from "./keep-registry.ts";
22
+
23
+ export interface SandboxCommandFlags {
24
+ all?: boolean;
25
+ window?: string;
26
+ path?: string;
27
+ leaveRunning?: boolean;
28
+ /** 仓库外执行时显式指定结果根(.niceeval 或其父目录)。 */
29
+ run?: string;
30
+ }
31
+
32
+ interface Io {
33
+ out(text: string): void;
34
+ err(text: string): void;
35
+ }
36
+
37
+ const LEASE_TTL_MS = 60 * 60 * 1000;
38
+
39
+ /** 入口:`niceeval sandbox <list|enter|history|diff|stop> …`;返回退出码。 */
40
+ export async function runSandboxCommand(
41
+ cwd: string,
42
+ positionals: string[],
43
+ flags: SandboxCommandFlags,
44
+ io: Io = { out: (s) => process.stdout.write(s), err: (s) => process.stderr.write(s) },
45
+ ): Promise<number> {
46
+ const sub = positionals[0];
47
+ const root = await resolveRegistryRoot(cwd, flags.run);
48
+ if (root === undefined) {
49
+ io.err(
50
+ `No .niceeval directory found from ${cwd} upward. Run this inside the project, or pass --results <results-root> to point at it.\n`,
51
+ );
52
+ return 1;
53
+ }
54
+ switch (sub) {
55
+ case "list":
56
+ return listCommand(root, io);
57
+ case "stop":
58
+ return stopCommand(root, positionals.slice(1), flags, io);
59
+ case "enter":
60
+ return enterCommand(root, positionals.slice(1), flags, io);
61
+ case "history":
62
+ return historyCommand(root, positionals.slice(1), flags, io);
63
+ case "diff":
64
+ return diffCommand(root, positionals.slice(1), flags, io);
65
+ default:
66
+ io.err(`usage: niceeval sandbox <list|enter|history|diff|stop> …\n`);
67
+ return 1;
68
+ }
69
+ }
70
+
71
+ async function resolveRegistryRoot(cwd: string, runFlag: string | undefined): Promise<string | undefined> {
72
+ if (runFlag !== undefined) {
73
+ const base = resolve(cwd, runFlag);
74
+ // --results 可以指 .niceeval 本身或它的父目录。
75
+ return base.endsWith(".niceeval") ? base : `${base}/.niceeval`;
76
+ }
77
+ return findNiceevalRoot(cwd);
78
+ }
79
+
80
+ /** 留存注册表里还有条目时,`niceeval exp` 启动打的一行提醒(不阻塞、不清理)。 */
81
+ export async function keptSandboxReminder(cwd: string): Promise<string | undefined> {
82
+ const root = await findNiceevalRoot(cwd);
83
+ if (!root) return undefined;
84
+ const { entries } = await readKeptEntries(root);
85
+ if (entries.length === 0) return undefined;
86
+ return `${entries.length} kept sandbox${entries.length === 1 ? "" : "es"} from earlier runs — niceeval sandbox list\n`;
87
+ }
88
+
89
+ async function listCommand(root: string, io: Io): Promise<number> {
90
+ const { entries } = await readKeptEntries(root);
91
+ if (entries.length === 0) {
92
+ io.out("No kept sandboxes.\n");
93
+ return 0;
94
+ }
95
+ io.out(`ID PROVIDER STATE FROM\n`);
96
+ for (const { id, entry } of entries) {
97
+ // STATE 是当下核对的现场状态,不是登记时的旧值。
98
+ const state = await inspectDetached(entry.provider, entry.sandboxId);
99
+ if (state !== entry.state) await updateKeptEntry(root, id, { state }).catch(() => false);
100
+ const from = `${entry.evalId} #${entry.attempt} · ${entry.verdict} · ${entry.locator} · ${formatWhen(entry.keptAt)}`;
101
+ io.out(`${id.padEnd(10)}${entry.provider.padEnd(10)}${state.padEnd(17)}${from}\n`);
102
+ if (state === "expired") {
103
+ const when = entry.expiresAt !== undefined ? `expired ${formatWhen(entry.expiresAt)} — ` : "";
104
+ io.out(` ${when}remove with: niceeval sandbox stop ${id}\n`);
105
+ } else {
106
+ io.out(` enter: niceeval sandbox enter ${id}\n`);
107
+ }
108
+ }
109
+ return 0;
110
+ }
111
+
112
+ /** id 接受 entry id / 实例 id 的唯一前缀;有歧义或不在注册表里时报错并列出候选。 */
113
+ async function resolveEntries(
114
+ root: string,
115
+ ids: string[],
116
+ io: Io,
117
+ ): Promise<{ id: string; entry: KeptSandboxEntry }[] | undefined> {
118
+ const { entries } = await readKeptEntries(root);
119
+ const resolved: { id: string; entry: KeptSandboxEntry }[] = [];
120
+ for (const raw of ids) {
121
+ const hits = entries.filter(
122
+ ({ id, entry }) => id === raw || id.startsWith(raw) || entry.sandboxId === raw || entry.sandboxId.startsWith(raw),
123
+ );
124
+ if (hits.length === 1) {
125
+ resolved.push(hits[0]!);
126
+ continue;
127
+ }
128
+ if (hits.length === 0) {
129
+ io.err(`"${raw}" is not in the kept-sandbox registry. Known: ${entries.map((e) => e.id).join(", ") || "(none)"}\n`);
130
+ } else {
131
+ io.err(`"${raw}" is ambiguous; candidates: ${hits.map((h) => `${h.id} (${h.entry.sandboxId})`).join(", ")}\n`);
132
+ }
133
+ return undefined;
134
+ }
135
+ return resolved;
136
+ }
137
+
138
+ function leaseHolder(): string {
139
+ return `${process.pid}@${hostname()}`;
140
+ }
141
+
142
+ function leaseActive(entry: KeptSandboxEntry): boolean {
143
+ if (!entry.lease) return false;
144
+ return Date.now() - Date.parse(entry.lease.acquiredAt) < entry.lease.ttlMs;
145
+ }
146
+
147
+ async function stopCommand(root: string, ids: string[], flags: SandboxCommandFlags, io: Io): Promise<number> {
148
+ let targets: { id: string; entry: KeptSandboxEntry }[];
149
+ if (flags.all) {
150
+ targets = (await readKeptEntries(root)).entries;
151
+ } else if (ids.length === 0) {
152
+ io.err("specify sandbox ids or --all\n");
153
+ return 1;
154
+ } else {
155
+ const resolved = await resolveEntries(root, ids, io);
156
+ if (!resolved) return 1;
157
+ targets = resolved;
158
+ }
159
+
160
+ let code = 0;
161
+ for (const { id, entry } of targets) {
162
+ if (leaseActive(entry)) {
163
+ io.err(`${entry.sandboxId} (${entry.provider}) is in use by ${entry.lease!.holder} since ${entry.lease!.acquiredAt}; not stopping.\n`);
164
+ code = 1;
165
+ continue;
166
+ }
167
+ try {
168
+ const outcome = await destroyDetached(entry.provider, entry.sandboxId);
169
+ await removeKeptEntry(root, id);
170
+ if (outcome === "stopped") io.out(`stopped ${entry.sandboxId} (${entry.provider})\n`);
171
+ else io.out(`${entry.sandboxId} (${entry.provider}) already gone — removed from registry\n`);
172
+ } catch (e) {
173
+ // 只有实例成功销毁或确认已不存在时才移除登记项;其它错误保留条目并退出 1,
174
+ // 不能把仍活着的资源从管理面隐藏掉。
175
+ io.err(`failed to stop ${entry.sandboxId} (${entry.provider}): ${e instanceof Error ? e.message : String(e)}\n`);
176
+ code = 1;
177
+ }
178
+ }
179
+ return code;
180
+ }
181
+
182
+ async function withLease<T>(
183
+ root: string,
184
+ id: string,
185
+ entry: KeptSandboxEntry,
186
+ op: string,
187
+ io: Io,
188
+ fn: () => Promise<T>,
189
+ ): Promise<T | undefined> {
190
+ if (leaseActive(entry) && entry.lease!.holder !== leaseHolder()) {
191
+ io.err(`${entry.sandboxId} is in use by ${entry.lease!.holder} since ${entry.lease!.acquiredAt}\n`);
192
+ return undefined;
193
+ }
194
+ if (entry.lease && !leaseActive(entry)) {
195
+ io.err(`taking over an expired lease from ${entry.lease.holder} (acquired ${entry.lease.acquiredAt})\n`);
196
+ }
197
+ await updateKeptEntry(root, id, {
198
+ lease: { holder: leaseHolder(), op, acquiredAt: new Date().toISOString(), ttlMs: LEASE_TTL_MS },
199
+ });
200
+ try {
201
+ return await fn();
202
+ } finally {
203
+ await updateKeptEntry(root, id, (e) => {
204
+ const { lease, ...rest } = e;
205
+ void lease;
206
+ return rest as KeptSandboxEntry;
207
+ }).catch(() => false);
208
+ }
209
+ }
210
+
211
+ async function enterCommand(root: string, ids: string[], flags: SandboxCommandFlags, io: Io): Promise<number> {
212
+ const resolved = await resolveEntries(root, ids.slice(0, 1), io);
213
+ if (!resolved || resolved.length === 0) {
214
+ if (ids.length === 0) io.err("usage: niceeval sandbox enter <id> [--leave-running]\n");
215
+ return 1;
216
+ }
217
+ const { id, entry } = resolved[0]!;
218
+ const state = await inspectDetached(entry.provider, entry.sandboxId);
219
+ if (state === "expired") {
220
+ io.err(`${entry.sandboxId} (${entry.provider}) is gone — the instance no longer exists. Clean up with: niceeval sandbox stop ${id}\n`);
221
+ return 1;
222
+ }
223
+ if (entry.provider !== "docker") {
224
+ // 云 provider 的交互式 shell 走各家原生 CLI(注册表里的 enter 命令);niceeval 不复刻各家 PTY。
225
+ io.err(
226
+ `interactive enter is currently only implemented for docker; use the provider-native command instead${entry.enter ? `:\n ${entry.enter}` : "."}\n`,
227
+ );
228
+ return 1;
229
+ }
230
+
231
+ const result = await withLease(root, id, entry, "enter", io, async () => {
232
+ await wakeDetached(entry.provider, entry.sandboxId);
233
+ await updateKeptEntry(root, id, { state: "alive" });
234
+ const code = await interactiveDockerShell(entry.sandboxId, entry.workdir);
235
+ if (flags.leaveRunning) {
236
+ await updateKeptEntry(root, id, { state: "alive" });
237
+ io.out(`left running: ${entry.sandboxId} (re-suspend with another enter, or destroy with niceeval sandbox stop ${id})\n`);
238
+ return code;
239
+ }
240
+ // shell 退出(含 Ctrl+C)后自动送回休眠——「休眠不烧资源」不因进去看过一眼失效。
241
+ try {
242
+ await suspendDetached(entry.provider, entry.sandboxId);
243
+ await updateKeptEntry(root, id, { state: "dormant" });
244
+ } catch (e) {
245
+ io.err(`failed to re-suspend ${entry.sandboxId}: ${e instanceof Error ? e.message : String(e)}\n`);
246
+ await updateKeptEntry(root, id, { state: "alive" });
247
+ }
248
+ return code;
249
+ });
250
+ return result ?? 1;
251
+ }
252
+
253
+ function interactiveDockerShell(containerId: string, workdir: string): Promise<number> {
254
+ return new Promise((resolvePromise) => {
255
+ const child = spawn("docker", ["exec", "-it", "-w", workdir, containerId, "bash", "-l"], {
256
+ stdio: "inherit",
257
+ });
258
+ child.on("exit", (code) => resolvePromise(code ?? 0));
259
+ child.on("error", () => resolvePromise(1));
260
+ });
261
+ }
262
+
263
+ /** 在留存现场里跑一条命令(非交互;history/diff 用)。docker 走 exec;其它 provider 报不支持。 */
264
+ async function execInKept(entry: KeptSandboxEntry, script: string): Promise<string> {
265
+ if (entry.provider !== "docker") {
266
+ throw new Error(`replaying the ledger is currently only implemented for docker sandboxes (got ${entry.provider})`);
267
+ }
268
+ const { default: Docker } = await import("dockerode");
269
+ const container = new Docker().getContainer(entry.sandboxId);
270
+ const exec = await container.exec({
271
+ Cmd: ["sh", "-c", script],
272
+ AttachStdout: true,
273
+ AttachStderr: true,
274
+ Env: [
275
+ "GIT_DIR=/tmp/.niceeval-ledger",
276
+ `GIT_WORK_TREE=${entry.workdir}`,
277
+ "HOME=/tmp",
278
+ ],
279
+ });
280
+ const stream = await exec.start({});
281
+ return await new Promise<string>((resolvePromise, reject) => {
282
+ const chunks: Buffer[] = [];
283
+ stream.on("data", (chunk: Buffer) => chunks.push(chunk));
284
+ stream.on("end", () => {
285
+ // docker 多路复用帧:每帧 8 字节头(stream type + 长度),逐帧剥掉。
286
+ const raw = Buffer.concat(chunks);
287
+ let out = "";
288
+ let offset = 0;
289
+ while (offset + 8 <= raw.length) {
290
+ const size = raw.readUInt32BE(offset + 4);
291
+ out += raw.subarray(offset + 8, offset + 8 + size).toString("utf-8");
292
+ offset += 8 + size;
293
+ }
294
+ resolvePromise(out || raw.toString("utf-8"));
295
+ });
296
+ stream.on("error", reject);
297
+ });
298
+ }
299
+
300
+ /** 唤醒 → 读 → 送回休眠(现场休眠中同样可用;读完不留 alive)。 */
301
+ async function withWokenSandbox<T>(
302
+ root: string,
303
+ id: string,
304
+ entry: KeptSandboxEntry,
305
+ fn: () => Promise<T>,
306
+ ): Promise<T> {
307
+ const state = await inspectDetached(entry.provider, entry.sandboxId);
308
+ if (state === "expired") {
309
+ throw new Error(`${entry.sandboxId} (${entry.provider}) is gone; the in-sandbox ledger died with it (artifacts are unaffected). Clean up with: niceeval sandbox stop ${id}`);
310
+ }
311
+ const wasDormant = state === "dormant";
312
+ if (wasDormant) await wakeDetached(entry.provider, entry.sandboxId);
313
+ try {
314
+ return await fn();
315
+ } finally {
316
+ if (wasDormant) {
317
+ await suspendDetached(entry.provider, entry.sandboxId).catch(() => {});
318
+ }
319
+ }
320
+ }
321
+
322
+ async function historyCommand(root: string, ids: string[], _flags: SandboxCommandFlags, io: Io): Promise<number> {
323
+ const resolved = await resolveEntries(root, ids.slice(0, 1), io);
324
+ if (!resolved || resolved.length === 0) {
325
+ if (ids.length === 0) io.err("usage: niceeval sandbox history <id>\n");
326
+ return 1;
327
+ }
328
+ const { id, entry } = resolved[0]!;
329
+ try {
330
+ const out = await withWokenSandbox(root, id, entry, () =>
331
+ execInKept(entry, `git log --reverse --format='%at %s' 2>/dev/null`),
332
+ );
333
+ const lines = out.trim().split("\n").filter(Boolean);
334
+ if (lines.length === 0) {
335
+ io.out("(no ledger found in this sandbox)\n");
336
+ return 0;
337
+ }
338
+ for (const line of lines) {
339
+ const space = line.indexOf(" ");
340
+ const at = new Date(Number(line.slice(0, space)) * 1000);
341
+ const subject = line.slice(space + 1);
342
+ if (subject === "anchor") {
343
+ io.out(`anchor ${formatWhen(at.toISOString())}\n`);
344
+ } else if (subject.startsWith("eval ")) {
345
+ io.out(`eval (window ${subject.slice(5)} 之前的 fixture / setup / 校验写入)\n`);
346
+ } else if (subject.startsWith("agent ")) {
347
+ io.out(`${subject.slice(6).padEnd(8)}agent\n`);
348
+ } else {
349
+ io.out(`${subject}\n`);
350
+ }
351
+ }
352
+ return 0;
353
+ } catch (e) {
354
+ io.err(`${e instanceof Error ? e.message : String(e)}\n`);
355
+ return 1;
356
+ }
357
+ }
358
+
359
+ async function diffCommand(root: string, ids: string[], flags: SandboxCommandFlags, io: Io): Promise<number> {
360
+ const resolved = await resolveEntries(root, ids.slice(0, 1), io);
361
+ if (!resolved || resolved.length === 0) {
362
+ if (ids.length === 0) io.err("usage: niceeval sandbox diff <id> [--window s1/t2] [--path <file>]\n");
363
+ return 1;
364
+ }
365
+ const { id, entry } = resolved[0]!;
366
+ try {
367
+ const out = await withWokenSandbox(root, id, entry, async () => {
368
+ const log = await execInKept(entry, `git log --reverse --format='%H %s' 2>/dev/null`);
369
+ const commits = log
370
+ .trim()
371
+ .split("\n")
372
+ .filter(Boolean)
373
+ .map((line) => {
374
+ const space = line.indexOf(" ");
375
+ return { hash: line.slice(0, space), subject: line.slice(space + 1) };
376
+ })
377
+ .filter((c) => c.subject.startsWith("agent "));
378
+ const wanted = flags.window !== undefined ? commits.filter((c) => c.subject === `agent ${flags.window}`) : commits;
379
+ if (wanted.length === 0) {
380
+ return flags.window !== undefined
381
+ ? `window "${flags.window}" not found; windows: ${commits.map((c) => c.subject.slice(6)).join(", ") || "(none)"}\n`
382
+ : "(no agent windows in this ledger)\n";
383
+ }
384
+ const pathArg = flags.path !== undefined ? ` -- '${flags.path.replaceAll("'", `'\\''`)}'` : "";
385
+ const sections: string[] = [];
386
+ for (const c of wanted) {
387
+ const patch = await execInKept(entry, `git diff ${c.hash}^ ${c.hash}${pathArg}`);
388
+ sections.push(`── window ${c.subject.slice(6)}\n${patch.trimEnd()}`);
389
+ }
390
+ return sections.join("\n\n") + "\n";
391
+ });
392
+ io.out(out);
393
+ return 0;
394
+ } catch (e) {
395
+ io.err(`${e instanceof Error ? e.message : String(e)}\n`);
396
+ return 1;
397
+ }
398
+ }
399
+
400
+ function formatWhen(iso: string): string {
401
+ const d = new Date(iso);
402
+ if (Number.isNaN(d.getTime())) return iso;
403
+ const pad = (n: number) => String(n).padStart(2, "0");
404
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
405
+ }
406
+
407
+ export { keptEntryId };