niceeval 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/dist/agents/types.d.ts +67 -5
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
  9. package/dist/report/built-ins/experiment-comparison.js +116 -10
  10. package/dist/report/built-ins/index.d.ts +1 -0
  11. package/dist/report/built-ins/index.js +1 -1
  12. package/dist/report/components.d.ts +8 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +54 -34
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -8
  19. package/dist/report/format.js +27 -12
  20. package/dist/report/index.d.ts +4 -3
  21. package/dist/report/index.js +5 -4
  22. package/dist/report/locale.d.ts +11 -2
  23. package/dist/report/locale.js +23 -5
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +65 -14
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +2 -2
  28. package/dist/report/react/AttemptList.js +5 -6
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +2 -1
  34. package/dist/report/react/ExperimentList.js +8 -10
  35. package/dist/report/react/MetricScatter.js +5 -11
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +21 -14
  40. package/dist/report/report.d.ts +5 -1
  41. package/dist/report/report.js +6 -2
  42. package/dist/report/text/faces.d.ts +1 -1
  43. package/dist/report/text/faces.js +42 -41
  44. package/dist/report/text/table.js +36 -5
  45. package/dist/report/types.d.ts +39 -21
  46. package/dist/results/types.d.ts +11 -0
  47. package/dist/runner/feedback/sink.d.ts +110 -0
  48. package/dist/runner/types.d.ts +513 -22
  49. package/dist/sandbox/docker.d.ts +23 -2
  50. package/dist/sandbox/e2b.d.ts +15 -1
  51. package/dist/sandbox/errors.d.ts +30 -3
  52. package/dist/sandbox/io-retry.d.ts +17 -0
  53. package/dist/sandbox/registry.d.ts +2 -0
  54. package/dist/sandbox/resolve.d.ts +18 -5
  55. package/dist/sandbox/retry.d.ts +11 -1
  56. package/dist/sandbox/types.d.ts +39 -5
  57. package/dist/sandbox/vercel.d.ts +7 -1
  58. package/dist/scoring/coverage.d.ts +30 -0
  59. package/dist/scoring/display.d.ts +21 -0
  60. package/dist/scoring/display.js +120 -0
  61. package/dist/scoring/types.d.ts +103 -20
  62. package/dist/shared/aggregate.d.ts +1 -0
  63. package/dist/shared/aggregate.js +3 -3
  64. package/dist/shared/types.d.ts +28 -0
  65. package/dist/tty-line.d.ts +0 -4
  66. package/dist/util.d.ts +23 -0
  67. package/docs-site/zh/concepts/adapter.mdx +22 -4
  68. package/docs-site/zh/concepts/experiment.mdx +1 -1
  69. package/docs-site/zh/concepts/overview.mdx +6 -6
  70. package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
  71. package/docs-site/zh/guides/authoring.mdx +33 -0
  72. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  73. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  74. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  75. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  76. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  77. package/docs-site/zh/guides/debugging.mdx +210 -0
  78. package/docs-site/zh/guides/experiments.mdx +10 -3
  79. package/docs-site/zh/guides/official-adapters.mdx +26 -2
  80. package/docs-site/zh/guides/publish-report.mdx +30 -16
  81. package/docs-site/zh/guides/report-components.mdx +42 -30
  82. package/docs-site/zh/guides/reporters.mdx +2 -2
  83. package/docs-site/zh/guides/results-data.mdx +17 -9
  84. package/docs-site/zh/guides/runner.mdx +17 -7
  85. package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
  86. package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
  87. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  88. package/docs-site/zh/guides/viewing-results.mdx +79 -36
  89. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  90. package/docs-site/zh/guides/write-send.mdx +17 -1
  91. package/docs-site/zh/index.mdx +1 -1
  92. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  93. package/docs-site/zh/reference/capabilities.mdx +2 -2
  94. package/docs-site/zh/reference/cli.mdx +33 -7
  95. package/docs-site/zh/reference/define-agent.mdx +57 -4
  96. package/docs-site/zh/reference/define-config.mdx +1 -1
  97. package/docs-site/zh/reference/define-eval.mdx +42 -9
  98. package/docs-site/zh/reference/expect.mdx +26 -1
  99. package/package.json +5 -1
  100. package/src/agents/ai-sdk-otel.test.ts +1 -0
  101. package/src/agents/ai-sdk.test.ts +3 -0
  102. package/src/agents/ai-sdk.ts +3 -0
  103. package/src/agents/bub-install-spec.test.ts +34 -0
  104. package/src/agents/bub-install-spec.ts +32 -0
  105. package/src/agents/bub.ts +31 -32
  106. package/src/agents/claude-code.test.ts +130 -9
  107. package/src/agents/claude-code.ts +76 -4
  108. package/src/agents/codex.test.ts +189 -40
  109. package/src/agents/codex.ts +155 -14
  110. package/src/agents/coding-cli-versions.test.ts +15 -0
  111. package/src/agents/coding-cli-versions.ts +3 -0
  112. package/src/agents/index.ts +11 -0
  113. package/src/agents/langgraph.test.ts +204 -0
  114. package/src/agents/langgraph.ts +495 -0
  115. package/src/agents/marketplace.ts +85 -0
  116. package/src/agents/native-config.test.ts +179 -0
  117. package/src/agents/native-config.ts +267 -0
  118. package/src/agents/openai-compat.test.ts +1 -0
  119. package/src/agents/openclaw.test.ts +31 -0
  120. package/src/agents/openclaw.ts +171 -0
  121. package/src/agents/plugin-config.test.ts +1 -0
  122. package/src/agents/sdk-streams.test.ts +79 -0
  123. package/src/agents/sdk-streams.ts +55 -10
  124. package/src/agents/skills.test.ts +1 -0
  125. package/src/agents/streaming.test.ts +3 -9
  126. package/src/agents/types.ts +68 -5
  127. package/src/agents/ui-message-stream.test.ts +3 -0
  128. package/src/cli.ts +411 -108
  129. package/src/context/context.test.ts +51 -12
  130. package/src/context/context.ts +161 -29
  131. package/src/context/session.test.ts +1 -0
  132. package/src/context/session.ts +114 -6
  133. package/src/context/types.ts +30 -12
  134. package/src/define.test.ts +13 -8
  135. package/src/define.ts +25 -4
  136. package/src/expect/index.ts +53 -23
  137. package/src/i18n/en.ts +64 -2
  138. package/src/i18n/zh-CN.ts +65 -3
  139. package/src/o11y/cost.test.ts +1 -0
  140. package/src/o11y/execution-tree.test.ts +1 -20
  141. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  142. package/src/o11y/otlp/parse.test.ts +1 -0
  143. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  144. package/src/o11y/parsers/bub.test.ts +1 -0
  145. package/src/o11y/parsers/claude-code.test.ts +1 -34
  146. package/src/o11y/parsers/openclaw.test.ts +154 -0
  147. package/src/o11y/parsers/openclaw.ts +310 -0
  148. package/src/o11y/prices.json +746 -311
  149. package/src/o11y/tool-names.test.ts +1 -0
  150. package/src/o11y/types.ts +16 -2
  151. package/src/report/aggregate.ts +34 -5
  152. package/src/report/built-in-user-parity.test.tsx +110 -153
  153. package/src/report/built-ins/experiment-comparison.tsx +173 -13
  154. package/src/report/built-ins/index.ts +6 -1
  155. package/src/report/components.tsx +9 -3
  156. package/src/report/compute.ts +70 -40
  157. package/src/report/dual-render.test.tsx +194 -67
  158. package/src/report/flag.ts +30 -2
  159. package/src/report/format.ts +35 -11
  160. package/src/report/index.ts +22 -4
  161. package/src/report/locale.ts +25 -5
  162. package/src/report/metrics.ts +67 -14
  163. package/src/report/primitives.tsx +6 -0
  164. package/src/report/react/AttemptList.tsx +6 -31
  165. package/src/report/react/EvalList.tsx +0 -0
  166. package/src/report/react/ExperimentComparison.tsx +68 -0
  167. package/src/report/react/ExperimentList.tsx +15 -9
  168. package/src/report/react/MetricScatter.tsx +12 -14
  169. package/src/report/react/chart-math.test.ts +85 -0
  170. package/src/report/react/chart-math.ts +101 -22
  171. package/src/report/react/enhance.js +33 -1
  172. package/src/report/react/fixtures.ts +24 -17
  173. package/src/report/react/render.test.tsx +9 -64
  174. package/src/report/react/styles.css +73 -2
  175. package/src/report/report.test.ts +306 -98
  176. package/src/report/report.ts +6 -2
  177. package/src/report/text/faces.ts +47 -43
  178. package/src/report/text/table.ts +42 -5
  179. package/src/report/types.ts +41 -21
  180. package/src/results/annotated-source.test.ts +62 -9
  181. package/src/results/annotated-source.ts +64 -6
  182. package/src/results/attempt-evidence.test.ts +9 -7
  183. package/src/results/attempt-evidence.ts +15 -8
  184. package/src/results/attempt-source.ts +6 -3
  185. package/src/results/copy.ts +145 -55
  186. package/src/results/host-equivalence.test.ts +8 -6
  187. package/src/results/index.ts +2 -0
  188. package/src/results/locator.test.ts +1 -22
  189. package/src/results/open.ts +7 -1
  190. package/src/results/publish.ts +149 -0
  191. package/src/results/results.test.ts +85 -51
  192. package/src/results/truncate.ts +90 -0
  193. package/src/results/types.ts +7 -0
  194. package/src/results/writer.ts +31 -13
  195. package/src/runner/attempt.test.ts +138 -7
  196. package/src/runner/attempt.ts +603 -104
  197. package/src/runner/discover.test.ts +47 -0
  198. package/src/runner/discover.ts +36 -2
  199. package/src/runner/eval-source.test.ts +1 -27
  200. package/src/runner/feedback/agent.test.ts +504 -0
  201. package/src/runner/feedback/agent.ts +409 -0
  202. package/src/runner/feedback/ci.test.ts +562 -0
  203. package/src/runner/feedback/ci.ts +401 -0
  204. package/src/runner/feedback/coordinator.test.ts +317 -0
  205. package/src/runner/feedback/coordinator.ts +397 -0
  206. package/src/runner/feedback/failure.ts +40 -0
  207. package/src/runner/feedback/human.test.ts +616 -0
  208. package/src/runner/feedback/human.ts +535 -0
  209. package/src/runner/feedback/index.ts +66 -0
  210. package/src/runner/feedback/io.ts +78 -0
  211. package/src/runner/feedback/profile.test.ts +50 -0
  212. package/src/runner/feedback/profile.ts +58 -0
  213. package/src/runner/feedback/reducer.test.ts +395 -0
  214. package/src/runner/feedback/reducer.ts +260 -0
  215. package/src/runner/feedback/renderer.ts +82 -0
  216. package/src/runner/feedback/sink.ts +203 -0
  217. package/src/runner/feedback/testing.ts +106 -0
  218. package/src/runner/ledger.test.ts +230 -0
  219. package/src/runner/ledger.ts +329 -0
  220. package/src/runner/report.test.ts +128 -3
  221. package/src/runner/report.ts +33 -9
  222. package/src/runner/reporters/artifacts.ts +8 -2
  223. package/src/runner/reporters/braintrust.test.ts +8 -7
  224. package/src/runner/reporters/braintrust.ts +9 -2
  225. package/src/runner/reporters/index.ts +2 -2
  226. package/src/runner/reporters/json.test.ts +162 -0
  227. package/src/runner/reporters/json.ts +35 -8
  228. package/src/runner/reporters/shared.ts +1 -5
  229. package/src/runner/run.test.ts +760 -3
  230. package/src/runner/run.ts +242 -36
  231. package/src/runner/sandbox-prep.ts +3 -42
  232. package/src/runner/timing.ts +158 -0
  233. package/src/runner/types.ts +518 -22
  234. package/src/sandbox/checkpoint.test.ts +55 -0
  235. package/src/sandbox/checkpoint.ts +29 -8
  236. package/src/sandbox/cli-commands.ts +407 -0
  237. package/src/sandbox/docker.ts +115 -16
  238. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  239. package/src/sandbox/e2b-agent-template.ts +94 -0
  240. package/src/sandbox/e2b.ts +74 -9
  241. package/src/sandbox/errors.ts +111 -4
  242. package/src/sandbox/index.ts +2 -0
  243. package/src/sandbox/io-retry.test.ts +58 -0
  244. package/src/sandbox/io-retry.ts +45 -0
  245. package/src/sandbox/keep-registry.test.ts +86 -0
  246. package/src/sandbox/keep-registry.ts +142 -0
  247. package/src/sandbox/keep.ts +178 -0
  248. package/src/sandbox/paths.test.ts +1 -0
  249. package/src/sandbox/paths.ts +19 -8
  250. package/src/sandbox/registry.ts +20 -3
  251. package/src/sandbox/resolve.ts +76 -11
  252. package/src/sandbox/retry.test.ts +70 -0
  253. package/src/sandbox/retry.ts +46 -4
  254. package/src/sandbox/types.ts +44 -6
  255. package/src/sandbox/vercel.ts +43 -20
  256. package/src/scoring/collector.ts +60 -17
  257. package/src/scoring/coverage.ts +95 -0
  258. package/src/scoring/diff.ts +81 -0
  259. package/src/scoring/display.test.ts +121 -0
  260. package/src/scoring/display.ts +133 -0
  261. package/src/scoring/evidence.test.ts +189 -0
  262. package/src/scoring/judge.test.ts +142 -0
  263. package/src/scoring/judge.ts +15 -18
  264. package/src/scoring/scoped.ts +217 -50
  265. package/src/scoring/types.ts +117 -20
  266. package/src/scoring/verdict.ts +16 -4
  267. package/src/shared/aggregate.ts +3 -2
  268. package/src/shared/types.ts +31 -0
  269. package/src/show/compose.ts +2 -2
  270. package/src/show/index.ts +21 -1
  271. package/src/show/render.ts +619 -104
  272. package/src/show/show.test.ts +235 -19
  273. package/src/tty-line.ts +8 -26
  274. package/src/util.test.ts +1 -0
  275. package/src/util.ts +41 -0
  276. package/src/view/app/components/AttemptModal.tsx +153 -2
  277. package/src/view/app/components/CodeView.tsx +32 -11
  278. package/src/view/app/components/CopyControls.tsx +2 -2
  279. package/src/view/app/i18n.ts +6 -0
  280. package/src/view/app/lib/attempt-route.test.ts +1 -0
  281. package/src/view/app/lib/verdict.ts +7 -9
  282. package/src/view/artifact-serving.test.ts +2 -1
  283. package/src/view/client-dist/app.css +1 -1
  284. package/src/view/client-dist/app.js +17 -17
  285. package/src/view/data.test.ts +1 -0
  286. package/src/view/data.ts +11 -1
  287. package/src/view/index.ts +11 -0
  288. package/src/view/server.ts +2 -0
  289. package/src/view/styles.css +3 -0
  290. package/src/view/view-report.test.ts +6 -5
  291. package/src/runner/reporters/console.ts +0 -70
  292. package/src/runner/reporters/live.test.ts +0 -56
  293. package/src/runner/reporters/live.ts +0 -247
  294. package/src/runner/reporters/quiet.test.ts +0 -66
  295. package/src/runner/reporters/quiet.ts +0 -49
  296. package/src/runner/reporters/table.ts +0 -277
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "defineEval:声明、配置并运行 NiceEval eval"
3
3
  sidebarTitle: "defineEval"
4
- description: "defineEval 参考:选项、test context t、Turn 返回值、sandbox helper 和数据集数组导出。"
4
+ description: "defineEval 参考:选项、test context t、Turn 返回值、sandbox helper,以及数组和 keyed record 数据集导出。"
5
5
  ---
6
6
 
7
7
  `defineEval` 是编写 eval 的主要入口。每个 eval 文件调用一次,传入描述和 `test(t)`,并默认导出结果。
@@ -84,15 +84,28 @@ metadata?: Record<string, unknown>;
84
84
 
85
85
  任意附加元数据,原样透传进 EvalResult,不参与调度或打分;供自定义 reporter 消费。
86
86
 
87
+ #### `diff`
88
+
89
+ ```ts
90
+ diff?: { include?: string[]; ignore?: string[] };
91
+ ```
92
+
93
+ 调整 agent diff 的归因排除清单(仅沙箱型;见 docs/feature/eval/README.md):两个数组都是
94
+ gitignore 风格 glob(workdir 相对)。默认排除 .git/node_modules/构建产物/包管理器缓存;
95
+ `ignore` 在默认清单上追加排除;`include` 优先级最高,把匹配路径显式加回。
96
+ 合成规则固定为「默认 ∪ ignore,再被 include 打洞」,清单在分类账锚点时冻结。
97
+
87
98
  #### `setup`
88
99
 
89
100
  ```ts
90
- setup?: (sandbox: Sandbox) => Promise<void | Cleanup> | void | Cleanup;
101
+ setup?: (sandbox: Sandbox, ctx: SandboxHookContext) => Promise<void | Cleanup> | void | Cleanup;
91
102
  ```
92
103
 
93
104
  eval 级预置:拿到沙箱(已上传 workspace + git 基线 + 装好依赖前)。
94
105
  默认命令以非 root 跑(agent 的自然环境);装系统依赖时给 `runCommand` 传 `{ root: true }`
95
106
  (如 `runCommand("apt-get", ["install", …], { root: true })`),跨 provider 语义一致。
107
+ 第二个参数是绑定到 `eval.setup` 的窄上下文(`ctx.progress` / `ctx.diagnostic`,
108
+ 见 docs/feature/eval/README.md);可返回 cleanup 闭包,归因到 `eval.teardown`。
96
109
 
97
110
  #### `test`
98
111
 
@@ -115,10 +128,10 @@ eval 主体:拿到 TestContext,驱动对话 / 沙箱操作并就地断言。
115
128
  #### `send`
116
129
 
117
130
  ```ts
118
- send(text: string): Promise<TurnHandle>;
131
+ send(input: SendInput): Promise<TurnHandle>;
119
132
  ```
120
133
 
121
- 向默认会话发一条消息,返回该轮的 TurnHandle。事件同时累加进默认会话的累计事件流,供下面的作用域断言使用。
134
+ 向默认会话发一条消息(字符串或结构化消息),返回该轮的 TurnHandle。事件同时累加进默认会话的累计事件流,供下面的作用域断言使用。
122
135
 
123
136
  #### `sendFile`
124
137
 
@@ -219,13 +232,31 @@ readonly flags: Readonly<Record<string, unknown>>;
219
232
 
220
233
  本次 attempt 生效的实验 flags(experiment.flags 的只读视图;实验条件,非命令行开关)。
221
234
 
235
+ #### `progress`
236
+
237
+ ```ts
238
+ progress(update: ProgressUpdate): void;
239
+ ```
240
+
241
+ 作用域反馈:报告 eval 自己执行的长步骤(上传 fixture、跑构建……)。短命状态,scope 固定
242
+ 为 `eval.run`;只报告不断言(见 docs/feature/eval/library/context.md「向运行反馈长步骤」)。
243
+
244
+ #### `diagnostic`
245
+
246
+ ```ts
247
+ diagnostic(input: DiagnosticInput): void;
248
+ ```
249
+
250
+ 作用域反馈:报告运行结束后仍需保留的问题(永久事件,落 attempt diagnostics)。
251
+ 即使 level 为 "error" 也不自动改变 verdict——测试结论仍由断言决定。
252
+
222
253
  #### `log`
223
254
 
224
255
  ```ts
225
256
  log(msg: string): void;
226
257
  ```
227
258
 
228
- 打一行调试日志;有 live 进度回调时走该回调,否则落到 stderr,不出现在最终结果里。
259
+ `progress({ message: msg })` 的别名(调试日志),不出现在最终结果里。
229
260
 
230
261
  #### `skip`
231
262
 
@@ -380,10 +411,10 @@ eventOrder(types: StreamEvent["type"][]): AssertionHandle;
380
411
  #### `eventsSatisfy`
381
412
 
382
413
  ```ts
383
- eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean, label?: string): AssertionHandle;
414
+ eventsSatisfy(label: string, predicate: (events: readonly StreamEvent[]) => boolean): AssertionHandle;
384
415
  ```
385
416
 
386
- 用自定义谓词对默认会话累计的整段事件流断言;失败时用 label 说明原因。
417
+ 用自定义谓词对默认会话累计的整段事件流断言;label 必填、进断言标题。
387
418
 
388
419
  #### `sandbox`
389
420
 
@@ -633,10 +664,10 @@ eventOrder(types: StreamEvent["type"][]): AssertionHandle;
633
664
  #### `eventsSatisfy`
634
665
 
635
666
  ```ts
636
- eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean, label?: string): AssertionHandle;
667
+ eventsSatisfy(label: string, predicate: (events: readonly StreamEvent[]) => boolean): AssertionHandle;
637
668
  ```
638
669
 
639
- 用自定义谓词对本轮整段事件流断言;失败时用 label 说明原因。
670
+ 用自定义谓词对本轮整段事件流断言;label 必填、进断言标题(谓词不透明,解释责任在 label)。
640
671
 
641
672
  #### `maxTokens`
642
673
 
@@ -678,3 +709,5 @@ export default rows.map((row) =>
678
709
  ```
679
710
 
680
711
  数组导出会生成稳定 ID:`file/0000`、`file/0001` 等。
712
+
713
+ 已有稳定业务 key 时也可以默认导出 `Record<string, EvalDef>`。例如 key `15193` 在 `swelancer.eval.ts` 中生成 `swelancer/15193`。key 必须是非空单一路径片段,不能是 `.` / `..`,不能含 `/`、`\\` 或控制字符;发现顺序按 key 字典序固定。
@@ -136,7 +136,7 @@ export function makeAssertion(spec: {
136
136
  t.check(t.reply, includes("Paris"));
137
137
  t.check(t.reply, includes(/order #\d+/i));
138
138
  t.check(turn.data, equals({ intent: "refund" }));
139
- t.check(t.reply, matches(/#[A-Z0-9]{8}/));
139
+ t.check(turn.data, matches(OrderSchema)); // Standard Schema / Zod 结构校验;正则匹配用 includes
140
140
  t.check(t.reply, similarity("The answer explains the refund window").atLeast(0.8));
141
141
  t.check(turn.data, satisfies((value) => Array.isArray(value)));
142
142
  ```
@@ -172,6 +172,22 @@ readonly severity: Severity;
172
172
  readonly threshold?: number;
173
173
  ```
174
174
 
175
+ #### `isOptional`
176
+
177
+ ```ts
178
+ readonly isOptional?: boolean;
179
+ ```
180
+
181
+ `.optional()` 链过的标记:评不了只记 unavailable,不把 attempt 拖成 errored。
182
+
183
+ #### `expected`
184
+
185
+ ```ts
186
+ readonly expected?: string;
187
+ ```
188
+
189
+ 期望条件的有界文本描述(如 `contains "Brooklyn"`),失败时进 AssertionResult.expected。
190
+
175
191
  #### `score`
176
192
 
177
193
  ```ts
@@ -195,6 +211,15 @@ atLeast(threshold: number): ValueAssertion;
195
211
  转成软阈值断言:未达 threshold 时该条记为 failed,但默认不拖累整条 eval 的 verdict;
196
212
  `--strict` 运行下,软阈值失败也会把整条 eval 的 verdict 计为 failed。返回新实例,不改原对象。
197
213
 
214
+ #### `optional`
215
+
216
+ ```ts
217
+ optional(): ValueAssertion;
218
+ ```
219
+
220
+ 允许这条断言证据缺席:评不了时只记录 `outcome: "unavailable"`,不影响判定。
221
+ 与 severity 正交(severity 说影不影响质量判定,optional 说证据允不允许缺席)。返回新实例,不改原对象。
222
+
198
223
  {/* GENERATED:END value-assertion */}
199
224
 
200
225
  ## 自定义 matcher
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niceeval",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Agent-native eval tool — eval agents, services, functions, and coding-agent fixtures",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,6 +24,10 @@
24
24
  "types": "./src/sandbox/index.ts",
25
25
  "import": "./src/sandbox/index.ts"
26
26
  },
27
+ "./sandbox/e2b-template": {
28
+ "types": "./src/sandbox/e2b-agent-template.ts",
29
+ "import": "./src/sandbox/e2b-agent-template.ts"
30
+ },
27
31
  "./adapter": {
28
32
  "types": "./src/agents/index.ts",
29
33
  "import": "./src/agents/index.ts"
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/adapters/cases.md
1
2
  import { describe, expect, it } from "vitest";
2
3
 
3
4
  import { aiSdkOtel } from "./ai-sdk-otel.ts";
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/adapters/cases.md
1
2
  import { describe, expect, it } from "vitest";
2
3
 
3
4
  import { aiSdkAgent, fromAiSdk } from "./ai-sdk.ts";
@@ -457,6 +458,8 @@ function fakeCtx(opts: { id?: string } = {}): AgentContext {
457
458
  flags: {},
458
459
  sandbox: undefined as unknown as AgentContext["sandbox"],
459
460
  session,
461
+ progress: () => {},
462
+ diagnostic: () => {},
460
463
  log: () => {},
461
464
  };
462
465
  }
@@ -13,6 +13,7 @@
13
13
  // (不在 steps 里),这里挖出来补成 `action.result` —— 拒绝(execution-denied)映射成
14
14
  // `status: "rejected"`,喂 `calledTool(..., { status: "rejected" })` 与 `noFailedActions()`。
15
15
 
16
+ import { completeCoverage } from "../scoring/coverage.ts";
16
17
  import { randomUUID } from "node:crypto";
17
18
 
18
19
  import { defineAgent } from "../define.ts";
@@ -466,6 +467,8 @@ export function aiSdkAgent<M = unknown>(options: AiSdkAgentOptions<M>): Agent {
466
467
 
467
468
  return defineAgent({
468
469
  name: options.name ?? "ai-sdk",
470
+ // 官方 SDK 完整 steps/output:全通道 complete(见 adapters/architecture/evidence.md)。
471
+ coverage: completeCoverage,
469
472
  // tracing 开了才让运行器为这个 agent 起 OTLP 接收器(ctx.telemetry 才会出现);
470
473
  // AgentTracing 的其余字段(env/configure/scope)这里都用不上,空对象即可。
471
474
  tracing: options.tracing ? {} : undefined,
@@ -0,0 +1,34 @@
1
+ // cases: docs/engineering/unit-tests/adapters/cases.md
2
+ import { describe, expect, it } from "vitest";
3
+ import { readFile } from "node:fs/promises";
4
+ import {
5
+ BUB_INSTALL_MARKER,
6
+ DEFAULT_BUB_OTEL_PLUGIN,
7
+ DEFAULT_BUB_OVERRIDE,
8
+ bubInstallHash,
9
+ bubInstallSpec,
10
+ } from "./bub-install-spec.ts";
11
+
12
+ describe("Bub install specification", () => {
13
+ it("uses immutable default Git commits", () => {
14
+ expect(DEFAULT_BUB_OVERRIDE).toMatch(/@[0-9a-f]{40}$/);
15
+ expect(DEFAULT_BUB_OTEL_PLUGIN).toMatch(/@[0-9a-f]{40}#subdirectory=/);
16
+ });
17
+
18
+ it("fingerprints plugins independent of caller ordering", () => {
19
+ const packages = ["alpha==1", "beta==2"];
20
+ expect(bubInstallHash(packages)).toBe(bubInstallHash([" beta==2 ", "alpha==1", "alpha==1"]));
21
+ expect(bubInstallSpec(packages)).toContain("--with alpha==1 --with beta==2");
22
+ expect(BUB_INSTALL_MARKER).toBe(".local/share/niceeval/bub-install-hash");
23
+ });
24
+
25
+ it("keeps the non-TypeScript Docker recipe in sync", async () => {
26
+ const [dockerfile, override] = await Promise.all([
27
+ readFile(new URL("../../sandbox/docker/Dockerfile", import.meta.url), "utf8"),
28
+ readFile(new URL("../../sandbox/docker/bub-override.txt", import.meta.url), "utf8"),
29
+ ]);
30
+ expect(override.trim()).toBe(DEFAULT_BUB_OVERRIDE);
31
+ expect(dockerfile).toContain(DEFAULT_BUB_OTEL_PLUGIN);
32
+ expect(dockerfile).toContain(bubInstallHash([]));
33
+ });
34
+ });
@@ -0,0 +1,32 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const DEFAULT_BUB_OVERRIDE =
4
+ "bub @ git+https://github.com/CorrectRoadH/bub.git@86fbd0febc1665353f5131173554e1f513e66b4c";
5
+
6
+ export const DEFAULT_BUB_OTEL_PLUGIN =
7
+ "git+https://github.com/bubbuild/bub-contrib.git@add4a6a133c5658aec8f167ef50804d9ee55d22e#subdirectory=packages/bub-tapestore-otel";
8
+
9
+ export const BUB_CHECKPOINT_SUBDIRS = [".local"] as const;
10
+ export const BUB_INSTALL_MARKER = ".local/share/niceeval/bub-install-hash";
11
+
12
+ export function normalizeBubPackages(packages: readonly string[]): string[] {
13
+ return [...new Set(packages.map((value) => value.trim()).filter(Boolean))].sort();
14
+ }
15
+
16
+ export function bubInstallSpec(
17
+ packages: readonly string[],
18
+ override = DEFAULT_BUB_OVERRIDE,
19
+ otelPlugin = DEFAULT_BUB_OTEL_PLUGIN,
20
+ ): string {
21
+ const normalized = normalizeBubPackages(packages);
22
+ const plugins = normalized.length ? ` --with ${normalized.join(" --with ")}` : "";
23
+ return `bub --override(${override}) --with ${otelPlugin}${plugins} --checkpoint(${BUB_CHECKPOINT_SUBDIRS.join(",")})`;
24
+ }
25
+
26
+ export function bubInstallHash(
27
+ packages: readonly string[],
28
+ override = DEFAULT_BUB_OVERRIDE,
29
+ otelPlugin = DEFAULT_BUB_OTEL_PLUGIN,
30
+ ): string {
31
+ return createHash("md5").update(bubInstallSpec(packages, override, otelPlugin)).digest("hex").slice(0, 12);
32
+ }
package/src/agents/bub.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { completeCoverage } from "../scoring/coverage.ts";
1
2
  import { defineSandboxAgent } from "../define.ts";
2
3
  import { requireEnv, getEnv } from "../util.ts";
3
4
  import { shared } from "./shared.ts";
@@ -16,6 +17,14 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
16
17
  import { homedir } from "node:os";
17
18
  import { join, dirname } from "node:path";
18
19
  import { t } from "../i18n/index.ts";
20
+ import {
21
+ BUB_CHECKPOINT_SUBDIRS,
22
+ BUB_INSTALL_MARKER,
23
+ DEFAULT_BUB_OTEL_PLUGIN,
24
+ DEFAULT_BUB_OVERRIDE,
25
+ bubInstallHash,
26
+ normalizeBubPackages,
27
+ } from "./bub-install-spec.ts";
19
28
 
20
29
  // ───────────────────────────────────────────────────────────────────────────
21
30
  // bub 的 agent adapter(沙箱型)。
@@ -64,50 +73,32 @@ const SKILL_DIR = ".agents/skills";
64
73
  // 可用 NICEEVAL_BUB_OVERRIDE / NICEEVAL_BUB_OTEL_PLUGIN 覆盖,不必改源码。
65
74
  const BUB_OVERRIDE =
66
75
  getEnv("NICEEVAL_BUB_OVERRIDE") ??
67
- "bub @ git+https://github.com/CorrectRoadH/bub.git@fix/tape-assistant-text-with-tool-calls";
76
+ DEFAULT_BUB_OVERRIDE;
68
77
  const BUB_OVERRIDE_FILE = "/tmp/bub-override.txt";
69
78
  // otel 插件跟上游 main 走(bub-contrib#50 起从 bub.tape 导入,要求 bub ≥ 0.3.10dev,
70
79
  // 与上面的 override 分支兼容)。插件不发 PyPI,git 依赖是唯一安装方式。
71
80
  const OTEL_PLUGIN =
72
81
  getEnv("NICEEVAL_BUB_OTEL_PLUGIN") ??
73
- "git+https://github.com/bubbuild/bub-contrib.git#subdirectory=packages/bub-tapestore-otel";
82
+ DEFAULT_BUB_OTEL_PLUGIN;
74
83
 
75
- // override 钉在 git ref 上时,镜像里烘焙的 bub 不可信:模板构建时间早于 ref 当前指向的
76
- // commit 的话,`command -v bub` 命中的就是修复前的旧构建 —— e2b 模板 fasteval-agents 上
77
- // 整轮 bub turn failed(send 后无 AI 回复)就是这么来的:override 分支修好了,但捷径
78
- // 让它从未被安装。所以 pinned 时绕开 PATH 捷径、恒走 uv 安装(有 checkpoint 缓存,
79
- // 不是每沙箱都全量装),且运行时钉死用 $HOME/.local/bin/bub —— 只改 ensureBub 不改这里
80
- // 的话,PATH 上的 /usr/local/bin/bub 仍会先于新装的被 command -v 找到,白装。
81
- const BUB_PINNED = BUB_OVERRIDE.includes("git+");
82
- // bub 二进制:pinned → 恒用 uv 装到 $HOME/.local/bin 的那个;非 pinned → 优先用镜像里
83
- // (预制模板)烘焙在 PATH 上的 bub(装到 /usr/local/bin,见 sandbox/docker/Dockerfile)。
84
- const BUB = BUB_PINNED ? "$HOME/.local/bin/bub" : "$(command -v bub || echo $HOME/.local/bin/bub)";
84
+ // NiceEval 的预制配方与运行时安装都写到 $HOME/.local;显式使用该路径,避免 PATH
85
+ // 另一个未知版本的 bub 抢先命中。
86
+ const BUB = "$HOME/.local/bin/bub";
85
87
 
86
88
  // checkpoint 只打 $HOME/.local:uv 装的 python 工具链、bub 的 tool venv 和 bin shim 全在
87
89
  // 这里,restore 后即可运行。~/.cache/uv 是 wheel/构建缓存,只在「下一次安装」有用,而
88
90
  // restore 场景 bub 已经装好、不会再装——打进去只是把单次 HTTP 传输撑到 100MB+,在 e2b
89
91
  // 文件 API 上超时/连接重置概率明显偏高。子目录列表参与 INSTALL_HASH:改它会换缓存文件
90
92
  // 名,不会继续复用老的大 checkpoint。
91
- const CHECKPOINT_SUBDIRS = [".local"];
93
+ const CHECKPOINT_SUBDIRS = BUB_CHECKPOINT_SUBDIRS;
92
94
 
93
95
  /** 规范化 python plugin:去空白、丢空串、去重 —— 安装命令与 checkpoint key 用同一份列表。 */
94
96
  function normalizePackages(plugins?: readonly PythonPluginSpec[]): string[] {
95
- const seen = new Set<string>();
96
- for (const p of plugins ?? []) {
97
- const pkg = p.package.trim();
98
- if (pkg) seen.add(pkg);
99
- }
100
- return [...seen].sort();
101
- }
102
-
103
- /** 安装环境的完整描述:bub 版本 + otel 插件 + python plugin 集合 + checkpoint 覆盖面。 */
104
- function installSpecOf(packages: readonly string[]): string {
105
- const plugins = packages.length ? ` --with ${packages.join(" --with ")}` : "";
106
- return `bub --override(${BUB_OVERRIDE}) --with ${OTEL_PLUGIN}${plugins} --checkpoint(${CHECKPOINT_SUBDIRS.join(",")})`;
97
+ return normalizeBubPackages((plugins ?? []).map((plugin) => plugin.package));
107
98
  }
108
99
 
109
100
  function installHashOf(packages: readonly string[]): string {
110
- return createHash("md5").update(installSpecOf(packages)).digest("hex").slice(0, 12);
101
+ return bubInstallHash(packages, BUB_OVERRIDE, OTEL_PLUGIN);
111
102
  }
112
103
 
113
104
  function diskCachePath(home: string, installHash: string): string {
@@ -127,15 +118,16 @@ async function ensureBub(
127
118
  log: AgentContext["log"],
128
119
  packages: readonly string[],
129
120
  ): Promise<void> {
130
- // 预制模板已把 bub 烘焙进镜像(PATH 上)→ 直接用,跳过 uv 安装 + checkpoint 全套。
131
- // pinned(git ref override)时不走此捷径:烘焙的 bub 无法验证是不是 ref 当前指向的
132
- // 构建,必须按 override 真装(见 BUB_PINNED 注释)。有 python plugin 时同理不能走捷径:
133
- // 镜像里烘焙的 bub 环境没有这些包。
134
- if (!BUB_PINNED && packages.length === 0 && (await sb.runShell("command -v bub >/dev/null 2>&1")).exitCode === 0) {
121
+ const installHash = installHashOf(packages);
122
+ const marker = `${home}/${BUB_INSTALL_MARKER}`;
123
+ // 只信任带完整安装规格指纹的预制环境。仅 command -v bub 无法证明版本、OTel 插件和
124
+ // 用户 pythonPlugins 一致;NiceEval 的 E2B Bub 配方和运行时安装都会写这个 marker。
125
+ if ((await sb.runShell(
126
+ `test -x ${BUB} && test "$(cat '${marker}' 2>/dev/null)" = '${installHash}'`,
127
+ )).exitCode === 0) {
135
128
  return;
136
129
  }
137
130
 
138
- const installHash = installHashOf(packages);
139
131
  const cacheKey = `${home}::${installHash}`;
140
132
  const withPlugins = packages.map((p) => `--with '${p}'`).join(" ");
141
133
  const checkpointPaths = CHECKPOINT_SUBDIRS.map((d) => `${home}/${d}`);
@@ -190,6 +182,11 @@ async function ensureBub(
190
182
  }));
191
183
  }
192
184
  }
185
+ const markerDir = marker.slice(0, marker.lastIndexOf("/"));
186
+ const mark = await sb.runShell(`mkdir -p '${markerDir}' && printf '%s' '${installHash}' > '${marker}'`);
187
+ if (mark.exitCode !== 0) {
188
+ throw new Error(`Failed to write Bub installation marker: ${mark.stderr || mark.stdout}`);
189
+ }
193
190
  // 到这里 bub 已装进本沙箱,checkpoint 只是给后续沙箱的缓存回填:capture/下载失败
194
191
  // (大 buffer 在 e2b 文件 API 上的瞬态错误)降级为警告,绝不反过来杀掉已就绪的 attempt。
195
192
  try {
@@ -224,6 +221,8 @@ export function bubAgent(config?: BubConfig): Agent {
224
221
 
225
222
  return defineSandboxAgent({
226
223
  name: "bub",
224
+ // 官方 adapter:transcript 经生命周期 fixture 验证,全通道 complete。
225
+ coverage: completeCoverage,
227
226
  spanMapper: mapBubSpans,
228
227
 
229
228
  tracing: {
@@ -1,19 +1,27 @@
1
+ // cases: docs/engineering/unit-tests/adapters/cases.md
1
2
  // Claude Code native plugin 安装(installPlugins)的单测:单 plugin 的命令构造、同名
2
- // marketplace 的去重、ref 钉定时改走 clone+checkout+本地路径连接、resolvedVersion 取不到
3
- // 时优雅省略、marketplace/plugin 安装失败的报错。沙箱是内存 fake,风格与
4
- // src/agents/skills.test.ts 一致(记命令的 FakeSandbox + 按命令前缀打脚本),不另起一套。
3
+ // marketplace 的去重、add 后的注册名回读校验、ref 钉定时改走 clone+checkout+本地路径连接、
4
+ // resolvedVersion 取不到时优雅省略、marketplace/plugin 安装失败的报错;外加 settingsFile
5
+ // (原生配置文件)的 setup 流。沙箱是内存 fake,风格与 src/agents/skills.test.ts 一致
6
+ // (记命令的 FakeSandbox + 按命令前缀打脚本),不另起一套。
5
7
  // 定稿见 docs/feature/adapters/architecture/coding-agent-extensions.md。
6
8
 
7
- import { describe, expect, it } from "vitest";
8
- import { installPlugins, type ClaudeCodePluginSpec } from "./claude-code.ts";
9
- import type { CommandResult, Sandbox, SandboxFile } from "../types.ts";
9
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
10
+ import { createHash } from "node:crypto";
11
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { claudeCodeAgent, installPlugins, type ClaudeCodePluginSpec } from "./claude-code.ts";
15
+ import type { AgentContext, AgentSetupManifest, CommandResult, Sandbox, SandboxFile } from "../types.ts";
10
16
 
11
- /** 内存沙箱:runShell 记命令(可按命令包含的子串打脚本化输出) */
17
+ /** 内存沙箱:runShell 记命令(可按命令包含的子串打脚本化输出),uploadFile / writeFiles 记内容。 */
12
18
  class FakeSandbox implements Partial<Sandbox> {
13
19
  readonly workdir = "/workspace";
14
20
  readonly sandboxId = "fake";
15
21
  readonly otlpHost = null;
16
22
  readonly commands: string[] = [];
23
+ readonly uploads: { path: string; content: Buffer }[] = [];
24
+ readonly written: Record<string, string> = {};
17
25
  script: { match: string; result: (cmd: string) => Partial<CommandResult> }[] = [];
18
26
 
19
27
  async runShell(script: string): Promise<CommandResult> {
@@ -21,7 +29,12 @@ class FakeSandbox implements Partial<Sandbox> {
21
29
  const hit = this.script.find((s) => script.includes(s.match));
22
30
  return { stdout: "", stderr: "", exitCode: 0, ...hit?.result(script) };
23
31
  }
24
- async writeFiles(): Promise<void> {}
32
+ async writeFiles(files: Record<string, string>): Promise<void> {
33
+ Object.assign(this.written, files);
34
+ }
35
+ async uploadFile(path: string, content: Buffer): Promise<void> {
36
+ this.uploads.push({ path, content });
37
+ }
25
38
  async uploadFiles(_files: SandboxFile[]): Promise<void> {}
26
39
  async fileExists(): Promise<boolean> {
27
40
  return false;
@@ -33,7 +46,11 @@ class FakeSandbox implements Partial<Sandbox> {
33
46
 
34
47
  const sb = (s?: FakeSandbox["script"]): FakeSandbox => {
35
48
  const box = new FakeSandbox();
36
- if (s) box.script = s;
49
+ // add 后的注册名回读默认回到配置名(校验通过);mismatch 用例把自己的脚本放前面覆盖。
50
+ box.script = [
51
+ ...(s ?? []),
52
+ { match: "claude plugin marketplace list --json", result: () => ({ stdout: JSON.stringify([{ name: "acme" }]) }) },
53
+ ];
37
54
  return box;
38
55
  };
39
56
  const asSandbox = (box: FakeSandbox): Sandbox => box as unknown as Sandbox;
@@ -48,6 +65,7 @@ describe("claude-code installPlugins · 命令构造", () => {
48
65
 
49
66
  expect(box.commands).toEqual([
50
67
  "claude plugin marketplace add 'acme/claude-code-plugins'",
68
+ "claude plugin marketplace list --json",
51
69
  "claude plugin install 'safe-shell@acme'",
52
70
  "claude plugin list --json",
53
71
  ]);
@@ -150,3 +168,106 @@ describe("claude-code installPlugins · 失败语义", () => {
150
168
  ).rejects.toThrow(/safe-shell/);
151
169
  });
152
170
  });
171
+
172
+ describe("claude-code installPlugins · marketplace 名回读校验", () => {
173
+ const plugins: ClaudeCodePluginSpec[] = [
174
+ { marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
175
+ ];
176
+
177
+ it("add 静默注册成别的名字(仓库 manifest 的真实 name)→ 立刻抛带两个名字的错误,不走到 plugin install", async () => {
178
+ const box = sb([
179
+ {
180
+ match: "claude plugin marketplace list --json",
181
+ result: () => ({ stdout: JSON.stringify([{ name: "duyet-claude-plugins" }]) }),
182
+ },
183
+ ]);
184
+ const err = await installPlugins(asSandbox(box), plugins).catch((e: Error) => e);
185
+ expect(err).toBeInstanceOf(Error);
186
+ expect((err as Error).message).toContain("acme");
187
+ expect((err as Error).message).toContain("duyet-claude-plugins");
188
+ expect(box.commands.some((c) => c.startsWith("claude plugin install"))).toBe(false);
189
+ });
190
+
191
+ it("回读命令失败 → 按回读失败抛错,不静默放行", async () => {
192
+ const box = sb([
193
+ { match: "claude plugin marketplace list --json", result: () => ({ exitCode: 1, stderr: "boom" }) },
194
+ ]);
195
+ await expect(installPlugins(asSandbox(box), plugins)).rejects.toThrow(/marketplace/);
196
+ });
197
+
198
+ it("回读输出解析不出(未知形状)→ 同样按回读失败抛错", async () => {
199
+ const box = sb([
200
+ { match: "claude plugin marketplace list --json", result: () => ({ stdout: "not json" }) },
201
+ ]);
202
+ await expect(installPlugins(asSandbox(box), plugins)).rejects.toThrow(/acme/);
203
+ });
204
+ });
205
+
206
+ describe("claudeCodeAgent settingsFile · setup", () => {
207
+ let root: string;
208
+ let cwdBefore: string;
209
+
210
+ beforeEach(async () => {
211
+ root = await mkdtemp(join(tmpdir(), "niceeval-claude-settings-"));
212
+ cwdBefore = process.cwd();
213
+ process.chdir(root); // settingsFile 相对项目根 = process.cwd()(docs 定稿口径)
214
+ });
215
+
216
+ afterEach(async () => {
217
+ process.chdir(cwdBefore);
218
+ await rm(root, { recursive: true, force: true });
219
+ });
220
+
221
+ const ctx = {} as AgentContext; // claude 的 setup 不读 ctx
222
+
223
+ it("原始字节原样上传并 mv 成用户级 ~/.claude/settings.json;manifest 记项目相对路径 + SHA-256,不落正文", async () => {
224
+ const body = '{\n "$schema": "https://json.schemastore.org/claude-code-settings.json",\n "permissions": { "deny": ["WebSearch", "WebFetch"] }\n}\n';
225
+ await mkdir(join(root, "configs"), { recursive: true });
226
+ await writeFile(join(root, "configs/no-web.json"), body);
227
+
228
+ const box = sb();
229
+ await claudeCodeAgent({ apiKey: "k", settingsFile: "./configs/no-web.json" }).setup!(asSandbox(box), ctx);
230
+
231
+ // 原始字节走 uploadFile(不走会补换行的 heredoc),再 mv 到用户层落点
232
+ expect(box.uploads).toHaveLength(1);
233
+ expect(box.uploads[0]!.content.toString("utf8")).toBe(body);
234
+ const mv = box.commands.find((c) => c.includes("~/.claude/settings.json"))!;
235
+ expect(mv).toContain(`mv ${box.uploads[0]!.path} ~/.claude/settings.json`);
236
+
237
+ const manifest = JSON.parse(box.written["__niceeval__/agent-setup.json"]!) as AgentSetupManifest;
238
+ expect(manifest.nativeConfigFile).toEqual({
239
+ agent: "claude-code",
240
+ path: "configs/no-web.json",
241
+ sha256: createHash("sha256").update(body).digest("hex"),
242
+ });
243
+ expect(box.written["__niceeval__/agent-setup.json"]).not.toContain("permissions");
244
+ });
245
+
246
+ it("保留键(model / env)出现在文件里 → setup 报错点名冲突键,什么都不上传", async () => {
247
+ await writeFile(join(root, "bad.json"), '{ "model": "opus", "permissions": {} }');
248
+ const box = sb();
249
+ await expect(
250
+ claudeCodeAgent({ apiKey: "k", settingsFile: "bad.json" }).setup!(asSandbox(box), ctx),
251
+ ).rejects.toThrow(/model/);
252
+ expect(box.uploads).toHaveLength(0);
253
+ expect(box.written["__niceeval__/agent-setup.json"]).toBeUndefined();
254
+ });
255
+
256
+ it("路径不存在 / 逃出项目根 → setup 报错(attempt errored 的通道)", async () => {
257
+ const box = sb();
258
+ await expect(
259
+ claudeCodeAgent({ apiKey: "k", settingsFile: "configs/nope.json" }).setup!(asSandbox(box), ctx),
260
+ ).rejects.toThrow(/configs\/nope\.json/);
261
+ await expect(
262
+ claudeCodeAgent({ apiKey: "k", settingsFile: "../escape.json" }).setup!(asSandbox(box), ctx),
263
+ ).rejects.toThrow(/相对路径|relative/);
264
+ });
265
+
266
+ it("没配 settingsFile 时不碰 settings.json,也不为它写 manifest", async () => {
267
+ const box = sb();
268
+ await claudeCodeAgent({ apiKey: "k" }).setup!(asSandbox(box), ctx);
269
+ expect(box.uploads).toHaveLength(0);
270
+ expect(box.commands.some((c) => c.includes("settings.json"))).toBe(false);
271
+ expect(box.written["__niceeval__/agent-setup.json"]).toBeUndefined();
272
+ });
273
+ });