niceeval 0.5.4 → 0.6.1
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 +321 -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/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -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/experiment-comparison.d.ts +1 -0
- package/dist/report/built-ins/experiment-comparison.js +13 -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 +180 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +600 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +26 -0
- package/dist/report/format.js +109 -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 +94 -0
- package/dist/report/locale.js +183 -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 +19 -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 +11 -0
- package/dist/report/react/ExperimentList.js +67 -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 +96 -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 +398 -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 +541 -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 +408 -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/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 +7 -7
- package/docs-site/zh/concepts/assert.mdx +19 -18
- package/docs-site/zh/concepts/evals.mdx +12 -11
- package/docs-site/zh/concepts/experiment.mdx +2 -2
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +3 -3
- 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 +237 -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 -1
- package/docs-site/zh/guides/fixtures.mdx +4 -2
- 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 +61 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
- package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
- package/docs-site/zh/guides/viewing-results.mdx +200 -79
- package/docs-site/zh/guides/write-experiment.mdx +9 -1
- package/docs-site/zh/guides/write-send.mdx +4 -3
- 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 +15 -13
- 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 +9 -3
- package/docs-site/zh/reference/expect.mdx +13 -8
- package/package.json +10 -6
- package/src/agents/bub.ts +129 -42
- 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 +9 -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 +59 -3
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +23 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +8 -8
- package/src/define.ts +11 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +33 -15
- package/src/i18n/zh-CN.ts +37 -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 +640 -0
- package/src/report/built-ins/experiment-comparison.tsx +19 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +225 -50
- package/src/report/compute.ts +335 -54
- package/src/report/dual-render.test.tsx +1129 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +35 -2
- package/src/report/index.ts +47 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +229 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +113 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +220 -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 +126 -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 +168 -0
- package/src/report/react/fixtures.ts +169 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +21 -6
- package/src/report/react/render.test.tsx +180 -31
- package/src/report/react/styles.css +432 -213
- package/src/report/report.test.ts +398 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +344 -96
- 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 +184 -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 +48 -13
- 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 +24 -24
- 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 +119 -95
- package/src/show/render.ts +462 -159
- package/src/show/show.test.ts +471 -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/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,17 +14,20 @@ 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,
|
|
27
29
|
AgentSetup,
|
|
30
|
+
AgentSetupManifest,
|
|
28
31
|
AgentTeardown,
|
|
29
32
|
Cleanup,
|
|
30
33
|
Config,
|
|
@@ -58,7 +61,7 @@ export function runAttemptEffect(
|
|
|
58
61
|
experiment: experimentRunInfo(run),
|
|
59
62
|
agent: run.agent.name,
|
|
60
63
|
model: run.model,
|
|
61
|
-
|
|
64
|
+
verdict: "errored",
|
|
62
65
|
fingerprint: a.fingerprint,
|
|
63
66
|
attempt,
|
|
64
67
|
startedAt: new Date(t0).toISOString(),
|
|
@@ -75,7 +78,7 @@ export function runAttemptEffect(
|
|
|
75
78
|
|
|
76
79
|
// 流式进度打到宿主 stderr(结果走 stdout,互不干扰)。容器主日志【不】放这些进度标记 ——
|
|
77
80
|
// 那里留给 agent 的原始输出(adapter 给 agent 命令开 { stream: true })。
|
|
78
|
-
const who = runWho(run);
|
|
81
|
+
const who = runWho({ agentName: run.agent.name, model: run.model, experimentId: run.experimentId });
|
|
79
82
|
// 同时保留最近 20 条进度消息,timeout 时嵌入 error 字段方便定位卡在哪一步。
|
|
80
83
|
const recentLogs: string[] = [];
|
|
81
84
|
const log = (m: string) => {
|
|
@@ -90,9 +93,15 @@ export function runAttemptEffect(
|
|
|
90
93
|
|
|
91
94
|
return Effect.scoped(
|
|
92
95
|
Effect.gen(function* () {
|
|
93
|
-
// run.sandbox ?? config.sandbox 是同一个 SandboxSpec
|
|
96
|
+
// run.sandbox ?? config.sandbox 是同一个 SandboxSpec 对象,既用来起沙箱 provider,
|
|
94
97
|
// 也是 sandbox.setup / sandbox.teardown 钩子(SandboxSpec.setup()/.teardown() 链式挂的)的来源。
|
|
95
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
|
+
};
|
|
96
105
|
const sandbox =
|
|
97
106
|
run.agent.kind === "sandbox"
|
|
98
107
|
? yield* sandboxSem.withPermits(1)(
|
|
@@ -102,6 +111,7 @@ export function runAttemptEffect(
|
|
|
102
111
|
log(t("runner.startSandbox"));
|
|
103
112
|
return yield* createSandbox({
|
|
104
113
|
sandbox: sandboxSpec,
|
|
114
|
+
provisionSlot,
|
|
105
115
|
timeout: timeoutMs,
|
|
106
116
|
runtime: "node24",
|
|
107
117
|
});
|
|
@@ -222,7 +232,7 @@ interface AttemptResources {
|
|
|
222
232
|
log: (m: string) => void;
|
|
223
233
|
}
|
|
224
234
|
|
|
225
|
-
// attempt 的固定段(上传→基线→setup→驱动 agent→采 diff
|
|
235
|
+
// attempt 的固定段(上传→基线→setup→驱动 agent→采 diff→脚本→评分→判定)。
|
|
226
236
|
// 资源已由 runAttemptEffect 的 Scope 持有;这里只在 finally 跑 agent 自己的 cleanup/teardown。
|
|
227
237
|
async function runAttemptBody(
|
|
228
238
|
a: Attempt,
|
|
@@ -248,6 +258,10 @@ async function runAttemptBody(
|
|
|
248
258
|
};
|
|
249
259
|
let agentCleanup: Cleanup | void = undefined;
|
|
250
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;
|
|
251
265
|
// SandboxSpec.setup() 返回的 cleanup 闭包,按调用顺序收集;finally 里 LIFO 跑(见下)。
|
|
252
266
|
const sandboxCleanups: Cleanup[] = [];
|
|
253
267
|
try {
|
|
@@ -268,7 +282,7 @@ async function runAttemptBody(
|
|
|
268
282
|
// setup 里需要 root 的(apt/pip)自己传 { root: true }。
|
|
269
283
|
if (evalDef.setup) {
|
|
270
284
|
log(t("runner.evalSetup"));
|
|
271
|
-
await evalDef.setup(withEvalLocalPaths(sandbox, evalDef.baseDir));
|
|
285
|
+
evalCleanup = await evalDef.setup(withEvalLocalPaths(sandbox, evalDef.baseDir));
|
|
272
286
|
}
|
|
273
287
|
}
|
|
274
288
|
|
|
@@ -279,6 +293,11 @@ async function runAttemptBody(
|
|
|
279
293
|
agentCleanup = await run.agent.setup(sandbox, attemptCtx);
|
|
280
294
|
}
|
|
281
295
|
|
|
296
|
+
// 安装 manifest:adapter 在 setup 收尾写进沙箱的固定路径,核心只把它抬成 attempt artifact
|
|
297
|
+
// (不解释内容、不按 agent 名字分支)。什么都没装的 adapter 不写这个文件 → undefined,
|
|
298
|
+
// 不生成空 artifact。读在 setup 之后:test 阶段抛错也留得住这份「这次装了什么」的证据。
|
|
299
|
+
if (usesSandbox && agentDidSetup) agentSetup = await readAgentSetupManifest(sandbox);
|
|
300
|
+
|
|
282
301
|
// OTLP 导出配置(file-based,如 codex 的 config.toml [otel] 块):与 setup 分开,
|
|
283
302
|
// 在主配置写完后追加。仅当 tracing 开 + 有 endpoint 时调一次(env-based 的不实现 configure)。
|
|
284
303
|
if (telemetry && run.agent.tracing?.configure) {
|
|
@@ -360,7 +379,7 @@ async function runAttemptBody(
|
|
|
360
379
|
};
|
|
361
380
|
if (!skipReason) log(t("runner.scoreJudge"));
|
|
362
381
|
const assertions = skipReason ? [] : await state.collector.finalize(scoringContext);
|
|
363
|
-
const
|
|
382
|
+
const verdict = computeVerdict({ error, assertions, skipReason, strict: run.strict });
|
|
364
383
|
|
|
365
384
|
// 收 OTLP trace:给最后一批导出留点落地时间,再 collect(空则不挂)。
|
|
366
385
|
// codex 的 OTLP 把内部 Rust tracing 全导出来(handle_responses / append_items … 上万条);
|
|
@@ -398,7 +417,7 @@ async function runAttemptBody(
|
|
|
398
417
|
if (cost !== undefined) o11y.estimatedCostUSD = cost;
|
|
399
418
|
|
|
400
419
|
// 收 test 引用到的 eval 源码(按 send / 断言的 loc 去重),供 view 渲染代码视图。
|
|
401
|
-
const sources = await collectSources(events, assertions);
|
|
420
|
+
const sources = await collectSources(events, assertions, evalDef.source);
|
|
402
421
|
|
|
403
422
|
return {
|
|
404
423
|
id: evalDef.id,
|
|
@@ -407,7 +426,7 @@ async function runAttemptBody(
|
|
|
407
426
|
experiment: experimentRunInfo(run),
|
|
408
427
|
agent: run.agent.name,
|
|
409
428
|
model: run.model,
|
|
410
|
-
|
|
429
|
+
verdict,
|
|
411
430
|
fingerprint: a.fingerprint,
|
|
412
431
|
attempt,
|
|
413
432
|
startedAt: new Date(t0).toISOString(),
|
|
@@ -421,6 +440,7 @@ async function runAttemptBody(
|
|
|
421
440
|
sources,
|
|
422
441
|
o11y,
|
|
423
442
|
trace,
|
|
443
|
+
agentSetup,
|
|
424
444
|
diff,
|
|
425
445
|
};
|
|
426
446
|
} catch (e) {
|
|
@@ -428,9 +448,10 @@ async function runAttemptBody(
|
|
|
428
448
|
...base,
|
|
429
449
|
durationMs: Date.now() - t0,
|
|
430
450
|
error: formatThrown(e),
|
|
451
|
+
...(agentSetup !== undefined ? { agentSetup } : {}),
|
|
431
452
|
};
|
|
432
453
|
} finally {
|
|
433
|
-
// teardown / cleanup 一律在 finally 跑(失败也跑)
|
|
454
|
+
// teardown / cleanup 一律在 finally 跑(失败也跑),不改判定,各自兜错(diagnostic)。
|
|
434
455
|
// LIFO:agent 级(setup 最晚 → cleanup / teardown 先跑)在前,sandbox 级(最早就绪)收尾在后
|
|
435
456
|
//(即 sandbox.setup 返回的 cleanup、sandbox.teardown 钩子最后跑,沙箱销毁前)。
|
|
436
457
|
// 沙箱 stop / 接收器 close 不在这里 —— 由 runAttemptEffect 的 Scope 在本函数返回后回收。
|
|
@@ -440,6 +461,12 @@ async function runAttemptBody(
|
|
|
440
461
|
} catch {
|
|
441
462
|
// teardown 失败只是 diagnostic,不影响已出的结果
|
|
442
463
|
}
|
|
464
|
+
try {
|
|
465
|
+
// eval.setup 返回的 cleanup:排在 agent 级之后、sandbox 级之前(LIFO,与 setup 顺序对称)。
|
|
466
|
+
if (typeof evalCleanup === "function") await evalCleanup();
|
|
467
|
+
} catch {
|
|
468
|
+
// 同上,只作 diagnostic
|
|
469
|
+
}
|
|
443
470
|
if (usesSandbox) {
|
|
444
471
|
// sandbox.setup 返回的 cleanup:LIFO(后 setup 先 cleanup),与 agent 级同构。
|
|
445
472
|
for (let i = sandboxCleanups.length - 1; i >= 0; i--) {
|
|
@@ -465,18 +492,26 @@ async function runAttemptBody(
|
|
|
465
492
|
}
|
|
466
493
|
|
|
467
494
|
/**
|
|
468
|
-
* 收集 test 引用到的 eval 源码:从 send(user message)与断言的 loc
|
|
469
|
-
*
|
|
495
|
+
* 收集 test 引用到的 eval 源码:从 send(user message)与断言的 loc 去重出文件集。
|
|
496
|
+
* 命中 eval 自己的定义文件(绝大多数情况——send / 断言几乎总在 eval 主体里直接调用)时,
|
|
497
|
+
* 直接用 discovery 时已经读好、归一化、算过哈希的 `evalSource`,不重新读盘;loc 指向
|
|
498
|
+
* 其它文件(共享 helper 里包装 t.send / 断言的少见情形)才现读现取,读不到就跳过
|
|
499
|
+
* (路径在沙箱内 / 已删 / 权限),view 用 loc 也能降级显示行号。
|
|
470
500
|
*/
|
|
471
501
|
async function collectSources(
|
|
472
502
|
events: readonly StreamEvent[],
|
|
473
503
|
assertions: readonly EvalResult["assertions"][number][],
|
|
504
|
+
evalSource: CapturedEvalSource,
|
|
474
505
|
): Promise<SourceArtifact[]> {
|
|
475
506
|
const paths = new Set<string>();
|
|
476
507
|
for (const e of events) if (e.type === "message" && e.loc) paths.add(e.loc.file);
|
|
477
508
|
for (const a of assertions) if (a.loc) paths.add(a.loc.file);
|
|
478
509
|
const out: SourceArtifact[] = [];
|
|
479
510
|
for (const path of paths) {
|
|
511
|
+
if (path === evalSource.path) {
|
|
512
|
+
out.push({ path, content: evalSource.content });
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
480
515
|
try {
|
|
481
516
|
out.push({ path, content: await readSourceFile(resolvePath(process.cwd(), path), "utf-8") });
|
|
482
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
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Eval 源码捕获:discovery 时读一次 eval 定义文件、归一化、算 SHA-256(定稿见
|
|
2
|
+
// docs/concepts.md「标注 Eval 源码」、docs/feature/results/architecture.md「sources.json」)。
|
|
3
|
+
//
|
|
4
|
+
// 目标形态是"发现时捕获,同一快照内相同内容只存一份"——本函数只做捕获这一步(读 + 归一化 +
|
|
5
|
+
// 哈希),不碰去重存储、不写 result.json、不改 discoverEvals()。它的产物形状是为了让后续
|
|
6
|
+
// 集成把 `captureEvalSource(evalDef.sourcePath)` 直接插进 discoverEvals() 的每个文件循环里:
|
|
7
|
+
// path/content 交给 writer 落盘(按 sha256 去重),sha256 也是 buildAnnotatedEvalSource()
|
|
8
|
+
// 重建标注模型时用来核对"这就是同一份源码"的锚。
|
|
9
|
+
//
|
|
10
|
+
// 归一化 + 哈希算法住在 results/source-hash.ts,不在这里重新实现一遍——discovery 侧捕获的
|
|
11
|
+
// 哈希与证据重建侧(annotated-source.ts)重算的哈希必须逐字节一致,唯一的办法是共用同一个
|
|
12
|
+
// 函数。runner 已经单向依赖 results(reporters/artifacts.ts 用 createResultsWriter),这里
|
|
13
|
+
// 反向引用不新增循环。
|
|
14
|
+
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { relative, sep } from "node:path";
|
|
17
|
+
import { hashEvalSource, normalizeEvalSource } from "../results/source-hash.ts";
|
|
18
|
+
|
|
19
|
+
export interface CapturedEvalSource {
|
|
20
|
+
/** 项目相对路径(正斜杠),与 SourceArtifact.path / SourceLoc.file 同一约定。 */
|
|
21
|
+
path: string;
|
|
22
|
+
/** 归一化后的源码文本(去 BOM、CRLF/CR 统一成 LF)。 */
|
|
23
|
+
content: string;
|
|
24
|
+
/** 归一化文本的 SHA-256 十六进制摘要;同一份内容(哪怕跨平台不同换行符)恒相同。 */
|
|
25
|
+
sha256: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 读一个 eval 定义文件、归一化文本、算哈希。`filePath` 是绝对路径(discoverEvals() 里
|
|
30
|
+
* `DiscoveredEval.sourcePath` 的形状);`opts.root` 决定 `path` 字段相对谁计算,省略时
|
|
31
|
+
* 用 `process.cwd()`(与 src/source-loc.ts 的 captureLoc()、runner/attempt.ts 的
|
|
32
|
+
* collectSources() 同一约定——项目相对路径永远相对进程 cwd,不是相对 discoverEvals() 的
|
|
33
|
+
* `root` 参数,两者通常相同但不保证,显式传参让调用方按需要覆盖,也让单测不依赖真实 cwd)。
|
|
34
|
+
*
|
|
35
|
+
* 读不到文件(已删除 / 权限问题)如实抛错——发现阶段的源码文件应当总是可读,
|
|
36
|
+
* 与 collectSources() 运行后"读不到就跳过"的降级语义不同:那时源码可能已经在沙箱里、
|
|
37
|
+
* 已被清理;这里读的是刚刚 import 成功的同一个文件,读不到是需要暴露的异常。
|
|
38
|
+
*/
|
|
39
|
+
export async function captureEvalSource(filePath: string, opts?: { root?: string }): Promise<CapturedEvalSource> {
|
|
40
|
+
const root = opts?.root ?? process.cwd();
|
|
41
|
+
const raw = await readFile(filePath, "utf-8");
|
|
42
|
+
const content = normalizeEvalSource(raw);
|
|
43
|
+
const path = relative(root, filePath).split(sep).join("/");
|
|
44
|
+
return { path, content, sha256: hashEvalSource(content) };
|
|
45
|
+
}
|