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
@@ -9,16 +9,28 @@
9
9
  import { mkdir, writeFile } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
11
  import type { AgentSetupManifest, EvalResult, ExperimentRunInfo, LocalizedText } from "../types.ts";
12
- import type { DiffData, O11ySummary, SourceArtifact, StreamEvent, TraceSpan } from "../types.ts";
12
+ import type { DiffArtifact, O11ySummary, SourceArtifact, StreamEvent, TraceSpan } from "../types.ts";
13
13
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
14
14
  import { RESULT_FILE, SNAPSHOT_FILE, artifactFileOf, attemptDirOf, experimentDirOf } from "./format.ts";
15
15
  import { encodeAttemptLocator } from "./locator.ts";
16
16
  import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
17
+ import { truncateEvents, truncateSpans } from "./truncate.ts";
17
18
  import type { Producer, SnapshotMeta } from "./types.ts";
18
19
 
19
20
  export interface ResultsWriterOptions {
20
21
  /** 谁在写这份结果:niceeval 自己,或第三方 harness(name 如实写,别冒充 "niceeval")。 */
21
22
  producer: Producer;
23
+ /**
24
+ * 本次 invocation 的快照身份锚点(ISO 时间戳,即 runner 的 `RunShape.snapshotStartedAt`)。
25
+ * `writeAttemptFor()` 的隐式 snapshot 声明统一用它做 `startedAt`,不再按「该 experiment
26
+ * 第一条落盘 result 的 attempt startedAt」猜 —— 那个锚点依赖并发完成顺序,不确定;
27
+ * 同一 writer 处理多个 experiment 时也共享这同一个值(locator 身份还含 experimentId,
28
+ * 不会碰撞)。省略时退回旧行为:每次隐式声明按 `result.startedAt ?? now()` 各自取锚,
29
+ * 供未提供该值的直调场景使用(测试、结果转换脚本、第三方 harness 直接调
30
+ * `createResultsWriter()` 而不经过 niceeval 自己的 runner)。显式调用 `writer.snapshot()`
31
+ * 声明快照的调用方不受这个选项影响,必须自己传 `SnapshotDeclaration.startedAt`。
32
+ */
33
+ snapshotStartedAt?: string;
22
34
  }
23
35
 
24
36
  /** 快照级声明:一个 experiment 声明一次,这些字段不塞进每条 attempt。 */
@@ -70,7 +82,7 @@ export interface AttemptArtifacts {
70
82
  o11y?: O11ySummary;
71
83
  /** agent setup 的安装清单(沙箱型 coding agent 装了 Skill / plugin / MCP 才有)。 */
72
84
  agentSetup?: AgentSetupManifest;
73
- diff?: DiffData;
85
+ diff?: DiffArtifact;
74
86
  sources?: SourceArtifact[];
75
87
  }
76
88
 
@@ -118,7 +130,7 @@ export function createResultsWriter(root: string, opts: ResultsWriterOptions): R
118
130
  producer: opts.producer,
119
131
  experimentId: decl.experimentId,
120
132
  // 运行配置不带 id:身份的家是顶层 experimentId,重复一份只会引出「以谁为准」。
121
- ...(decl.experiment !== undefined ? { experiment: stripInfoId(decl.experiment) } : {}),
133
+ ...(decl.experiment !== undefined ? { experiment: (decl.experiment) } : {}),
122
134
  agent: decl.agent,
123
135
  ...(decl.model !== undefined ? { model: decl.model } : {}),
124
136
  startedAt: decl.startedAt,
@@ -172,8 +184,11 @@ export function createResultsWriter(root: string, opts: ResultsWriterOptions): R
172
184
  experimentId: result.experimentId,
173
185
  agent: result.agent,
174
186
  model: result.model,
175
- // 快照 startedAt 以该实验首条落盘结果的 attempt 时刻为锚(首条 ≈ 实验开跑)
176
- startedAt: result.startedAt ?? new Date().toISOString(),
187
+ // 快照 startedAt 优先用 writer 级的 invocation 锚点( ResultsWriterOptions.snapshotStartedAt)——
188
+ // niceeval 自己的 runner 恒会提供,多个 experiment 共享同一个值。省略时(第三方直调
189
+ // createResultsWriter() 未传该选项)退回旧行为:以本次调用这个 result 自己的 attempt
190
+ // startedAt 为锚,首条落盘的 result 决定了这个 experiment 快照的身份锚点。
191
+ startedAt: opts.snapshotStartedAt ?? result.startedAt ?? new Date().toISOString(),
177
192
  experiment: result.experiment,
178
193
  });
179
194
 
@@ -288,9 +303,14 @@ async function writeAttemptFiles(
288
303
  const hasTrace = !!(artifacts?.trace && artifacts.trace.length);
289
304
 
290
305
  const writes: Promise<unknown>[] = [];
291
- if (hasEvents) writes.push(writeFile(join(attemptDir, "events.json"), JSON.stringify(artifacts!.events), "utf-8"));
306
+ // 大值截断只发生在这里(序列化的那一刻):events 的事件字段与 trace span 属性里的任意
307
+ // 字符串值按 ARTIFACT_VALUE_MAX_BYTES 截断并留结构化 truncated 标记;运行时(断言 / o11y
308
+ // 派生)看到的始终是完整值。sources 与 diff 不截断(见 docs/feature/results/architecture.md)。
309
+ if (hasEvents)
310
+ writes.push(writeFile(join(attemptDir, "events.json"), JSON.stringify(truncateEvents(artifacts!.events!)), "utf-8"));
292
311
  if (hasSources) writes.push(writeSourcesRef(snapDir, attemptDir, artifacts!.sources!, sourceStore));
293
- if (hasTrace) writes.push(writeFile(join(attemptDir, "trace.json"), JSON.stringify(artifacts!.trace), "utf-8"));
312
+ if (hasTrace)
313
+ writes.push(writeFile(join(attemptDir, "trace.json"), JSON.stringify(truncateSpans(artifacts!.trace!)), "utf-8"));
294
314
  if (artifacts?.o11y) writes.push(writeFile(join(attemptDir, "o11y.json"), JSON.stringify(artifacts.o11y), "utf-8"));
295
315
  if (artifacts?.agentSetup) {
296
316
  writes.push(
@@ -303,6 +323,9 @@ async function writeAttemptFiles(
303
323
  // locator:caller(如第三方 harness 直接调 SnapshotWriter.writeAttempt)已经带了就尊重,
304
324
  // 否则按当前身份元组算一份 —— 这条路径只服务「非携带」的新写入,携带条目走
305
325
  // writeAttemptForImpl 的 artifactBase 分支,原样透传 result.locator,从不落到这里重算。
326
+ // niceeval 自己的 runner(src/runner/run.ts)在 fresh attempt 完成时已经把 locator 写进
327
+ // entry.locator(用的正是同一个 snapshot.startedAt),所以这条 encodeAttemptLocator 兜底
328
+ // 对 niceeval 自身运行永不触发,只在第三方 harness 未预先算好 locator 时才会走到。
306
329
  const locator =
307
330
  entry.locator ??
308
331
  encodeAttemptLocator({
@@ -365,12 +388,7 @@ async function createSnapshotDir(root: string, experimentId: string): Promise<st
365
388
  throw new Error(`Could not create a unique snapshot directory under "${parent}" after 5 attempts (${String(lastError)}).`);
366
389
  }
367
390
 
368
- /** 运行配置落盘前剥掉 id:experimentId 的家在 snapshot.json 顶层。 */
369
- function stripInfoId(info: ExperimentRunInfo): ExperimentRunInfo {
370
- const { id, ...rest } = info;
371
- void id;
372
- return rest;
373
- }
391
+
374
392
 
375
393
  /** 快照目录名的时间戳段:Date#toISOString 把 : 与 . 换成 -(与 docs/feature/results/architecture.md 一致)。 */
376
394
  function safeTimestamp(d: Date): string {
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/experiments-runner/cases.md
1
2
  // 路径提升单测:agent.setup 写进沙箱 `__niceeval__/agent-setup.json` 的安装 manifest,
2
3
  // runAttemptEffect 在 setup 之后把它读出来、原样挂到 EvalResult.agentSetup(见
3
4
  // docs/feature/results/architecture.md「agent-setup.json」、src/agents/manifest.ts 的注释)。
@@ -11,7 +12,7 @@ import { runAttemptEffect } from "./attempt.ts";
11
12
  import { defineSandboxAgent, defineSandbox } from "../define.ts";
12
13
  import { writeAgentSetupManifest, AGENT_SETUP_MANIFEST_PATH } from "../agents/manifest.ts";
13
14
  import type { CapturedEvalSource } from "./eval-source.ts";
14
- import type { Attempt, AgentRun, RunOptions } from "./types.ts";
15
+ import type { Attempt, AgentRun, LifecyclePhase, RunOptions } from "./types.ts";
15
16
  import type {
16
17
  AgentSetupManifest,
17
18
  Agent,
@@ -29,6 +30,8 @@ class FakeSandbox implements Partial<Sandbox> {
29
30
  readonly otlpHost = null;
30
31
  readonly files = new Map<string, string>();
31
32
 
33
+ constructor(private readonly stopDelayMs = 0) {}
34
+
32
35
  async runShell(): Promise<CommandResult> {
33
36
  return { stdout: "", stderr: "", exitCode: 0 };
34
37
  }
@@ -63,21 +66,31 @@ class FakeSandbox implements Partial<Sandbox> {
63
66
  async readSourceFiles(): Promise<never> {
64
67
  throw new Error("not implemented");
65
68
  }
66
- async stop(): Promise<void> {}
69
+ async stop(): Promise<void> {
70
+ if (this.stopDelayMs > 0) await new Promise((resolve) => setTimeout(resolve, this.stopDelayMs));
71
+ }
67
72
  }
68
73
 
69
74
  const asSandbox = (box: FakeSandbox): Sandbox => box as unknown as Sandbox;
70
75
 
71
76
  const source: CapturedEvalSource = { path: "fake.eval.ts", content: "", sha256: "0".repeat(64) };
72
77
 
73
- /** 跑一次 attempt:给定 agent,返回 EvalResult。沙箱用内存 fake,不起容器/不联网。 */
74
- async function runOnce(agent: Agent, box: FakeSandbox): Promise<import("../types.ts").EvalResult> {
78
+ /** 跑一次 attempt:给定 agent,返回 EvalResult。沙箱用内存 fake,不起容器/不联网。
79
+ * 可选 `evalDefOverrides` 覆盖默认 evalDef 的字段(如挂一个 `setup`);可选 `onPhase` 透传给
80
+ * `runAttemptEffect` 的第五个参数,原样转发 attempt.ts 的 enterPhase 边界(见下方
81
+ * onPhase 回调专用的 describe 块)。 */
82
+ async function runOnce(
83
+ agent: Agent,
84
+ box: FakeSandbox,
85
+ opts: { evalDefOverrides?: Partial<DiscoveredEval>; onPhase?: (phase: LifecyclePhase) => void } = {},
86
+ ): Promise<import("../types.ts").EvalResult> {
75
87
  const evalDef: DiscoveredEval = {
76
88
  id: "fake/eval",
77
89
  baseDir: "/project",
78
90
  sourcePath: "/project/fake.eval.ts",
79
91
  source,
80
92
  test: () => {},
93
+ ...opts.evalDefOverrides,
81
94
  };
82
95
  const run: AgentRun = {
83
96
  agent,
@@ -91,16 +104,15 @@ async function runOnce(agent: Agent, box: FakeSandbox): Promise<import("../types
91
104
  };
92
105
  const attempt: Attempt = { evalDef, run, attempt: 0, key: "fake/eval", fingerprint: "" };
93
106
  const config: Config = {};
94
- const opts: RunOptions = {
107
+ const runOpts: RunOptions = {
95
108
  config,
96
109
  evals: [evalDef],
97
110
  agentRuns: [run],
98
111
  reporters: [],
99
112
  maxConcurrency: 1,
100
- onProgress: () => {}, // 静音进度日志,不污染测试输出
101
113
  };
102
114
  const sandboxSem = Effect.runSync(Effect.makeSemaphore(1));
103
- return Effect.runPromise(runAttemptEffect(attempt, opts, sandboxSem));
115
+ return Effect.runPromise(runAttemptEffect(attempt, runOpts, sandboxSem, undefined, opts.onPhase));
104
116
  }
105
117
 
106
118
  describe("runAttemptEffect · agent-setup 路径提升(沙箱 __niceeval__/agent-setup.json → EvalResult.agentSetup)", () => {
@@ -168,3 +180,122 @@ describe("runAttemptEffect · agent-setup 路径提升(沙箱 __niceeval__/agent
168
180
  expect(result.agentSetup).toBeUndefined();
169
181
  });
170
182
  });
183
+
184
+ // run.ts 的 reportFailure() 需要「失败发生时所在的阶段」,但 attempt:complete 一发出 coordinator
185
+ // 就把 active map 里对应条目删了,run.ts 没法事后反查——只能在 attempt.ts 每次真正跨入一个新
186
+ // phase 边界时同步拿到通知。这里直接单测 runAttemptEffect 的第五个参数(onPhase)是否真的随
187
+ // enterPhase 同步触发、顺序是否符合「没有对应 hook/配置的步骤直接跳过」的契约
188
+ //(docs/feature/experiments/cli.md「Attempt 阶段」),而不是只在 run.ts 集成测试里间接验证。
189
+ describe("runAttemptEffect · onPhase 回调随 enterPhase 同步触发", () => {
190
+ it("挂了 eval.setup 与 agent.setup 时,phase 序列包含两者且不产生空阶段", async () => {
191
+ const agent = defineSandboxAgent({
192
+ name: "fake-agent-with-setup",
193
+ setup: async () => {},
194
+ send: async () => ({ events: [], status: "completed" }),
195
+ });
196
+
197
+ const phases: LifecyclePhase[] = [];
198
+ const box = new FakeSandbox();
199
+ const result = await runOnce(agent, box, {
200
+ evalDefOverrides: { setup: async () => {} },
201
+ onPhase: (phase) => phases.push(phase),
202
+ });
203
+
204
+ expect(result.error).toBeUndefined();
205
+ // sandbox-setup(没有 SandboxSpec.setup 钩子)与 telemetry-setup(没有 tracing)都该跳过——
206
+ // 不产生空阶段,序列只含实际执行到的边界,严格按生命周期顺序出现一次。
207
+ expect(phases).toEqual([
208
+ "sandbox.queue",
209
+ "sandbox.create",
210
+ "workspace.baseline",
211
+ "eval.setup",
212
+ "agent.setup",
213
+ "eval.run",
214
+ "workspace.diff",
215
+ "scoring.evaluate",
216
+ ]);
217
+ });
218
+
219
+ it("没有 eval.setup / agent.setup 时,对应阶段整个不出现(不是出现后立刻跳过的空事件)", async () => {
220
+ const agent = defineSandboxAgent({
221
+ name: "fake-agent-no-setup",
222
+ send: async () => ({ events: [], status: "completed" }),
223
+ });
224
+
225
+ const phases: LifecyclePhase[] = [];
226
+ const box = new FakeSandbox();
227
+ await runOnce(agent, box, { onPhase: (phase) => phases.push(phase) });
228
+
229
+ expect(phases).toEqual(["sandbox.queue", "sandbox.create", "workspace.baseline", "eval.run", "workspace.diff", "scoring.evaluate"]);
230
+ });
231
+
232
+ it("test() 抛出的普通执行错误不设置 skipReason,diff/scoring 仍照常进入", async () => {
233
+ const agent = defineSandboxAgent({
234
+ name: "fake-agent-throws",
235
+ send: async () => ({ events: [], status: "completed" }),
236
+ });
237
+
238
+ const phases: LifecyclePhase[] = [];
239
+ const box = new FakeSandbox();
240
+ const result = await runOnce(agent, box, {
241
+ evalDefOverrides: {
242
+ test: () => {
243
+ throw new Error("boom-from-eval");
244
+ },
245
+ },
246
+ onPhase: (phase) => phases.push(phase),
247
+ });
248
+
249
+ expect(result.error?.message).toContain("boom-from-eval");
250
+ expect(result.error?.phase).toBe("eval.run");
251
+ // test() 里的普通异常被 runAttemptBody 内层 try/catch 收作 result.error,不设置
252
+ // skipReason——所以 diff/scoring 的跳过条件(`!skipReason`)不成立,两个阶段仍会进入,
253
+ // 最后落 teardown。这是「running 阶段失败」的真实序列。
254
+ expect(phases).toEqual(["sandbox.queue", "sandbox.create", "workspace.baseline", "eval.run", "workspace.diff", "scoring.evaluate"]);
255
+ });
256
+
257
+ it("agent.setup 中途抛错时,phase 序列停在 agent-setup 就跳进 teardown(不会假装跑到了 running)", async () => {
258
+ const agent = defineSandboxAgent({
259
+ name: "fake-agent-setup-throws",
260
+ setup: async () => {
261
+ throw new Error("boom-from-setup");
262
+ },
263
+ send: async () => ({ events: [], status: "completed" }),
264
+ });
265
+
266
+ const phases: LifecyclePhase[] = [];
267
+ const box = new FakeSandbox();
268
+ const result = await runOnce(agent, box, { onPhase: (phase) => phases.push(phase) });
269
+
270
+ expect(result.error?.message).toContain("boom-from-setup");
271
+ expect(result.error?.phase).toBe("agent.setup");
272
+ // 失败发生在 agent-setup:之后不再出现 running/diff/scoring —— run.ts 的 reportFailure()
273
+ // 靠的正是这个真实的「最后已知阶段」,不是硬编码成 running(见 run.ts 的 lastPhase 注释)。
274
+ expect(phases).toEqual(["sandbox.queue", "sandbox.create", "workspace.baseline", "agent.setup"]);
275
+ });
276
+ });
277
+
278
+ describe("runAttemptEffect · 主链与 Scope 收尾的计时边界", () => {
279
+ it("sandbox.stop 只计入收尾,主链 phase 合计不超过 durationMs", async () => {
280
+ const agent = defineSandboxAgent({
281
+ name: "fake-agent-timed-stop",
282
+ send: async () => ({ events: [], status: "completed" }),
283
+ });
284
+ const result = await runOnce(agent, new FakeSandbox(40));
285
+ const phases = result.phases ?? [];
286
+ const closing = new Set<LifecyclePhase>([
287
+ "eval.teardown",
288
+ "agent.teardown",
289
+ "sandbox.teardown",
290
+ "sandbox.suspend",
291
+ "sandbox.stop",
292
+ ]);
293
+ const mainDurationMs = phases
294
+ .filter((phase) => !closing.has(phase.name))
295
+ .reduce((sum, phase) => sum + phase.durationMs, 0);
296
+ const stop = phases.find((phase) => phase.name === "sandbox.stop");
297
+
298
+ expect(stop?.durationMs).toBeGreaterThanOrEqual(30);
299
+ expect(mainDurationMs).toBeLessThanOrEqual(result.durationMs);
300
+ });
301
+ });