niceeval 0.5.3 → 0.6.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.
- package/INDEX.md +55 -0
- package/dist/agents/types.d.ts +317 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/execution-tree.d.ts +103 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/select.d.ts +22 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +178 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +618 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +32 -0
- package/dist/report/format.js +126 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +83 -0
- package/dist/report/locale.js +160 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +20 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +10 -0
- package/dist/report/react/ExperimentList.js +15 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +100 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +393 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +503 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +420 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/annotated-source.d.ts +61 -0
- package/dist/results/attempt-evidence.d.ts +69 -0
- package/dist/results/attempt-source.d.ts +15 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +9 -4
- package/docs-site/zh/concepts/assert.mdx +8 -8
- package/docs-site/zh/concepts/evals.mdx +5 -5
- package/docs-site/zh/concepts/experiment.mdx +3 -1
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +2 -2
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -0
- package/docs-site/zh/guides/fixtures.mdx +1 -1
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +4 -4
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +35 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
- package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
- package/docs-site/zh/guides/viewing-results.mdx +199 -79
- package/docs-site/zh/guides/write-experiment.mdx +23 -1
- package/docs-site/zh/guides/write-send.mdx +2 -2
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +24 -12
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +7 -1
- package/docs-site/zh/reference/expect.mdx +3 -3
- package/package.json +10 -6
- package/src/agents/bub.ts +134 -43
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +11 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +62 -2
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/context.ts +3 -0
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +26 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +82 -0
- package/src/define.ts +72 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +36 -15
- package/src/i18n/zh-CN.ts +39 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +643 -0
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +223 -50
- package/src/report/compute.ts +358 -53
- package/src/report/dual-render.test.tsx +1125 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +50 -2
- package/src/report/index.ts +48 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +206 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +115 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +108 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +136 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +129 -0
- package/src/report/react/fixtures.ts +163 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +22 -6
- package/src/report/react/render.test.tsx +164 -31
- package/src/report/react/styles.css +405 -213
- package/src/report/report.test.ts +403 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +303 -95
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +196 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +97 -15
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +69 -28
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +125 -94
- package/src/show/render.ts +660 -160
- package/src/show/show.test.ts +472 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// 路径提升单测:agent.setup 写进沙箱 `__niceeval__/agent-setup.json` 的安装 manifest,
|
|
2
|
+
// runAttemptEffect 在 setup 之后把它读出来、原样挂到 EvalResult.agentSetup(见
|
|
3
|
+
// docs/feature/results/architecture.md「agent-setup.json」、src/agents/manifest.ts 的注释)。
|
|
4
|
+
// 沙箱是内存 fake(记文件,不起容器)——这里要验的是运行器自己「何时读、读到什么、读不到
|
|
5
|
+
// 怎么办」这段编排逻辑,不是 adapter 侧的 manifest 构造规则(那部分已在 agents/skills.test.ts
|
|
6
|
+
// 覆盖)。
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { Effect } from "effect";
|
|
10
|
+
import { runAttemptEffect } from "./attempt.ts";
|
|
11
|
+
import { defineSandboxAgent, defineSandbox } from "../define.ts";
|
|
12
|
+
import { writeAgentSetupManifest, AGENT_SETUP_MANIFEST_PATH } from "../agents/manifest.ts";
|
|
13
|
+
import type { CapturedEvalSource } from "./eval-source.ts";
|
|
14
|
+
import type { Attempt, AgentRun, RunOptions } from "./types.ts";
|
|
15
|
+
import type {
|
|
16
|
+
AgentSetupManifest,
|
|
17
|
+
Agent,
|
|
18
|
+
CommandResult,
|
|
19
|
+
Config,
|
|
20
|
+
DiscoveredEval,
|
|
21
|
+
Sandbox,
|
|
22
|
+
SandboxFile,
|
|
23
|
+
} from "../types.ts";
|
|
24
|
+
|
|
25
|
+
/** 内存沙箱:writeFiles/readFile 记文件,runShell 恒成功(供 initGitAndCommit / diff 采集用)。 */
|
|
26
|
+
class FakeSandbox implements Partial<Sandbox> {
|
|
27
|
+
readonly workdir = "/workspace";
|
|
28
|
+
readonly sandboxId = "fake";
|
|
29
|
+
readonly otlpHost = null;
|
|
30
|
+
readonly files = new Map<string, string>();
|
|
31
|
+
|
|
32
|
+
async runShell(): Promise<CommandResult> {
|
|
33
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
34
|
+
}
|
|
35
|
+
async runCommand(): Promise<CommandResult> {
|
|
36
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
37
|
+
}
|
|
38
|
+
async writeFiles(files: Record<string, string>, targetDir?: string): Promise<void> {
|
|
39
|
+
for (const [path, content] of Object.entries(files)) {
|
|
40
|
+
this.files.set(targetDir ? `${targetDir}/${path}` : path, content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async uploadFiles(files: SandboxFile[], targetDir?: string): Promise<void> {
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
this.files.set(targetDir ? `${targetDir}/${f.path}` : f.path, f.content.toString());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async uploadFile(path: string, content: Buffer): Promise<void> {
|
|
49
|
+
this.files.set(path, content.toString());
|
|
50
|
+
}
|
|
51
|
+
async uploadDirectory(): Promise<void> {}
|
|
52
|
+
async downloadFile(path: string): Promise<Buffer> {
|
|
53
|
+
return Buffer.from(this.files.get(path) ?? "");
|
|
54
|
+
}
|
|
55
|
+
async fileExists(path: string): Promise<boolean> {
|
|
56
|
+
return this.files.has(path);
|
|
57
|
+
}
|
|
58
|
+
async readFile(path: string): Promise<string> {
|
|
59
|
+
const hit = this.files.get(path);
|
|
60
|
+
if (hit === undefined) throw new Error(`no such file: ${path}`);
|
|
61
|
+
return hit;
|
|
62
|
+
}
|
|
63
|
+
async readSourceFiles(): Promise<never> {
|
|
64
|
+
throw new Error("not implemented");
|
|
65
|
+
}
|
|
66
|
+
async stop(): Promise<void> {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const asSandbox = (box: FakeSandbox): Sandbox => box as unknown as Sandbox;
|
|
70
|
+
|
|
71
|
+
const source: CapturedEvalSource = { path: "fake.eval.ts", content: "", sha256: "0".repeat(64) };
|
|
72
|
+
|
|
73
|
+
/** 跑一次 attempt:给定 agent,返回 EvalResult。沙箱用内存 fake,不起容器/不联网。 */
|
|
74
|
+
async function runOnce(agent: Agent, box: FakeSandbox): Promise<import("../types.ts").EvalResult> {
|
|
75
|
+
const evalDef: DiscoveredEval = {
|
|
76
|
+
id: "fake/eval",
|
|
77
|
+
baseDir: "/project",
|
|
78
|
+
sourcePath: "/project/fake.eval.ts",
|
|
79
|
+
source,
|
|
80
|
+
test: () => {},
|
|
81
|
+
};
|
|
82
|
+
const run: AgentRun = {
|
|
83
|
+
agent,
|
|
84
|
+
flags: {},
|
|
85
|
+
runs: 1,
|
|
86
|
+
earlyExit: true,
|
|
87
|
+
// 自定义 provider:create() 直接返回内存 fake,绕开真实沙箱 provider。
|
|
88
|
+
sandbox: defineSandbox({ name: "fake-provider", create: async () => asSandbox(box) }),
|
|
89
|
+
timeoutMs: 5_000,
|
|
90
|
+
evalFilter: () => true,
|
|
91
|
+
};
|
|
92
|
+
const attempt: Attempt = { evalDef, run, attempt: 0, key: "fake/eval", fingerprint: "" };
|
|
93
|
+
const config: Config = {};
|
|
94
|
+
const opts: RunOptions = {
|
|
95
|
+
config,
|
|
96
|
+
evals: [evalDef],
|
|
97
|
+
agentRuns: [run],
|
|
98
|
+
reporters: [],
|
|
99
|
+
maxConcurrency: 1,
|
|
100
|
+
onProgress: () => {}, // 静音进度日志,不污染测试输出
|
|
101
|
+
};
|
|
102
|
+
const sandboxSem = Effect.runSync(Effect.makeSemaphore(1));
|
|
103
|
+
return Effect.runPromise(runAttemptEffect(attempt, opts, sandboxSem));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("runAttemptEffect · agent-setup 路径提升(沙箱 __niceeval__/agent-setup.json → EvalResult.agentSetup)", () => {
|
|
107
|
+
it("沙箱内有 manifest 时,原样读出挂到 EvalResult.agentSetup(不做任何转换/裁剪)", async () => {
|
|
108
|
+
const manifest: AgentSetupManifest = {
|
|
109
|
+
skills: [
|
|
110
|
+
{ kind: "local", name: "effect-ts", path: "skills/effect-ts", sha256: "a".repeat(64) },
|
|
111
|
+
{ kind: "repo", source: "anthropics/skills", ref: "9d2f1ae187231d8199c64b5b762e1bdf2244733d", skills: ["pdf", "docx"] },
|
|
112
|
+
],
|
|
113
|
+
nativePlugins: [
|
|
114
|
+
{
|
|
115
|
+
agent: "claude-code",
|
|
116
|
+
marketplace: { name: "duyet", source: "duyet/codex-claude-plugins", ref: "82de4021a311034a9596e891baf3a8266fb33bf7" },
|
|
117
|
+
name: "example-plugin",
|
|
118
|
+
resolvedVersion: "1.2.3",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
mcpServers: [{ name: "fs", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem"] }],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const agent = defineSandboxAgent({
|
|
125
|
+
name: "fake-agent",
|
|
126
|
+
setup: async (sandbox) => {
|
|
127
|
+
await writeAgentSetupManifest(sandbox, manifest);
|
|
128
|
+
},
|
|
129
|
+
send: async () => ({ events: [], status: "completed" }),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const box = new FakeSandbox();
|
|
133
|
+
const result = await runOnce(agent, box);
|
|
134
|
+
|
|
135
|
+
expect(result.error).toBeUndefined();
|
|
136
|
+
// 沙箱内确实落了这个文件(否则下面的断言测不出"提升"这一步真的发生了)。
|
|
137
|
+
expect(box.files.has(`${box.workdir}/${AGENT_SETUP_MANIFEST_PATH}`)).toBe(true);
|
|
138
|
+
expect(result.agentSetup).toEqual(manifest); // 深相等:内容原样保留,没有裁剪或改形。
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("沙箱内没有 manifest 时(没装任何 Skill/plugin/MCP 的基线场景),不生成空/伪造的 artifact", async () => {
|
|
142
|
+
const agent = defineSandboxAgent({
|
|
143
|
+
name: "fake-agent-no-install",
|
|
144
|
+
// agent.setup 跑了(比如只装了 CLI 本体),但没有任何 skill/plugin/mcp 可写,
|
|
145
|
+
// 所以从不调用 writeAgentSetupManifest —— 这是「基线场景」的真实形状。
|
|
146
|
+
setup: async () => {},
|
|
147
|
+
send: async () => ({ events: [], status: "completed" }),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const box = new FakeSandbox();
|
|
151
|
+
const result = await runOnce(agent, box);
|
|
152
|
+
|
|
153
|
+
expect(result.error).toBeUndefined();
|
|
154
|
+
expect(box.files.has(`${box.workdir}/${AGENT_SETUP_MANIFEST_PATH}`)).toBe(false);
|
|
155
|
+
expect(result.agentSetup).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("agent 根本没有 setup 钩子时(非 coding agent adapter),同样不生成 agentSetup", async () => {
|
|
159
|
+
const agent = defineSandboxAgent({
|
|
160
|
+
name: "fake-agent-no-setup",
|
|
161
|
+
send: async () => ({ events: [], status: "completed" }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const box = new FakeSandbox();
|
|
165
|
+
const result = await runOnce(agent, box);
|
|
166
|
+
|
|
167
|
+
expect(result.error).toBeUndefined();
|
|
168
|
+
expect(result.agentSetup).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
package/src/runner/attempt.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 单个 attempt 的完整生命周期:资源(沙箱 / OTLP 接收器)经 Effect.Scope 的
|
|
2
2
|
// acquireRelease 接管,无论 body 成功 / 抛错 / 被中断,stop() / close() 都保证执行。
|
|
3
|
-
// 沙箱编排的固定段在 runAttemptBody(基线→setup→驱动 test→采 diff
|
|
3
|
+
// 沙箱编排的固定段在 runAttemptBody(基线→setup→驱动 test→采 diff→评分→判定→收 trace),
|
|
4
4
|
// adapter 只填「把 agent 跑起来」一段。
|
|
5
5
|
|
|
6
6
|
import { resolve as resolvePath } from "node:path";
|
|
@@ -14,16 +14,21 @@ import { selectTraceSpans, enrichTraceWithIO } from "../o11y/otlp/select.ts";
|
|
|
14
14
|
import { mapGenericSpans } from "../o11y/otlp/mappers/index.ts";
|
|
15
15
|
import { createEvalContext } from "../context/context.ts";
|
|
16
16
|
import { createAgentSession } from "../context/session.ts";
|
|
17
|
+
import { readAgentSetupManifest } from "../agents/manifest.ts";
|
|
17
18
|
import { EvalRequirementFailed, EvalSkipped, TurnFailed } from "../context/control-flow.ts";
|
|
18
|
-
import {
|
|
19
|
+
import { computeVerdict } from "../scoring/verdict.ts";
|
|
19
20
|
import { deriveRunFacts, buildO11ySummary } from "../o11y/derive.ts";
|
|
20
21
|
import { estimateCost } from "../o11y/cost.ts";
|
|
21
22
|
import { t } from "../i18n/index.ts";
|
|
22
23
|
import { formatThrown } from "../util.ts";
|
|
23
24
|
import { captureGeneratedFiles, initGitAndCommit } from "./sandbox-prep.ts";
|
|
24
25
|
import { createRemoteSandbox, withEvalLocalPaths } from "./remote-sandbox.ts";
|
|
26
|
+
import type { CapturedEvalSource } from "./eval-source.ts";
|
|
25
27
|
import type {
|
|
26
28
|
AgentContext,
|
|
29
|
+
AgentSetup,
|
|
30
|
+
AgentSetupManifest,
|
|
31
|
+
AgentTeardown,
|
|
27
32
|
Cleanup,
|
|
28
33
|
Config,
|
|
29
34
|
EvalResult,
|
|
@@ -56,7 +61,7 @@ export function runAttemptEffect(
|
|
|
56
61
|
experiment: experimentRunInfo(run),
|
|
57
62
|
agent: run.agent.name,
|
|
58
63
|
model: run.model,
|
|
59
|
-
|
|
64
|
+
verdict: "errored",
|
|
60
65
|
fingerprint: a.fingerprint,
|
|
61
66
|
attempt,
|
|
62
67
|
startedAt: new Date(t0).toISOString(),
|
|
@@ -73,7 +78,7 @@ export function runAttemptEffect(
|
|
|
73
78
|
|
|
74
79
|
// 流式进度打到宿主 stderr(结果走 stdout,互不干扰)。容器主日志【不】放这些进度标记 ——
|
|
75
80
|
// 那里留给 agent 的原始输出(adapter 给 agent 命令开 { stream: true })。
|
|
76
|
-
const who = runWho(run);
|
|
81
|
+
const who = runWho({ agentName: run.agent.name, model: run.model, experimentId: run.experimentId });
|
|
77
82
|
// 同时保留最近 20 条进度消息,timeout 时嵌入 error 字段方便定位卡在哪一步。
|
|
78
83
|
const recentLogs: string[] = [];
|
|
79
84
|
const log = (m: string) => {
|
|
@@ -88,6 +93,15 @@ export function runAttemptEffect(
|
|
|
88
93
|
|
|
89
94
|
return Effect.scoped(
|
|
90
95
|
Effect.gen(function* () {
|
|
96
|
+
// run.sandbox ?? config.sandbox 是同一个 SandboxSpec 对象,既用来起沙箱 provider,
|
|
97
|
+
// 也是 sandbox.setup / sandbox.teardown 钩子(SandboxSpec.setup()/.teardown() 链式挂的)的来源。
|
|
98
|
+
const sandboxSpec = run.sandbox ?? config.sandbox;
|
|
99
|
+
// 退避重试(resolve.ts → retry.ts)期间临时归还这个名额:被限流的 provider 只是在
|
|
100
|
+
// setTimeout 里睡觉,不该攥着 sandboxSem 的槽位陪跑,不然一批 429 能把整体并发拖成个位数。
|
|
101
|
+
const provisionSlot = {
|
|
102
|
+
release: () => Effect.runPromise(sandboxSem.release(1)).then(() => {}),
|
|
103
|
+
reacquire: () => Effect.runPromise(sandboxSem.take(1)).then(() => {}),
|
|
104
|
+
};
|
|
91
105
|
const sandbox =
|
|
92
106
|
run.agent.kind === "sandbox"
|
|
93
107
|
? yield* sandboxSem.withPermits(1)(
|
|
@@ -96,7 +110,8 @@ export function runAttemptEffect(
|
|
|
96
110
|
// sandboxSem 只覆盖「容器创建」阶段;容器起好后立即释放,后续 npm install / agent 不占位。
|
|
97
111
|
log(t("runner.startSandbox"));
|
|
98
112
|
return yield* createSandbox({
|
|
99
|
-
sandbox:
|
|
113
|
+
sandbox: sandboxSpec,
|
|
114
|
+
provisionSlot,
|
|
100
115
|
timeout: timeoutMs,
|
|
101
116
|
runtime: "node24",
|
|
102
117
|
});
|
|
@@ -161,6 +176,8 @@ export function runAttemptEffect(
|
|
|
161
176
|
return yield* Effect.promise((interruptSignal) =>
|
|
162
177
|
runAttemptBody(a, config, t0, base, {
|
|
163
178
|
sandbox,
|
|
179
|
+
sandboxSetupHooks: sandboxSpec?.setupHooks ?? [],
|
|
180
|
+
sandboxTeardownHooks: sandboxSpec?.teardownHooks ?? [],
|
|
164
181
|
receiver,
|
|
165
182
|
telemetry,
|
|
166
183
|
otel: otelChannel,
|
|
@@ -203,6 +220,10 @@ function causeToError(cause: Cause.Cause<never>): string {
|
|
|
203
220
|
|
|
204
221
|
interface AttemptResources {
|
|
205
222
|
sandbox: Sandbox;
|
|
223
|
+
/** SandboxSpec.setup() 链式挂的钩子,按追加顺序;非沙箱 agent 传空数组(usesSandbox 挡住不会跑)。 */
|
|
224
|
+
sandboxSetupHooks: readonly AgentSetup[];
|
|
225
|
+
/** SandboxSpec.teardown() 链式挂的钩子,按追加顺序保存,执行时逆序。 */
|
|
226
|
+
sandboxTeardownHooks: readonly AgentTeardown[];
|
|
206
227
|
receiver?: TraceReceiver;
|
|
207
228
|
telemetry?: Telemetry;
|
|
208
229
|
/** 非沙箱 tracing agent 的共享 OTLP 通道(run 级池持有,不随 attempt 关)。 */
|
|
@@ -211,7 +232,7 @@ interface AttemptResources {
|
|
|
211
232
|
log: (m: string) => void;
|
|
212
233
|
}
|
|
213
234
|
|
|
214
|
-
// attempt 的固定段(上传→基线→setup→驱动 agent→采 diff
|
|
235
|
+
// attempt 的固定段(上传→基线→setup→驱动 agent→采 diff→脚本→评分→判定)。
|
|
215
236
|
// 资源已由 runAttemptEffect 的 Scope 持有;这里只在 finally 跑 agent 自己的 cleanup/teardown。
|
|
216
237
|
async function runAttemptBody(
|
|
217
238
|
a: Attempt,
|
|
@@ -221,7 +242,7 @@ async function runAttemptBody(
|
|
|
221
242
|
res: AttemptResources,
|
|
222
243
|
): Promise<EvalResult> {
|
|
223
244
|
const { evalDef, run, attempt } = a;
|
|
224
|
-
const { sandbox, receiver, telemetry, otel, signal, log } = res;
|
|
245
|
+
const { sandbox, sandboxSetupHooks, sandboxTeardownHooks, receiver, telemetry, otel, signal, log } = res;
|
|
225
246
|
const usesSandbox = run.agent.kind === "sandbox";
|
|
226
247
|
// 整个 attempt 共用一份 agent ctx(sandbox 钩子 / agent setup / tracing configure / teardown 都用它)。
|
|
227
248
|
const attemptCtx: AgentContext = {
|
|
@@ -229,6 +250,7 @@ async function runAttemptBody(
|
|
|
229
250
|
model: run.model,
|
|
230
251
|
reasoningEffort: run.reasoningEffort,
|
|
231
252
|
flags: run.flags,
|
|
253
|
+
experimentId: run.experimentId,
|
|
232
254
|
sandbox,
|
|
233
255
|
session: createAgentSession(),
|
|
234
256
|
telemetry,
|
|
@@ -236,15 +258,31 @@ async function runAttemptBody(
|
|
|
236
258
|
};
|
|
237
259
|
let agentCleanup: Cleanup | void = undefined;
|
|
238
260
|
let agentDidSetup = false;
|
|
261
|
+
/** agent.setup 写进沙箱的安装清单(装了 Skill / plugin / MCP 的沙箱型 adapter 才有)。 */
|
|
262
|
+
let agentSetup: AgentSetupManifest | undefined;
|
|
263
|
+
// EvalDef.setup() 返回的 cleanup 闭包;finally 里按 LIFO 跑(见下)。
|
|
264
|
+
let evalCleanup: Cleanup | void = undefined;
|
|
265
|
+
// SandboxSpec.setup() 返回的 cleanup 闭包,按调用顺序收集;finally 里 LIFO 跑(见下)。
|
|
266
|
+
const sandboxCleanups: Cleanup[] = [];
|
|
239
267
|
try {
|
|
240
268
|
if (usesSandbox) {
|
|
269
|
+
// 沙箱级生命周期钩子(SandboxSpec.setup):环境预置层,先于 workspace 上传 / git 基线 /
|
|
270
|
+
// eval.setup 跑——改动进 git 基线,不会被误算进 agent 产出的 diff。按追加顺序依次执行;
|
|
271
|
+
// 单个抛错走下面的执行错误路径(与 eval.setup / agent.setup 同一条),已跑过的 cleanup /
|
|
272
|
+
// sandbox.teardown 钩子仍在 finally 里跑(见 catch/finally)。
|
|
273
|
+
if (sandboxSetupHooks.length > 0) log(t("runner.startSandboxSetup"));
|
|
274
|
+
for (const hook of sandboxSetupHooks) {
|
|
275
|
+
const cleanup = await hook(sandbox, attemptCtx);
|
|
276
|
+
if (typeof cleanup === "function") sandboxCleanups.push(cleanup);
|
|
277
|
+
}
|
|
278
|
+
|
|
241
279
|
await initGitAndCommit(sandbox);
|
|
242
280
|
|
|
243
281
|
// eval 级 setup(starter prep:npm install / 装系统依赖等)。命令默认非 root;
|
|
244
282
|
// setup 里需要 root 的(apt/pip)自己传 { root: true }。
|
|
245
283
|
if (evalDef.setup) {
|
|
246
284
|
log(t("runner.evalSetup"));
|
|
247
|
-
await evalDef.setup(withEvalLocalPaths(sandbox, evalDef.baseDir));
|
|
285
|
+
evalCleanup = await evalDef.setup(withEvalLocalPaths(sandbox, evalDef.baseDir));
|
|
248
286
|
}
|
|
249
287
|
}
|
|
250
288
|
|
|
@@ -255,6 +293,11 @@ async function runAttemptBody(
|
|
|
255
293
|
agentCleanup = await run.agent.setup(sandbox, attemptCtx);
|
|
256
294
|
}
|
|
257
295
|
|
|
296
|
+
// 安装 manifest:adapter 在 setup 收尾写进沙箱的固定路径,核心只把它抬成 attempt artifact
|
|
297
|
+
// (不解释内容、不按 agent 名字分支)。什么都没装的 adapter 不写这个文件 → undefined,
|
|
298
|
+
// 不生成空 artifact。读在 setup 之后:test 阶段抛错也留得住这份「这次装了什么」的证据。
|
|
299
|
+
if (usesSandbox && agentDidSetup) agentSetup = await readAgentSetupManifest(sandbox);
|
|
300
|
+
|
|
258
301
|
// OTLP 导出配置(file-based,如 codex 的 config.toml [otel] 块):与 setup 分开,
|
|
259
302
|
// 在主配置写完后追加。仅当 tracing 开 + 有 endpoint 时调一次(env-based 的不实现 configure)。
|
|
260
303
|
if (telemetry && run.agent.tracing?.configure) {
|
|
@@ -271,6 +314,7 @@ async function runAttemptBody(
|
|
|
271
314
|
model: run.model,
|
|
272
315
|
reasoningEffort: run.reasoningEffort,
|
|
273
316
|
flags: run.flags,
|
|
317
|
+
experimentId: run.experimentId,
|
|
274
318
|
signal,
|
|
275
319
|
log,
|
|
276
320
|
judge,
|
|
@@ -335,7 +379,7 @@ async function runAttemptBody(
|
|
|
335
379
|
};
|
|
336
380
|
if (!skipReason) log(t("runner.scoreJudge"));
|
|
337
381
|
const assertions = skipReason ? [] : await state.collector.finalize(scoringContext);
|
|
338
|
-
const
|
|
382
|
+
const verdict = computeVerdict({ error, assertions, skipReason, strict: run.strict });
|
|
339
383
|
|
|
340
384
|
// 收 OTLP trace:给最后一批导出留点落地时间,再 collect(空则不挂)。
|
|
341
385
|
// codex 的 OTLP 把内部 Rust tracing 全导出来(handle_responses / append_items … 上万条);
|
|
@@ -373,7 +417,7 @@ async function runAttemptBody(
|
|
|
373
417
|
if (cost !== undefined) o11y.estimatedCostUSD = cost;
|
|
374
418
|
|
|
375
419
|
// 收 test 引用到的 eval 源码(按 send / 断言的 loc 去重),供 view 渲染代码视图。
|
|
376
|
-
const sources = await collectSources(events, assertions);
|
|
420
|
+
const sources = await collectSources(events, assertions, evalDef.source);
|
|
377
421
|
|
|
378
422
|
return {
|
|
379
423
|
id: evalDef.id,
|
|
@@ -382,7 +426,7 @@ async function runAttemptBody(
|
|
|
382
426
|
experiment: experimentRunInfo(run),
|
|
383
427
|
agent: run.agent.name,
|
|
384
428
|
model: run.model,
|
|
385
|
-
|
|
429
|
+
verdict,
|
|
386
430
|
fingerprint: a.fingerprint,
|
|
387
431
|
attempt,
|
|
388
432
|
startedAt: new Date(t0).toISOString(),
|
|
@@ -396,6 +440,7 @@ async function runAttemptBody(
|
|
|
396
440
|
sources,
|
|
397
441
|
o11y,
|
|
398
442
|
trace,
|
|
443
|
+
agentSetup,
|
|
399
444
|
diff,
|
|
400
445
|
};
|
|
401
446
|
} catch (e) {
|
|
@@ -403,10 +448,12 @@ async function runAttemptBody(
|
|
|
403
448
|
...base,
|
|
404
449
|
durationMs: Date.now() - t0,
|
|
405
450
|
error: formatThrown(e),
|
|
451
|
+
...(agentSetup !== undefined ? { agentSetup } : {}),
|
|
406
452
|
};
|
|
407
453
|
} finally {
|
|
408
|
-
// teardown / cleanup 一律在 finally 跑(失败也跑)
|
|
409
|
-
// LIFO
|
|
454
|
+
// teardown / cleanup 一律在 finally 跑(失败也跑),不改判定,各自兜错(diagnostic)。
|
|
455
|
+
// LIFO:agent 级(setup 最晚 → cleanup / teardown 先跑)在前,sandbox 级(最早就绪)收尾在后
|
|
456
|
+
//(即 sandbox.setup 返回的 cleanup、sandbox.teardown 钩子最后跑,沙箱销毁前)。
|
|
410
457
|
// 沙箱 stop / 接收器 close 不在这里 —— 由 runAttemptEffect 的 Scope 在本函数返回后回收。
|
|
411
458
|
try {
|
|
412
459
|
if (typeof agentCleanup === "function") await agentCleanup();
|
|
@@ -414,22 +461,57 @@ async function runAttemptBody(
|
|
|
414
461
|
} catch {
|
|
415
462
|
// teardown 失败只是 diagnostic,不影响已出的结果
|
|
416
463
|
}
|
|
464
|
+
try {
|
|
465
|
+
// eval.setup 返回的 cleanup:排在 agent 级之后、sandbox 级之前(LIFO,与 setup 顺序对称)。
|
|
466
|
+
if (typeof evalCleanup === "function") await evalCleanup();
|
|
467
|
+
} catch {
|
|
468
|
+
// 同上,只作 diagnostic
|
|
469
|
+
}
|
|
470
|
+
if (usesSandbox) {
|
|
471
|
+
// sandbox.setup 返回的 cleanup:LIFO(后 setup 先 cleanup),与 agent 级同构。
|
|
472
|
+
for (let i = sandboxCleanups.length - 1; i >= 0; i--) {
|
|
473
|
+
try {
|
|
474
|
+
await sandboxCleanups[i]();
|
|
475
|
+
} catch {
|
|
476
|
+
// 同上,只作 diagnostic
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// sandbox.teardown 钩子:按追加的逆序执行,沙箱销毁前最后一步。
|
|
480
|
+
if (sandboxTeardownHooks.length > 0) {
|
|
481
|
+
log(t("runner.startSandboxTeardown"));
|
|
482
|
+
for (let i = sandboxTeardownHooks.length - 1; i >= 0; i--) {
|
|
483
|
+
try {
|
|
484
|
+
await sandboxTeardownHooks[i](sandbox, attemptCtx);
|
|
485
|
+
} catch {
|
|
486
|
+
// 同上,只作 diagnostic
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
417
491
|
}
|
|
418
492
|
}
|
|
419
493
|
|
|
420
494
|
/**
|
|
421
|
-
* 收集 test 引用到的 eval 源码:从 send(user message)与断言的 loc
|
|
422
|
-
*
|
|
495
|
+
* 收集 test 引用到的 eval 源码:从 send(user message)与断言的 loc 去重出文件集。
|
|
496
|
+
* 命中 eval 自己的定义文件(绝大多数情况——send / 断言几乎总在 eval 主体里直接调用)时,
|
|
497
|
+
* 直接用 discovery 时已经读好、归一化、算过哈希的 `evalSource`,不重新读盘;loc 指向
|
|
498
|
+
* 其它文件(共享 helper 里包装 t.send / 断言的少见情形)才现读现取,读不到就跳过
|
|
499
|
+
* (路径在沙箱内 / 已删 / 权限),view 用 loc 也能降级显示行号。
|
|
423
500
|
*/
|
|
424
501
|
async function collectSources(
|
|
425
502
|
events: readonly StreamEvent[],
|
|
426
503
|
assertions: readonly EvalResult["assertions"][number][],
|
|
504
|
+
evalSource: CapturedEvalSource,
|
|
427
505
|
): Promise<SourceArtifact[]> {
|
|
428
506
|
const paths = new Set<string>();
|
|
429
507
|
for (const e of events) if (e.type === "message" && e.loc) paths.add(e.loc.file);
|
|
430
508
|
for (const a of assertions) if (a.loc) paths.add(a.loc.file);
|
|
431
509
|
const out: SourceArtifact[] = [];
|
|
432
510
|
for (const path of paths) {
|
|
511
|
+
if (path === evalSource.path) {
|
|
512
|
+
out.push({ path, content: evalSource.content });
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
433
515
|
try {
|
|
434
516
|
out.push({ path, content: await readSourceFile(resolvePath(process.cwd(), path), "utf-8") });
|
|
435
517
|
} catch {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// discoverEvals 的 eval 源码捕获:同一文件(数组默认导出)共享一份 CapturedEvalSource,
|
|
2
|
+
// 内容/路径/哈希与 captureEvalSource() 直接调出来的一致(定稿见 docs/concepts.md「标注 Eval 源码」)。
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it, afterEach } from "vitest";
|
|
5
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { discoverEvals } from "./discover.ts";
|
|
9
|
+
import { captureEvalSource } from "./eval-source.ts";
|
|
10
|
+
|
|
11
|
+
const roots: string[] = [];
|
|
12
|
+
async function makeRoot(): Promise<string> {
|
|
13
|
+
const root = await mkdtemp(join(tmpdir(), "niceeval-discover-"));
|
|
14
|
+
roots.push(root);
|
|
15
|
+
return root;
|
|
16
|
+
}
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await Promise.all(roots.splice(0).map((r) => rm(r, { recursive: true, force: true })));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("discoverEvals · 源码捕获", () => {
|
|
22
|
+
it("单个默认导出:source 与 captureEvalSource() 直接调出来的一致(路径/内容/哈希)", async () => {
|
|
23
|
+
const root = await makeRoot();
|
|
24
|
+
await mkdir(join(root, "evals"), { recursive: true });
|
|
25
|
+
const file = join(root, "evals", "hello.eval.ts");
|
|
26
|
+
await writeFile(file, 'export default {\n test() {},\n};\n', "utf-8");
|
|
27
|
+
|
|
28
|
+
const evals = await discoverEvals(root);
|
|
29
|
+
expect(evals).toHaveLength(1);
|
|
30
|
+
const expected = await captureEvalSource(file, { root });
|
|
31
|
+
expect(evals[0]!.source).toEqual(expected);
|
|
32
|
+
expect(evals[0]!.source.path).toBe("evals/hello.eval.ts");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("数组默认导出:多个 eval 共享同一份 CapturedEvalSource 引用(同哈希,同一个文件只读一次)", async () => {
|
|
36
|
+
const root = await makeRoot();
|
|
37
|
+
await mkdir(join(root, "evals"), { recursive: true });
|
|
38
|
+
const file = join(root, "evals", "batch.eval.ts");
|
|
39
|
+
await writeFile(
|
|
40
|
+
file,
|
|
41
|
+
"export default [\n { test() {} },\n { test() {} },\n];\n",
|
|
42
|
+
"utf-8",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const evals = await discoverEvals(root);
|
|
46
|
+
expect(evals.map((e) => e.id)).toEqual(["batch/0000", "batch/0001"]);
|
|
47
|
+
expect(evals[0]!.source).toBe(evals[1]!.source); // 同一份引用,不是内容相等的两份拷贝
|
|
48
|
+
expect(evals[0]!.source.sha256).toHaveLength(64);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("CRLF 源码归一化后哈希与 LF 版本一致(discovery 侧与 collectSources/annotated-source 共用归一化)", async () => {
|
|
52
|
+
const rootLf = await makeRoot();
|
|
53
|
+
const rootCrlf = await makeRoot();
|
|
54
|
+
await mkdir(join(rootLf, "evals"), { recursive: true });
|
|
55
|
+
await mkdir(join(rootCrlf, "evals"), { recursive: true });
|
|
56
|
+
const body = "export default {\n test() {},\n};\n";
|
|
57
|
+
await writeFile(join(rootLf, "evals", "a.eval.ts"), body, "utf-8");
|
|
58
|
+
await writeFile(join(rootCrlf, "evals", "a.eval.ts"), body.replace(/\n/g, "\r\n"), "utf-8");
|
|
59
|
+
|
|
60
|
+
const [lf] = await discoverEvals(rootLf);
|
|
61
|
+
const [crlf] = await discoverEvals(rootCrlf);
|
|
62
|
+
expect(lf!.source.sha256).toBe(crlf!.source.sha256);
|
|
63
|
+
expect(crlf!.source.content).toBe(body); // 归一化后不含 \r
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/runner/discover.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { readdir } from "node:fs/promises";
|
|
|
5
5
|
import { dirname, join, relative, sep } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { pad4 } from "../util.ts";
|
|
8
|
+
import { captureEvalSource } from "./eval-source.ts";
|
|
8
9
|
import type { DiscoveredEval, DiscoveredExperiment, EvalDef, ExperimentDef } from "../types.ts";
|
|
9
10
|
|
|
10
11
|
const SKIP_DIRS = new Set(["node_modules", ".git", ".niceeval", "dist", ".next"]);
|
|
@@ -42,10 +43,13 @@ export async function discoverEvals(root: string): Promise<DiscoveredEval[]> {
|
|
|
42
43
|
if (!def) continue;
|
|
43
44
|
const baseId = relative(dir, file).replace(/\.eval\.tsx?$/, "").split(sep).join("/");
|
|
44
45
|
const baseDir = dirname(file);
|
|
46
|
+
// discovery 时读一次、归一化、算 SHA-256:同一文件(数组默认导出多个 eval)只读一次盘,
|
|
47
|
+
// 全部共享同一份 CapturedEvalSource 引用——写入面按哈希去重靠的就是这份内容天然相同。
|
|
48
|
+
const source = await captureEvalSource(file, { root });
|
|
45
49
|
if (Array.isArray(def)) {
|
|
46
|
-
def.forEach((d, i) => out.push({ ...d, id: `${baseId}/${pad4(i)}`, baseDir, sourcePath: file }));
|
|
50
|
+
def.forEach((d, i) => out.push({ ...d, id: `${baseId}/${pad4(i)}`, baseDir, sourcePath: file, source }));
|
|
47
51
|
} else {
|
|
48
|
-
out.push({ ...def, id: baseId, baseDir, sourcePath: file });
|
|
52
|
+
out.push({ ...def, id: baseId, baseDir, sourcePath: file, source });
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
return out;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// captureEvalSource 的单测(定稿见 docs/feature/results/architecture.md「sources.json」)。
|
|
2
|
+
// 覆盖:哈希确定性、path 相对 root 计算、CRLF/BOM 归一化行为、与 results/source-hash.ts
|
|
3
|
+
// 算法保持一致(两处哈希必须逐字节相同,见 eval-source.ts 顶部注释)。
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { hashEvalSource, normalizeEvalSource } from "../results/source-hash.ts";
|
|
10
|
+
import { captureEvalSource } from "./eval-source.ts";
|
|
11
|
+
|
|
12
|
+
const roots: string[] = [];
|
|
13
|
+
async function makeRoot(): Promise<string> {
|
|
14
|
+
const root = await mkdtemp(join(tmpdir(), "niceeval-eval-source-"));
|
|
15
|
+
roots.push(root);
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await Promise.all(roots.splice(0).map((r) => rm(r, { recursive: true, force: true })));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("captureEvalSource", () => {
|
|
23
|
+
it("computes a deterministic sha256 for the same file content", async () => {
|
|
24
|
+
const root = await makeRoot();
|
|
25
|
+
await mkdir(join(root, "evals"), { recursive: true });
|
|
26
|
+
const file = join(root, "evals", "weather.eval.ts");
|
|
27
|
+
await writeFile(file, "export default { id: 'weather' };\n", "utf-8");
|
|
28
|
+
|
|
29
|
+
const first = await captureEvalSource(file, { root });
|
|
30
|
+
const second = await captureEvalSource(file, { root });
|
|
31
|
+
|
|
32
|
+
expect(first.sha256).toBe(second.sha256);
|
|
33
|
+
expect(first.sha256).toMatch(/^[0-9a-f]{64}$/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns a path relative to the given root, using forward slashes", async () => {
|
|
37
|
+
const root = await makeRoot();
|
|
38
|
+
await mkdir(join(root, "evals", "nested"), { recursive: true });
|
|
39
|
+
const file = join(root, "evals", "nested", "weather.eval.ts");
|
|
40
|
+
await writeFile(file, "x\n", "utf-8");
|
|
41
|
+
|
|
42
|
+
const captured = await captureEvalSource(file, { root });
|
|
43
|
+
|
|
44
|
+
expect(captured.path).toBe("evals/nested/weather.eval.ts");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defaults root to process.cwd() when omitted", async () => {
|
|
48
|
+
const root = await makeRoot();
|
|
49
|
+
const cwdBefore = process.cwd();
|
|
50
|
+
process.chdir(root);
|
|
51
|
+
try {
|
|
52
|
+
// 用 chdir 之后的 process.cwd() 拼绝对路径:mkdtemp 返回的 root 与 chdir 后的
|
|
53
|
+
// process.cwd() 在 macOS 上可能因 /var → /private/var 的符号链接解析而不是同一字符串,
|
|
54
|
+
// 直接比较 root 会因这条无关的平台差异误报,不是本函数要覆盖的行为。
|
|
55
|
+
const file = join(process.cwd(), "weather.eval.ts");
|
|
56
|
+
await writeFile(file, "x\n", "utf-8");
|
|
57
|
+
const captured = await captureEvalSource(file);
|
|
58
|
+
expect(captured.path).toBe("weather.eval.ts");
|
|
59
|
+
} finally {
|
|
60
|
+
process.chdir(cwdBefore);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("normalizes CRLF to LF so cross-platform checkouts hash identically", async () => {
|
|
65
|
+
const root = await makeRoot();
|
|
66
|
+
await mkdir(join(root, "evals"), { recursive: true });
|
|
67
|
+
const lfFile = join(root, "evals", "lf.eval.ts");
|
|
68
|
+
const crlfFile = join(root, "evals", "crlf.eval.ts");
|
|
69
|
+
const body = "line one\nline two\nline three\n";
|
|
70
|
+
await writeFile(lfFile, body, "utf-8");
|
|
71
|
+
await writeFile(crlfFile, body.replace(/\n/g, "\r\n"), "utf-8");
|
|
72
|
+
|
|
73
|
+
const lf = await captureEvalSource(lfFile, { root });
|
|
74
|
+
const crlf = await captureEvalSource(crlfFile, { root });
|
|
75
|
+
|
|
76
|
+
expect(crlf.content).toBe(lf.content);
|
|
77
|
+
expect(crlf.sha256).toBe(lf.sha256);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("strips a UTF-8 BOM from the captured content", async () => {
|
|
81
|
+
const root = await makeRoot();
|
|
82
|
+
await mkdir(join(root, "evals"), { recursive: true });
|
|
83
|
+
const withBom = join(root, "evals", "bom.eval.ts");
|
|
84
|
+
const withoutBom = join(root, "evals", "no-bom.eval.ts");
|
|
85
|
+
await writeFile(withBom, "export default {};\n", "utf-8");
|
|
86
|
+
await writeFile(withoutBom, "export default {};\n", "utf-8");
|
|
87
|
+
|
|
88
|
+
const a = await captureEvalSource(withBom, { root });
|
|
89
|
+
const b = await captureEvalSource(withoutBom, { root });
|
|
90
|
+
|
|
91
|
+
expect(a.content).toBe(b.content);
|
|
92
|
+
expect(a.content.startsWith("")).toBe(false);
|
|
93
|
+
expect(a.sha256).toBe(b.sha256);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("matches results/source-hash.ts's normalize+hash algorithm exactly", async () => {
|
|
97
|
+
const root = await makeRoot();
|
|
98
|
+
const file = join(root, "weather.eval.ts");
|
|
99
|
+
const raw = "export default {};\r\n// comment\r\n";
|
|
100
|
+
await writeFile(file, raw, "utf-8");
|
|
101
|
+
|
|
102
|
+
const captured = await captureEvalSource(file, { root });
|
|
103
|
+
|
|
104
|
+
expect(captured.content).toBe(normalizeEvalSource(raw));
|
|
105
|
+
expect(captured.sha256).toBe(hashEvalSource(normalizeEvalSource(raw)));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("rejects two files with different content by producing different hashes", async () => {
|
|
109
|
+
const root = await makeRoot();
|
|
110
|
+
await mkdir(join(root, "evals"), { recursive: true });
|
|
111
|
+
const fileA = join(root, "evals", "a.eval.ts");
|
|
112
|
+
const fileB = join(root, "evals", "b.eval.ts");
|
|
113
|
+
await writeFile(fileA, "content a\n", "utf-8");
|
|
114
|
+
await writeFile(fileB, "content b\n", "utf-8");
|
|
115
|
+
|
|
116
|
+
const a = await captureEvalSource(fileA, { root });
|
|
117
|
+
const b = await captureEvalSource(fileB, { root });
|
|
118
|
+
|
|
119
|
+
expect(a.sha256).not.toBe(b.sha256);
|
|
120
|
+
});
|
|
121
|
+
});
|