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
package/src/show/show.test.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
// niceeval show 终端宿主的测试(行为规范:docs-site/zh/guides/viewing-results.mdx;
|
|
2
|
-
// 组合语义:docs/reports.md
|
|
3
|
-
// - 榜单合成口径:每 experiment × eval
|
|
4
|
-
// -
|
|
2
|
+
// 组合语义:docs/feature/reports/architecture.md「Selection 是计算入口」)。覆盖:
|
|
3
|
+
// - 榜单合成口径:每 experiment × eval 取最新判定,局部重跑从更早快照补齐,头部标注合成自几个快照;
|
|
4
|
+
// - 前缀过滤收窄 Selection,覆盖警告分母 = 已知并集 ∩ 范围;
|
|
5
5
|
// - --history 时间轴只列真实执行,resume 携带的复印件不占行;
|
|
6
|
-
// - --report 装载(合法 / 非法默认导出 / 文件缺失)
|
|
6
|
+
// - --report 装载(合法 / 非法默认导出 / 文件缺失)、位置前缀收窄注入 Selection、attemptCommand 下钻;
|
|
7
7
|
// - 互斥:--history 与 --report;
|
|
8
|
-
// - 单 eval
|
|
8
|
+
// - 单 eval 详情、`@<locator>` 精确定位与 --eval / --execution / --diff 证据切面的输出形态。
|
|
9
|
+
//
|
|
10
|
+
// fixture 直接写新布局(<expDir>/<snapDir>/snapshot.json + <evalId>/a<n>/result.json),
|
|
11
|
+
// 依据是 docs/feature/results/architecture.md 的稳定磁盘契约,不经 writer 运行时 API(避免与并行重写的
|
|
12
|
+
// niceeval/results 写入面签名耦合)。
|
|
9
13
|
|
|
10
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
14
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
11
15
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
12
16
|
import { tmpdir } from "node:os";
|
|
13
17
|
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type EvalResult,
|
|
20
|
-
type RunSummary,
|
|
21
|
-
} from "../results/index.ts";
|
|
22
|
-
import { composeShowSelection, evalHistory } from "./compose.ts";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { openResults } from "../results/index.ts";
|
|
20
|
+
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Verdict } from "../types.ts";
|
|
21
|
+
import { selectCurrentResults } from "../results/select.ts";
|
|
22
|
+
import { evalHistory } from "./compose.ts";
|
|
23
23
|
import { runShow, type ShowFlags } from "./index.ts";
|
|
24
|
+
import { stringWidth } from "../report/text/layout.ts";
|
|
24
25
|
|
|
25
26
|
// ───────────────────────── fixture 工具 ─────────────────────────
|
|
26
27
|
|
|
@@ -34,40 +35,72 @@ afterEach(async () => {
|
|
|
34
35
|
await Promise.all(roots.splice(0).map((r) => rm(r, { recursive: true, force: true })));
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
// show 的报告 chrome 跟随 CLI 界面语言(detectLocale);本文件的断言按英文写,
|
|
39
|
+
// 固定 en 让用例不随宿主机 LANG 漂移;zh-CN 传递单独一条用例覆盖。
|
|
40
|
+
let langBackup: string | undefined;
|
|
41
|
+
beforeAll(() => {
|
|
42
|
+
langBackup = process.env.NICEEVAL_LANG;
|
|
43
|
+
process.env.NICEEVAL_LANG = "en";
|
|
44
|
+
});
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
if (langBackup === undefined) delete process.env.NICEEVAL_LANG;
|
|
47
|
+
else process.env.NICEEVAL_LANG = langBackup;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** 一条 attempt 的最小 fixture;字段照 docs/feature/results/architecture.md 的 AttemptRecord。 */
|
|
51
|
+
type AttemptFixture = Pick<EvalResult, "id" | "verdict"> &
|
|
52
|
+
Partial<
|
|
53
|
+
Pick<
|
|
54
|
+
EvalResult,
|
|
55
|
+
"attempt" | "durationMs" | "assertions" | "estimatedCostUSD" | "usage" | "error" | "startedAt" | "artifactBase" | "hasEvents" | "hasTrace"
|
|
56
|
+
>
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
function res(id: string, verdict: Verdict, extra: Partial<AttemptFixture> = {}): AttemptFixture {
|
|
60
|
+
return { id, verdict, attempt: 0, durationMs: 1000, assertions: [], ...extra };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 实验目录名的清洗:与 docs/feature/results/architecture.md 一致(/ 与非 [\w.@-] 换成 _)。 */
|
|
64
|
+
function cleanDirName(id: string): string {
|
|
65
|
+
return id.replace(/[^\w.@-]/g, "_");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface SnapshotOpts {
|
|
69
|
+
experimentId: string;
|
|
70
|
+
agent?: string;
|
|
71
|
+
model?: string;
|
|
72
|
+
startedAt: string;
|
|
73
|
+
completedAt?: string;
|
|
74
|
+
knownEvalIds?: string[];
|
|
46
75
|
}
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
77
|
+
/** 写一份新布局快照:snapshot.json + 各 attempt 的 result.json。返回快照目录绝对路径。 */
|
|
78
|
+
async function writeSnapshot(
|
|
79
|
+
root: string,
|
|
80
|
+
snapDirName: string,
|
|
81
|
+
opts: SnapshotOpts,
|
|
82
|
+
results: AttemptFixture[],
|
|
83
|
+
): Promise<string> {
|
|
84
|
+
const dir = join(root, cleanDirName(opts.experimentId), snapDirName);
|
|
85
|
+
await mkdir(dir, { recursive: true });
|
|
86
|
+
const meta = {
|
|
51
87
|
format: RESULTS_FORMAT,
|
|
52
88
|
schemaVersion: RESULTS_SCHEMA_VERSION,
|
|
53
89
|
producer: { name: "niceeval", version: "0.4.6" },
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
errored: count("errored"),
|
|
61
|
-
durationMs: 60_000,
|
|
62
|
-
results,
|
|
63
|
-
...over,
|
|
90
|
+
experimentId: opts.experimentId,
|
|
91
|
+
agent: opts.agent ?? "bub",
|
|
92
|
+
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
93
|
+
startedAt: opts.startedAt,
|
|
94
|
+
completedAt: opts.completedAt ?? opts.startedAt,
|
|
95
|
+
...(opts.knownEvalIds ? { knownEvalIds: opts.knownEvalIds } : {}),
|
|
64
96
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
await writeFile(join(dir, "snapshot.json"), JSON.stringify(meta, null, 2), "utf-8");
|
|
98
|
+
for (const r of results) {
|
|
99
|
+
const attemptDir = join(dir, r.id, `a${r.attempt ?? 0}`);
|
|
100
|
+
await mkdir(attemptDir, { recursive: true });
|
|
101
|
+
await writeFile(join(attemptDir, "result.json"), JSON.stringify(r, null, 2), "utf-8");
|
|
102
|
+
}
|
|
103
|
+
return dir;
|
|
71
104
|
}
|
|
72
105
|
|
|
73
106
|
interface Captured {
|
|
@@ -76,89 +109,113 @@ interface Captured {
|
|
|
76
109
|
code: number;
|
|
77
110
|
}
|
|
78
111
|
|
|
79
|
-
async function show(root: string, patterns: string[], flags: ShowFlags = {}): Promise<Captured> {
|
|
112
|
+
async function show(root: string, patterns: string[], flags: ShowFlags = {}, width = 100): Promise<Captured> {
|
|
80
113
|
let out = "";
|
|
81
114
|
let err = "";
|
|
82
115
|
const code = await runShow(root, patterns, { run: root, ...flags }, {
|
|
83
116
|
out: (s) => (out += s),
|
|
84
117
|
err: (s) => (err += s),
|
|
85
|
-
width
|
|
118
|
+
width,
|
|
86
119
|
now: Date.parse("2026-07-09T10:01:00.000Z"),
|
|
87
120
|
});
|
|
88
121
|
return { out, err, code };
|
|
89
122
|
}
|
|
90
123
|
|
|
91
|
-
/**
|
|
124
|
+
/** 两个快照:老快照全量(a ✓ b ✓),新快照只重跑 b(✗)—— 榜单该跨快照合成。 */
|
|
92
125
|
async function seedComposedRoot(): Promise<string> {
|
|
93
126
|
const root = await makeRoot();
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
summaryOf(
|
|
109
|
-
[
|
|
110
|
-
res({
|
|
111
|
-
id: "fixtures/button",
|
|
112
|
-
experimentId: "compare/bub",
|
|
113
|
-
outcome: "failed",
|
|
114
|
-
startedAt: "2026-07-09T10:00:01.000Z",
|
|
115
|
-
assertions: [
|
|
116
|
-
{
|
|
117
|
-
name: 'fileChanged("src/components/Button.tsx")',
|
|
118
|
-
severity: "gate",
|
|
119
|
-
score: 0,
|
|
120
|
-
passed: false,
|
|
121
|
-
detail: "file was not modified",
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
}),
|
|
127
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
128
|
+
res("weather/brooklyn", "passed"),
|
|
129
|
+
res("fixtures/button", "passed"),
|
|
130
|
+
]);
|
|
131
|
+
await writeSnapshot(root, "2026-07-09T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-09T10:00:00.000Z" }, [
|
|
132
|
+
res("fixtures/button", "failed", {
|
|
133
|
+
assertions: [
|
|
134
|
+
{
|
|
135
|
+
name: 'fileChanged("src/components/Button.tsx")',
|
|
136
|
+
severity: "gate",
|
|
137
|
+
score: 0,
|
|
138
|
+
passed: false,
|
|
139
|
+
detail: "file was not modified",
|
|
140
|
+
},
|
|
125
141
|
],
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
);
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
129
144
|
return root;
|
|
130
145
|
}
|
|
131
146
|
|
|
132
147
|
// ───────────────────────── 榜单合成口径 ─────────────────────────
|
|
133
148
|
|
|
134
|
-
describe("
|
|
135
|
-
it("
|
|
149
|
+
describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text 面)", () => {
|
|
150
|
+
it("局部重跑不撕报告:另一题从更早快照补齐,缺成本时散点明确空态", async () => {
|
|
136
151
|
const root = await seedComposedRoot();
|
|
137
152
|
const { out, code } = await show(root, []);
|
|
138
153
|
expect(code).toBe(0);
|
|
139
|
-
expect(out).toContain("
|
|
140
|
-
expect(out).toContain("
|
|
141
|
-
|
|
142
|
-
expect(out).toContain("
|
|
143
|
-
expect(out).
|
|
144
|
-
expect(out).
|
|
145
|
-
|
|
146
|
-
expect(out).
|
|
147
|
-
expect(out).
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
expect(out).toContain("No data to plot Cost × Pass rate");
|
|
155
|
+
expect(out).toContain("1 point missing data");
|
|
156
|
+
expect(out).not.toContain("needed to compare");
|
|
157
|
+
expect(out).not.toContain("COMPARISON");
|
|
158
|
+
expect(out).toMatch(/compare\/bub\s+default\s+bub\s+1s\s+50%/);
|
|
159
|
+
expect(out).toContain("1 passed / 1 failed");
|
|
160
|
+
expect(out).toMatch(/✗ failed\s+fixtures\/button[\s\S]*└─ @1[0-9a-z]{7}[\s\S]*fileChanged/);
|
|
161
|
+
expect(out).toMatch(/✓ passed\s+weather\/brooklyn[\s\S]*└─ @1[0-9a-z]{7}/);
|
|
162
|
+
expect(out).not.toMatch(/\[[EXD⏱,]+\]/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("裸 show 的默认报告 chrome 跟随 locale", async () => {
|
|
166
|
+
const root = await seedComposedRoot();
|
|
167
|
+
process.env.NICEEVAL_LANG = "zh-CN";
|
|
168
|
+
try {
|
|
169
|
+
const { out, code } = await show(root, []);
|
|
170
|
+
expect(code).toBe(0);
|
|
171
|
+
expect(out).toContain("预估成本 × 成功率 没有可绘制的数据");
|
|
172
|
+
expect(out).toContain("1 通过 / 1 失败");
|
|
173
|
+
expect(out).toContain("compare/bub");
|
|
174
|
+
} finally {
|
|
175
|
+
process.env.NICEEVAL_LANG = "en";
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("窄终端截断长 eval 与原因,每一行都不超过终端显示宽度", async () => {
|
|
180
|
+
const root = await seedComposedRoot();
|
|
181
|
+
const { out, code } = await show(root, [], {}, 60);
|
|
182
|
+
expect(code).toBe(0);
|
|
183
|
+
expect(out).toContain("fixtures/button");
|
|
184
|
+
for (const line of out.trimEnd().split("\n")) {
|
|
185
|
+
expect(stringWidth(line), line).toBeLessThanOrEqual(60);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("多个 experiment 显示散点图,再逐 experiment → Eval → Attempt", async () => {
|
|
190
|
+
const root = await makeRoot();
|
|
191
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/a", agent: "codex", model: "mini", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
192
|
+
res("q1", "passed", { estimatedCostUSD: 0.1 }),
|
|
193
|
+
res("q2", "failed", { estimatedCostUSD: 0.2 }),
|
|
194
|
+
]);
|
|
195
|
+
await writeSnapshot(root, "2026-07-08T11-00-00-000Z", { experimentId: "compare/b", agent: "claude", model: "large", startedAt: "2026-07-08T11:00:00.000Z" }, [
|
|
196
|
+
res("q1", "passed", { estimatedCostUSD: 0.3 }),
|
|
197
|
+
res("q2", "passed", { estimatedCostUSD: 0.3 }),
|
|
198
|
+
]);
|
|
199
|
+
const { out, code } = await show(root, []);
|
|
200
|
+
expect(code).toBe(0);
|
|
201
|
+
expect(out).toContain("better → upper right");
|
|
202
|
+
expect(out).toContain("A compare/a B compare/b");
|
|
203
|
+
expect(out).toMatch(/compare\/b\s+large\s+claude\s+1s\s+100%/);
|
|
204
|
+
expect(out).toMatch(/compare\/a\s+mini\s+codex\s+1s\s+50%/);
|
|
205
|
+
expect(out).toMatch(/✓ passed\s+q1[\s\S]*└─ @1[0-9a-z]{7}/);
|
|
206
|
+
expect(out).toMatch(/✗ failed\s+q2[\s\S]*└─ @1[0-9a-z]{7}/);
|
|
207
|
+
expect(out).not.toContain("needed to compare");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("合成 Selection:每 experiment × eval 取最新判定;compose 不产生残缺警告", async () => {
|
|
154
211
|
const root = await seedComposedRoot();
|
|
155
212
|
const results = await openResults(root);
|
|
156
|
-
const selection =
|
|
213
|
+
const selection = selectCurrentResults(results);
|
|
157
214
|
expect(selection.snapshots).toHaveLength(1);
|
|
158
215
|
const evals = selection.snapshots[0].evals.map((e) => e.id).sort();
|
|
159
216
|
expect(evals).toEqual(["fixtures/button", "weather/brooklyn"]);
|
|
160
217
|
const button = selection.snapshots[0].evals.find((e) => e.id === "fixtures/button")!;
|
|
161
|
-
expect(button.attempts[0].result.
|
|
218
|
+
expect(button.attempts[0].result.verdict).toBe("failed"); // 新快照的判定赢
|
|
162
219
|
expect(selection.warnings).toEqual([]);
|
|
163
220
|
// 对照:results.latest() 的最新快照是残缺的(这正是宿主要合成的原因)
|
|
164
221
|
expect(results.latest().warnings.some((w) => w.kind === "partial-coverage")).toBe(true);
|
|
@@ -168,32 +225,28 @@ describe("榜单:跨 run 合成的现刻水位", () => {
|
|
|
168
225
|
// ───────────────────────── 前缀过滤 ─────────────────────────
|
|
169
226
|
|
|
170
227
|
describe("位置前缀收窄", () => {
|
|
171
|
-
it("
|
|
228
|
+
it("前缀收窄 Selection 覆盖的 eval;覆盖警告分母 = 已知并集 ∩ 范围", async () => {
|
|
172
229
|
const root = await makeRoot();
|
|
173
|
-
await
|
|
230
|
+
await writeSnapshot(
|
|
174
231
|
root,
|
|
175
232
|
"2026-07-08T10-00-00-000Z",
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
],
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// 已知并集包含一道从未落盘的题:algebra 范围外,不该刷 weather 范围的屏
|
|
184
|
-
snapshots: { "compare/bub": { knownEvalIds: ["weather/brooklyn", "weather/queens", "algebra/quadratic"] } },
|
|
185
|
-
},
|
|
186
|
-
),
|
|
233
|
+
{
|
|
234
|
+
experimentId: "compare/bub",
|
|
235
|
+
startedAt: "2026-07-08T10:00:00.000Z",
|
|
236
|
+
// 已知并集包含一道从未落盘的题:algebra 范围外,不该刷 weather 范围的屏
|
|
237
|
+
knownEvalIds: ["weather/brooklyn", "weather/queens", "algebra/quadratic"],
|
|
238
|
+
},
|
|
239
|
+
[res("weather/brooklyn", "passed"), res("algebra/quadratic", "passed")],
|
|
187
240
|
);
|
|
188
241
|
const results = await openResults(root);
|
|
189
242
|
|
|
190
|
-
const weather =
|
|
243
|
+
const weather = selectCurrentResults(results, { patterns: ["weather"] });
|
|
191
244
|
expect(weather.snapshots[0].evals.map((e) => e.id)).toEqual(["weather/brooklyn"]);
|
|
192
245
|
// 分母 = {weather/brooklyn, weather/queens}:缺 queens → 1/2;algebra 的缺口不进来
|
|
193
246
|
const coverage = weather.warnings.find((w) => w.kind === "partial-coverage");
|
|
194
247
|
expect(coverage).toMatchObject({ covered: 1, total: 2 });
|
|
195
248
|
|
|
196
|
-
const algebra =
|
|
249
|
+
const algebra = selectCurrentResults(results, { patterns: ["algebra"] });
|
|
197
250
|
expect(algebra.warnings.filter((w) => w.kind === "partial-coverage")).toEqual([]);
|
|
198
251
|
});
|
|
199
252
|
|
|
@@ -211,52 +264,36 @@ describe("位置前缀收窄", () => {
|
|
|
211
264
|
describe("单 eval 详情", () => {
|
|
212
265
|
it("attempt 行 + 断言明细 + artifacts 路径 + 下钻提示", async () => {
|
|
213
266
|
const root = await makeRoot();
|
|
214
|
-
await
|
|
267
|
+
await writeSnapshot(
|
|
215
268
|
root,
|
|
216
269
|
"2026-07-09T10-00-00-000Z",
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
passed: false,
|
|
245
|
-
detail: "tool was never called",
|
|
246
|
-
},
|
|
247
|
-
{ name: "succeeded()", severity: "gate", score: 1, passed: true },
|
|
248
|
-
{
|
|
249
|
-
name: 'judge("回答基于实时数据")',
|
|
250
|
-
severity: "soft",
|
|
251
|
-
score: 0.2,
|
|
252
|
-
passed: false,
|
|
253
|
-
detail: "reply invents a temperature without any tool call",
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
}),
|
|
257
|
-
],
|
|
258
|
-
{ agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" },
|
|
259
|
-
),
|
|
270
|
+
{ experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" },
|
|
271
|
+
[
|
|
272
|
+
res("weather/brooklyn", "failed", { attempt: 0, durationMs: 40_000 }),
|
|
273
|
+
res("weather/brooklyn", "failed", {
|
|
274
|
+
attempt: 1,
|
|
275
|
+
durationMs: 41_000,
|
|
276
|
+
usage: { inputTokens: 12_000, outputTokens: 300 },
|
|
277
|
+
estimatedCostUSD: 0.04,
|
|
278
|
+
assertions: [
|
|
279
|
+
{
|
|
280
|
+
name: 'calledTool("get_weather")',
|
|
281
|
+
severity: "gate",
|
|
282
|
+
score: 0,
|
|
283
|
+
passed: false,
|
|
284
|
+
detail: "tool was never called",
|
|
285
|
+
},
|
|
286
|
+
{ name: "succeeded()", severity: "gate", score: 1, passed: true },
|
|
287
|
+
{
|
|
288
|
+
name: 'judge("回答基于实时数据")',
|
|
289
|
+
severity: "soft",
|
|
290
|
+
score: 0.2,
|
|
291
|
+
passed: false,
|
|
292
|
+
detail: "reply invents a temperature without any tool call",
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
}),
|
|
296
|
+
],
|
|
260
297
|
);
|
|
261
298
|
const { out, code } = await show(root, ["weather/brooklyn"]);
|
|
262
299
|
expect(code).toBe(0);
|
|
@@ -269,63 +306,44 @@ describe("单 eval 详情", () => {
|
|
|
269
306
|
expect(out).toContain('✗ gate calledTool("get_weather") — tool was never called');
|
|
270
307
|
expect(out).toContain("✓ gate succeeded()");
|
|
271
308
|
expect(out).toContain('✗ soft judge("回答基于实时数据") — 0.2/1: reply invents a temperature');
|
|
272
|
-
|
|
273
|
-
expect(out).toContain("
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const { err, code } = await show(root, ["fixtures/button"], { attempt: 5 });
|
|
279
|
-
expect(code).toBe(1);
|
|
280
|
-
expect(err).toContain("Attempt 5 not found for fixtures/button");
|
|
309
|
+
// artifacts 路径 = <experiment-dir>/<snapshot-dir>/<evalId>/a<n>(root 相对)
|
|
310
|
+
expect(out).toContain("compare_codex/2026-07-09T10-00-00-000Z/weather/brooklyn/a1");
|
|
311
|
+
// 每 experiment 一行都带紧凑索引(locator + 失败原因),详情块再补一条精确的 attempt locator
|
|
312
|
+
expect(out).toMatch(/✗ failed\s+2 attempts.*@1[0-9a-z]{7}.*gate calledTool/);
|
|
313
|
+
expect(out).toMatch(/attempt locator: @1[0-9a-z]{7}/);
|
|
314
|
+
expect(out).toMatch(/next: niceeval show @1[0-9a-z]{7} \[--eval\|--execution\|--diff\]/);
|
|
281
315
|
});
|
|
282
316
|
});
|
|
283
317
|
|
|
284
318
|
// ───────────────────────── --history:复印件不占行 ─────────────────────────
|
|
285
319
|
|
|
286
320
|
describe("--history 时间轴", () => {
|
|
287
|
-
/**
|
|
321
|
+
/** 快照1 真实执行;快照2 resume 携带同一判定(身份键相同的复印件)+ 新题真实执行。 */
|
|
288
322
|
async function seedHistoryRoot(): Promise<string> {
|
|
289
323
|
const root = await makeRoot();
|
|
290
|
-
|
|
291
|
-
id: "weather/brooklyn",
|
|
292
|
-
experimentId: "compare/bub",
|
|
293
|
-
outcome: "passed",
|
|
294
|
-
startedAt: "2026-07-07T09:00:01.000Z",
|
|
295
|
-
estimatedCostUSD: 0.03,
|
|
296
|
-
artifactsDir: "artifacts/compare__bub__weather__brooklyn__0",
|
|
297
|
-
});
|
|
298
|
-
await writeRun(
|
|
324
|
+
await writeSnapshot(
|
|
299
325
|
root,
|
|
300
326
|
"2026-07-07T09-00-00-000Z",
|
|
301
|
-
|
|
327
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-07T09:00:00.000Z" },
|
|
328
|
+
[res("weather/brooklyn", "passed", { estimatedCostUSD: 0.03 })],
|
|
302
329
|
);
|
|
303
|
-
// 复印件:同 id / attempt / startedAt,artifactBase
|
|
304
|
-
|
|
305
|
-
const carried: EvalResult = {
|
|
306
|
-
...carriedBase,
|
|
307
|
-
artifactBase: "2026-07-07T09-00-00-000Z/artifacts/compare__bub__weather__brooklyn__0",
|
|
308
|
-
};
|
|
309
|
-
await writeRun(
|
|
330
|
+
// 复印件:同 id / attempt / startedAt(锚定原快照的 startedAt),artifactBase 指回原快照。
|
|
331
|
+
await writeSnapshot(
|
|
310
332
|
root,
|
|
311
333
|
"2026-07-09T10-00-00-000Z",
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}),
|
|
326
|
-
],
|
|
327
|
-
{ startedAt: "2026-07-09T10:00:00.000Z" },
|
|
328
|
-
),
|
|
334
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-09T10:00:00.000Z" },
|
|
335
|
+
[
|
|
336
|
+
res("weather/brooklyn", "passed", {
|
|
337
|
+
estimatedCostUSD: 0.03,
|
|
338
|
+
startedAt: "2026-07-07T09:00:00.000Z",
|
|
339
|
+
artifactBase: "compare_bub/2026-07-07T09-00-00-000Z/weather/brooklyn/a0",
|
|
340
|
+
}),
|
|
341
|
+
res("weather/brooklyn", "failed", {
|
|
342
|
+
attempt: 1,
|
|
343
|
+
estimatedCostUSD: 0.04,
|
|
344
|
+
assertions: [{ name: 'calledTool("get_weather")', severity: "gate", score: 0, passed: false }],
|
|
345
|
+
}),
|
|
346
|
+
],
|
|
329
347
|
);
|
|
330
348
|
return root;
|
|
331
349
|
}
|
|
@@ -335,18 +353,18 @@ describe("--history 时间轴", () => {
|
|
|
335
353
|
const results = await openResults(root);
|
|
336
354
|
const exp = results.experiments.find((e) => e.id === "compare/bub")!;
|
|
337
355
|
const rows = evalHistory(exp, "weather/brooklyn");
|
|
338
|
-
//
|
|
356
|
+
// 快照2 里复印件被识别,真实执行只有:快照1 的 passed + 快照2 的 failed(新 attempt)
|
|
339
357
|
expect(rows).toHaveLength(2);
|
|
340
|
-
expect(rows[0]).toMatchObject({
|
|
358
|
+
expect(rows[0]).toMatchObject({ verdict: "failed", attempts: 1, costUSD: 0.04 });
|
|
341
359
|
expect(rows[0].failedAssertion).toBe('gate calledTool("get_weather")');
|
|
342
|
-
expect(rows[1]).toMatchObject({
|
|
360
|
+
expect(rows[1]).toMatchObject({ verdict: "passed", attempts: 1, costUSD: 0.03 });
|
|
343
361
|
|
|
344
362
|
const { out, code } = await show(root, ["weather/brooklyn"], { history: true });
|
|
345
363
|
expect(code).toBe(0);
|
|
346
364
|
expect(out).toContain("compare/bub · 2 runs · passed 1/2");
|
|
347
365
|
expect(out).toContain("2026-07-09T10-00");
|
|
348
366
|
expect(out).toContain("2026-07-07T09-00");
|
|
349
|
-
// 复印件那份 passed
|
|
367
|
+
// 复印件那份 passed 判定只出现一行(快照1),不在快照2 再占一行
|
|
350
368
|
expect(out.match(/✓ passed/g)).toHaveLength(1);
|
|
351
369
|
});
|
|
352
370
|
|
|
@@ -355,7 +373,7 @@ describe("--history 时间轴", () => {
|
|
|
355
373
|
const { out, code } = await show(root, [], { history: true });
|
|
356
374
|
expect(code).toBe(0);
|
|
357
375
|
expect(out).toContain("compare/bub · 2 runs");
|
|
358
|
-
// 每个快照一行(
|
|
376
|
+
// 每个快照一行(快照2 折叠含携带的 passed 复印件,任一轮通过 → passed)
|
|
359
377
|
expect(out.match(/1\/1 passed/g)).toHaveLength(2);
|
|
360
378
|
});
|
|
361
379
|
});
|
|
@@ -373,7 +391,7 @@ describe("--report 装载", () => {
|
|
|
373
391
|
"const Custom = () => null;",
|
|
374
392
|
"Custom[FACES] = {",
|
|
375
393
|
" web: () => null,",
|
|
376
|
-
" text: (props, ctx) => `CUSTOM ${props.evals} · drill ${ctx.attemptCommand(props.
|
|
394
|
+
" text: (props, ctx) => `CUSTOM ${props.evals} · drill ${ctx.attemptCommand(props.locator)}`,",
|
|
377
395
|
"};",
|
|
378
396
|
"export default {",
|
|
379
397
|
' [Symbol.for("niceeval.report.definition")]: true,',
|
|
@@ -381,7 +399,7 @@ describe("--report 装载", () => {
|
|
|
381
399
|
" type: Custom,",
|
|
382
400
|
" props: {",
|
|
383
401
|
" evals: ctx.selection.snapshots.flatMap((s) => s.evals.map((e) => e.id)).sort().join(\",\"),",
|
|
384
|
-
"
|
|
402
|
+
" locator: ctx.selection.snapshots[0].evals[0].attempts[0].locator,",
|
|
385
403
|
" },",
|
|
386
404
|
" }),",
|
|
387
405
|
"};",
|
|
@@ -392,7 +410,7 @@ describe("--report 装载", () => {
|
|
|
392
410
|
return path;
|
|
393
411
|
}
|
|
394
412
|
|
|
395
|
-
it("装载 +
|
|
413
|
+
it("装载 + 注入 Selection + attemptCommand 下钻命令", async () => {
|
|
396
414
|
const root = await seedComposedRoot();
|
|
397
415
|
const report = await writeReportFile(root);
|
|
398
416
|
const { out, code } = await show(root, [], { report });
|
|
@@ -401,7 +419,7 @@ describe("--report 装载", () => {
|
|
|
401
419
|
expect(out).toContain("drill niceeval show");
|
|
402
420
|
});
|
|
403
421
|
|
|
404
|
-
it("位置前缀对 --report
|
|
422
|
+
it("位置前缀对 --report 生效:收窄注入 Selection 覆盖的 eval", async () => {
|
|
405
423
|
const root = await seedComposedRoot();
|
|
406
424
|
const report = await writeReportFile(root);
|
|
407
425
|
const { out, code } = await show(root, ["weather"], { report });
|
|
@@ -409,18 +427,16 @@ describe("--report 装载", () => {
|
|
|
409
427
|
expect(out).toContain("CUSTOM weather/brooklyn ·");
|
|
410
428
|
});
|
|
411
429
|
|
|
412
|
-
it("--experiment
|
|
430
|
+
it("--experiment 让 Selection 只留该实验", async () => {
|
|
413
431
|
const root = await seedComposedRoot();
|
|
414
|
-
await
|
|
432
|
+
await writeSnapshot(
|
|
415
433
|
root,
|
|
416
434
|
"2026-07-09T11-00-00-000Z",
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
{ agent: "codex", startedAt: "2026-07-09T11:00:00.000Z" },
|
|
420
|
-
),
|
|
435
|
+
{ experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T11:00:00.000Z" },
|
|
436
|
+
[res("weather/brooklyn", "passed")],
|
|
421
437
|
);
|
|
422
438
|
const results = await openResults(root);
|
|
423
|
-
const selection =
|
|
439
|
+
const selection = selectCurrentResults(results, { experiment: "compare/codex" });
|
|
424
440
|
expect(selection.snapshots.map((s) => s.experimentId)).toEqual(["compare/codex"]);
|
|
425
441
|
});
|
|
426
442
|
|
|
@@ -454,52 +470,254 @@ describe("--report 装载", () => {
|
|
|
454
470
|
const { out, code } = await show(root, ["fixtures/button"], { report, diff: true });
|
|
455
471
|
expect(code).toBe(0);
|
|
456
472
|
expect(out).not.toContain("CUSTOM");
|
|
457
|
-
expect(out).toContain("no diff recorded"); // fixture 没有 diff
|
|
473
|
+
expect(out).toContain("no diff recorded"); // fixture 没有 diff artifact:如实说缺
|
|
458
474
|
});
|
|
459
475
|
});
|
|
460
476
|
|
|
461
|
-
// ───────────────────────── 证据切面:--
|
|
477
|
+
// ───────────────────────── 证据切面:--execution ─────────────────────────
|
|
462
478
|
|
|
463
|
-
describe("--
|
|
464
|
-
it("
|
|
479
|
+
describe("--execution", () => {
|
|
480
|
+
it("标准事件流(消息);没有 OTel 时如实标 timing unavailable", async () => {
|
|
465
481
|
const root = await makeRoot();
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
agent: "codex",
|
|
470
|
-
|
|
471
|
-
});
|
|
472
|
-
await snap.writeAttempt(
|
|
473
|
-
{
|
|
474
|
-
id: "weather/brooklyn",
|
|
475
|
-
outcome: "failed",
|
|
476
|
-
attempt: 2,
|
|
477
|
-
durationMs: 41_000,
|
|
478
|
-
assertions: [],
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
events: [
|
|
482
|
-
{ type: "message", role: "user", text: "布鲁克林今天天气怎么样?" },
|
|
483
|
-
{ type: "message", role: "assistant", text: "布鲁克林今天大约 24°C,晴。" },
|
|
484
|
-
],
|
|
485
|
-
},
|
|
482
|
+
const dir = await writeSnapshot(
|
|
483
|
+
root,
|
|
484
|
+
"2026-07-09T10-00-00-000Z",
|
|
485
|
+
{ experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" },
|
|
486
|
+
[res("weather/brooklyn", "failed", { attempt: 2, durationMs: 41_000, hasEvents: true })],
|
|
486
487
|
);
|
|
487
|
-
await
|
|
488
|
+
await writeFile(
|
|
489
|
+
join(dir, "weather/brooklyn/a2/events.json"),
|
|
490
|
+
JSON.stringify([
|
|
491
|
+
{ type: "message", role: "user", text: "布鲁克林今天天气怎么样?" },
|
|
492
|
+
{ type: "message", role: "assistant", text: "布鲁克林今天大约 24°C,晴。" },
|
|
493
|
+
]),
|
|
494
|
+
"utf-8",
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { execution: true });
|
|
498
|
+
expect(code).toBe(0);
|
|
499
|
+
expect(out).toMatch(/^@1[0-9a-z]{7} · weather\/brooklyn · compare\/codex · failed/m);
|
|
500
|
+
expect(out).toMatch(/USER\s+布鲁克林今天天气怎么样?/);
|
|
501
|
+
expect(out).toMatch(/ASSISTANT\s+布鲁克林今天大约 24°C,晴。/);
|
|
502
|
+
expect(out).toContain("timing unavailable · OTel trace was not collected");
|
|
503
|
+
expect(out).toContain("full events: ");
|
|
504
|
+
expect(out).toContain("events.json");
|
|
505
|
+
});
|
|
488
506
|
|
|
489
|
-
|
|
507
|
+
it("execution === null 时如实说没有事件记录", async () => {
|
|
508
|
+
const root = await makeRoot();
|
|
509
|
+
await writeSnapshot(
|
|
510
|
+
root,
|
|
511
|
+
"2026-07-08T10-00-00-000Z",
|
|
512
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
|
|
513
|
+
[res("weather/brooklyn", "passed")],
|
|
514
|
+
);
|
|
515
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { execution: true });
|
|
490
516
|
expect(code).toBe(0);
|
|
491
|
-
expect(out).toContain("
|
|
492
|
-
expect(out).toContain("[user]");
|
|
493
|
-
expect(out).toContain("布鲁克林今天天气怎么样?");
|
|
494
|
-
expect(out).toContain("[assistant]");
|
|
495
|
-
expect(out).toContain("(2 events · no tool calls · full stream: ");
|
|
496
|
-
expect(out).toContain("events.json)");
|
|
517
|
+
expect(out).toContain("no events recorded for this attempt");
|
|
497
518
|
});
|
|
498
519
|
|
|
499
|
-
it("
|
|
520
|
+
it("不把未关联的 telemetry spans 混进 Agent 执行记录", async () => {
|
|
521
|
+
const root = await makeRoot();
|
|
522
|
+
const dir = await writeSnapshot(root, "2026-07-09T10-00-00-000Z", { experimentId: "compare/codex", startedAt: "2026-07-09T10:00:00.000Z" }, [
|
|
523
|
+
res("weather/brooklyn", "failed", { hasEvents: true, hasTrace: true }),
|
|
524
|
+
]);
|
|
525
|
+
await writeFile(join(dir, "weather/brooklyn/a0/events.json"), JSON.stringify([{ type: "message", role: "user", text: "hello" }]), "utf-8");
|
|
526
|
+
await writeFile(join(dir, "weather/brooklyn/a0/trace.json"), JSON.stringify([{ traceId: "t", spanId: "s", name: "sdk.internal", startMs: 1, endMs: 2 }]), "utf-8");
|
|
527
|
+
const { out } = await show(root, ["weather/brooklyn"], { execution: true });
|
|
528
|
+
expect(out).toContain("USER");
|
|
529
|
+
expect(out).not.toMatch(/telemetry\s+sdk\.internal/);
|
|
530
|
+
expect(out).toContain("unlinked telemetry spans omitted");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("多个 eval 匹配时证据切面报错:给紧凑索引(locator + 失败原因)而不是只报个数", async () => {
|
|
500
534
|
const root = await seedComposedRoot();
|
|
501
|
-
const { err, code } = await show(root, [], {
|
|
535
|
+
const { err, code } = await show(root, [], { execution: true });
|
|
536
|
+
expect(code).toBe(1);
|
|
537
|
+
expect(err).toContain("matched 2 evals");
|
|
538
|
+
expect(err).toMatch(/✗ fixtures\/button\s+@1[0-9a-z]{7}/);
|
|
539
|
+
expect(err).toContain('gate fileChanged("src/components/Button.tsx")');
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// ───────────────────────── 证据切面:--eval ─────────────────────────
|
|
544
|
+
|
|
545
|
+
describe("--eval", () => {
|
|
546
|
+
it("evalSource === null 时如实说源码未捕获", async () => {
|
|
547
|
+
const root = await makeRoot();
|
|
548
|
+
await writeSnapshot(
|
|
549
|
+
root,
|
|
550
|
+
"2026-07-08T10-00-00-000Z",
|
|
551
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
|
|
552
|
+
[res("weather/brooklyn", "failed", { assertions: [{ name: "succeeded()", severity: "gate", score: 0, passed: false }] })],
|
|
553
|
+
);
|
|
554
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { eval: true });
|
|
555
|
+
expect(code).toBe(0);
|
|
556
|
+
expect(out).toContain("eval source unavailable for this attempt");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("有捕获的源码时:源码行标回断言,unmapped 断言单独成段,断言计票摘要", async () => {
|
|
560
|
+
const root = await makeRoot();
|
|
561
|
+
const dir = await writeSnapshot(
|
|
562
|
+
root,
|
|
563
|
+
"2026-07-08T10-00-00-000Z",
|
|
564
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
|
|
565
|
+
[
|
|
566
|
+
res("weather/brooklyn", "failed", {
|
|
567
|
+
assertions: [
|
|
568
|
+
{
|
|
569
|
+
name: 'calledTool("get_weather")',
|
|
570
|
+
severity: "gate",
|
|
571
|
+
score: 0,
|
|
572
|
+
passed: false,
|
|
573
|
+
detail: "tool was never called",
|
|
574
|
+
loc: { file: "evals/weather/brooklyn.eval.ts", line: 2 },
|
|
575
|
+
},
|
|
576
|
+
{ name: "unlocated()", severity: "soft", score: 0.5, passed: false, detail: "no loc on this one" },
|
|
577
|
+
],
|
|
578
|
+
}),
|
|
579
|
+
],
|
|
580
|
+
);
|
|
581
|
+
const attemptDir = join(dir, "weather/brooklyn/a0");
|
|
582
|
+
const content = 'defineEval({\n turn.calledTool("get_weather");\n});\n';
|
|
583
|
+
const sha = createHash("sha256").update(content).digest("hex");
|
|
584
|
+
await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/weather/brooklyn.eval.ts", sha256: sha }]), "utf-8");
|
|
585
|
+
await mkdir(join(dir, "sources"), { recursive: true });
|
|
586
|
+
await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
|
|
587
|
+
|
|
588
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { eval: true });
|
|
589
|
+
expect(code).toBe(0);
|
|
590
|
+
expect(out).toContain(`eval source: evals/weather/brooklyn.eval.ts · sha256:${sha.slice(0, 8)}`);
|
|
591
|
+
// 源码行的缩进是语义的一部分,渲染必须原样保留(不按词重排折叠掉这两个空格)。
|
|
592
|
+
expect(out).toMatch(/2✗ {3}turn\.calledTool\("get_weather"\);/);
|
|
593
|
+
expect(out).toContain('gate · calledTool("get_weather") · tool was never called');
|
|
594
|
+
expect(out).toContain("unmapped assertions (1, no source location):");
|
|
595
|
+
expect(out).toContain("unlocated()");
|
|
596
|
+
expect(out).toContain("assertions: 1 gate failed · 1 soft below target");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("源码行标注保留 group 与值断言的 expected/received", async () => {
|
|
600
|
+
const root = await makeRoot();
|
|
601
|
+
const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
602
|
+
res("manager", "failed", { assertions: [{ name: "equals(4)", severity: "gate", score: 0, passed: false, evidence: "1", group: "Issue 15193", loc: { file: "evals/manager.eval.ts", line: 1 } }] }),
|
|
603
|
+
]);
|
|
604
|
+
const attemptDir = join(dir, "manager/a0");
|
|
605
|
+
const content = "t.check(actual, equals(expected));\n";
|
|
606
|
+
const sha = createHash("sha256").update(content).digest("hex");
|
|
607
|
+
await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/manager.eval.ts", sha256: sha }]), "utf-8");
|
|
608
|
+
await mkdir(join(dir, "sources"), { recursive: true });
|
|
609
|
+
await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
|
|
610
|
+
const { out } = await show(root, ["manager"], { eval: true });
|
|
611
|
+
expect(out).toContain("gate · Issue 15193 · equals(4) · expected 4 · received 1");
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// ───────────────────────── @<locator> ─────────────────────────
|
|
616
|
+
|
|
617
|
+
describe("show @<locator>", () => {
|
|
618
|
+
it("默认页直接解释失败断言的 group、期望值、实际值和源码位置", async () => {
|
|
619
|
+
const root = await makeRoot();
|
|
620
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
621
|
+
res("manager", "failed", { assertions: [{ name: "equals(4)", severity: "gate", score: 0, passed: false, evidence: "1", group: "Issue 15193", loc: { file: "evals/manager.eval.ts", line: 40, column: 11 } }] }),
|
|
622
|
+
]);
|
|
623
|
+
const results = await openResults(root);
|
|
624
|
+
const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
|
|
625
|
+
const { out } = await show(root, [locator]);
|
|
626
|
+
expect(out).toContain("gate · Issue 15193");
|
|
627
|
+
expect(out).toContain("assertion: equals(4)");
|
|
628
|
+
expect(out).toContain("expected: 4");
|
|
629
|
+
expect(out).toContain("received: 1");
|
|
630
|
+
expect(out).toContain("source: evals/manager.eval.ts:40:11");
|
|
631
|
+
});
|
|
632
|
+
it("解析到对应 attempt,渲染紧凑全景(不当成 eval id 前缀匹配)", async () => {
|
|
633
|
+
const root = await makeRoot();
|
|
634
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
635
|
+
res("weather/brooklyn", "passed"),
|
|
636
|
+
]);
|
|
637
|
+
const results = await openResults(root);
|
|
638
|
+
const attempt = results.experiments[0]!.latest.evals[0]!.attempts[0]!;
|
|
639
|
+
const locator = attempt.locator!;
|
|
640
|
+
expect(locator).toMatch(/^@1[0-9a-z]{7}$/);
|
|
641
|
+
|
|
642
|
+
const { out, code } = await show(root, [locator]);
|
|
643
|
+
expect(code).toBe(0);
|
|
644
|
+
expect(out).toContain(`${locator} · weather/brooklyn · compare/bub · passed`);
|
|
645
|
+
expect(out).toContain("attempt 1 · ");
|
|
646
|
+
expect(out).toContain("execution: unavailable (no events recorded for this attempt)");
|
|
647
|
+
expect(out).toContain("changes: diff unavailable");
|
|
648
|
+
expect(out).not.toContain("evidence:");
|
|
649
|
+
expect(out).not.toContain("available:");
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("available 只逐行列出实际存在的证据命令,不打印能力字母或合并式 next", async () => {
|
|
653
|
+
const root = await makeRoot();
|
|
654
|
+
const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
655
|
+
res("weather/brooklyn", "failed", { hasEvents: true }),
|
|
656
|
+
]);
|
|
657
|
+
const attemptDir = join(dir, "weather/brooklyn/a0");
|
|
658
|
+
const content = "t.check(false, equals(true));\n";
|
|
659
|
+
const sha = createHash("sha256").update(content).digest("hex");
|
|
660
|
+
await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/weather.eval.ts", sha256: sha }]), "utf-8");
|
|
661
|
+
await mkdir(join(dir, "sources"), { recursive: true });
|
|
662
|
+
await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
|
|
663
|
+
await writeFile(join(attemptDir, "events.json"), JSON.stringify([{ type: "message", role: "user", text: "hello" }]), "utf-8");
|
|
664
|
+
const results = await openResults(root);
|
|
665
|
+
const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
|
|
666
|
+
|
|
667
|
+
const { out, code } = await show(root, [locator]);
|
|
668
|
+
expect(code).toBe(0);
|
|
669
|
+
expect(out).toContain(`available:\n niceeval show ${locator} --eval\n niceeval show ${locator} --execution`);
|
|
670
|
+
expect(out).not.toContain(`niceeval show ${locator} --diff`);
|
|
671
|
+
expect(out).not.toContain("evidence:");
|
|
672
|
+
expect(out).not.toContain("next:");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("配 --execution 走证据切面,不是紧凑全景", async () => {
|
|
676
|
+
const root = await makeRoot();
|
|
677
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
678
|
+
res("weather/brooklyn", "passed"),
|
|
679
|
+
]);
|
|
680
|
+
const results = await openResults(root);
|
|
681
|
+
const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
|
|
682
|
+
|
|
683
|
+
const { out, code } = await show(root, [locator], { execution: true });
|
|
684
|
+
expect(code).toBe(0);
|
|
685
|
+
expect(out).toContain(`${locator} · weather/brooklyn · compare/bub · passed`);
|
|
686
|
+
expect(out).toContain("no events recorded for this attempt");
|
|
687
|
+
// 紧凑全景独有的行不应该出现在证据切面输出里
|
|
688
|
+
expect(out).not.toContain("available:");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("语法不对的 locator 报「not a valid attempt locator」,退出码 1,不崩", async () => {
|
|
692
|
+
const root = await makeRoot();
|
|
693
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
694
|
+
res("weather/brooklyn", "passed"),
|
|
695
|
+
]);
|
|
696
|
+
const { err, code } = await show(root, ["@not-valid"]);
|
|
697
|
+
expect(code).toBe(1);
|
|
698
|
+
expect(err).toContain("not a valid attempt locator");
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it("语法合法但索引里没有的 locator 报「No attempt found」,退出码 1,不崩", async () => {
|
|
702
|
+
const root = await makeRoot();
|
|
703
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
704
|
+
res("weather/brooklyn", "passed"),
|
|
705
|
+
]);
|
|
706
|
+
const { err, code } = await show(root, ["@1nosuch1"]);
|
|
707
|
+
expect(code).toBe(1);
|
|
708
|
+
expect(err).toContain("No attempt found");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("locator 与其它位置参数混用时报错,不静默只取第一个", async () => {
|
|
712
|
+
const root = await makeRoot();
|
|
713
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
714
|
+
res("weather/brooklyn", "passed"),
|
|
715
|
+
]);
|
|
716
|
+
const results = await openResults(root);
|
|
717
|
+
const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
|
|
718
|
+
|
|
719
|
+
const { err, code } = await show(root, [locator, "weather/brooklyn"]);
|
|
502
720
|
expect(code).toBe(1);
|
|
503
|
-
expect(err).toContain("
|
|
721
|
+
expect(err).toContain("must be the only positional argument");
|
|
504
722
|
});
|
|
505
723
|
});
|