niceeval 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +55 -0
- package/dist/agents/types.d.ts +317 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/execution-tree.d.ts +103 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/select.d.ts +22 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +178 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +618 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +32 -0
- package/dist/report/format.js +126 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +83 -0
- package/dist/report/locale.js +160 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +20 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +10 -0
- package/dist/report/react/ExperimentList.js +15 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +100 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +393 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +503 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +420 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/annotated-source.d.ts +61 -0
- package/dist/results/attempt-evidence.d.ts +69 -0
- package/dist/results/attempt-source.d.ts +15 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +9 -4
- package/docs-site/zh/concepts/assert.mdx +8 -8
- package/docs-site/zh/concepts/evals.mdx +5 -5
- package/docs-site/zh/concepts/experiment.mdx +3 -1
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +2 -2
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -0
- package/docs-site/zh/guides/fixtures.mdx +1 -1
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +4 -4
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +35 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
- package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
- package/docs-site/zh/guides/viewing-results.mdx +199 -79
- package/docs-site/zh/guides/write-experiment.mdx +23 -1
- package/docs-site/zh/guides/write-send.mdx +2 -2
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +24 -12
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +7 -1
- package/docs-site/zh/reference/expect.mdx +3 -3
- package/package.json +10 -6
- package/src/agents/bub.ts +134 -43
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +11 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +62 -2
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/context.ts +3 -0
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +26 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +82 -0
- package/src/define.ts +72 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +36 -15
- package/src/i18n/zh-CN.ts +39 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +643 -0
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +223 -50
- package/src/report/compute.ts +358 -53
- package/src/report/dual-render.test.tsx +1125 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +50 -2
- package/src/report/index.ts +48 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +206 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +115 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +108 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +136 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +129 -0
- package/src/report/react/fixtures.ts +163 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +22 -6
- package/src/report/react/render.test.tsx +164 -31
- package/src/report/react/styles.css +405 -213
- package/src/report/report.test.ts +403 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +303 -95
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +196 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +97 -15
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +69 -28
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +125 -94
- package/src/show/render.ts +660 -160
- package/src/show/show.test.ts +472 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
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,114 @@ 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("榜单:跨快照合成的现刻水位(show 专用 attempt 表)", () => {
|
|
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).toContain("
|
|
144
|
-
expect(out).
|
|
145
|
-
|
|
146
|
-
expect(out).
|
|
147
|
-
expect(out).
|
|
148
|
-
expect(out).toContain(
|
|
149
|
-
expect(out).toContain("
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
expect(out).not.toContain("No data to plot");
|
|
155
|
+
expect(out).not.toContain("At least 2 experiments");
|
|
156
|
+
expect(out).not.toContain("COMPARISON");
|
|
157
|
+
expect(out).toContain("EXPERIMENT compare/bub · bub");
|
|
158
|
+
expect(out).toContain("SUMMARY 1 passed · 1 failed · 0 errored · 0 skipped · 2 attempts");
|
|
159
|
+
expect(out).toMatch(/STATUS\s+EVAL\s+ATTEMPT\s+RESULT\s+DURATION\s+COST/);
|
|
160
|
+
expect(out).toMatch(/✗ failed\s+fixtures\/button\s+@1[0-9a-z]{7}\s+file was not modified/);
|
|
161
|
+
expect(out).toMatch(/✓ passed\s+weather\/brooklyn\s+@1[0-9a-z]{7}\s+—/);
|
|
162
|
+
expect(out).not.toMatch(/@1[0-9a-z]{7}[✓✗]/);
|
|
163
|
+
expect(out).not.toContain("[E,X,");
|
|
164
|
+
expect(out).toContain("DRILL DOWN niceeval show @<attempt>");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("裸 show 使用稳定表头,不受自定义报告 locale chrome 影响", async () => {
|
|
168
|
+
const root = await seedComposedRoot();
|
|
169
|
+
process.env.NICEEVAL_LANG = "zh-CN";
|
|
170
|
+
try {
|
|
171
|
+
const { out, code } = await show(root, []);
|
|
172
|
+
expect(code).toBe(0);
|
|
173
|
+
expect(out).toMatch(/STATUS\s+EVAL\s+ATTEMPT\s+RESULT\s+DURATION\s+COST/);
|
|
174
|
+
expect(out).toContain("1 passed · 1 failed");
|
|
175
|
+
expect(out).not.toContain("没有可绘制的数据");
|
|
176
|
+
expect(out).toContain("compare/bub");
|
|
177
|
+
} finally {
|
|
178
|
+
process.env.NICEEVAL_LANG = "en";
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("窄终端截断长 eval 与原因,每一行都不超过终端显示宽度", async () => {
|
|
183
|
+
const root = await seedComposedRoot();
|
|
184
|
+
const { out, code } = await show(root, [], {}, 60);
|
|
185
|
+
expect(code).toBe(0);
|
|
186
|
+
expect(out).toContain("…");
|
|
187
|
+
for (const line of out.trimEnd().split("\n")) {
|
|
188
|
+
expect(stringWidth(line), line).toBeLessThanOrEqual(60);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("多个 experiment 先显示紧凑比较表,再逐 experiment 显示 attempt 表", async () => {
|
|
193
|
+
const root = await makeRoot();
|
|
194
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/a", agent: "codex", model: "mini", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
195
|
+
res("q1", "passed", { estimatedCostUSD: 0.1 }),
|
|
196
|
+
res("q2", "failed", { estimatedCostUSD: 0.2 }),
|
|
197
|
+
]);
|
|
198
|
+
await writeSnapshot(root, "2026-07-08T11-00-00-000Z", { experimentId: "compare/b", agent: "claude", model: "large", startedAt: "2026-07-08T11:00:00.000Z" }, [
|
|
199
|
+
res("q1", "passed", { estimatedCostUSD: 0.3 }),
|
|
200
|
+
res("q2", "passed", { estimatedCostUSD: 0.3 }),
|
|
201
|
+
]);
|
|
202
|
+
const { out, code } = await show(root, []);
|
|
203
|
+
expect(code).toBe(0);
|
|
204
|
+
expect(out).toMatch(/COMPARISON\nEXPERIMENT\s+AGENT\s+MODEL\s+PASS\s+FAILED\s+ERROR\s+SKIP\s+DURATION\s+COST/);
|
|
205
|
+
expect(out).toContain("50% 1/2");
|
|
206
|
+
expect(out).toContain("100% 2/2");
|
|
207
|
+
expect(out.match(/^EXPERIMENT compare\//gm)).toHaveLength(2);
|
|
208
|
+
expect(out).not.toContain("At least 2 experiments");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("合成 Selection:每 experiment × eval 取最新判定;compose 不产生残缺警告", async () => {
|
|
154
212
|
const root = await seedComposedRoot();
|
|
155
213
|
const results = await openResults(root);
|
|
156
|
-
const selection =
|
|
214
|
+
const selection = selectCurrentResults(results);
|
|
157
215
|
expect(selection.snapshots).toHaveLength(1);
|
|
158
216
|
const evals = selection.snapshots[0].evals.map((e) => e.id).sort();
|
|
159
217
|
expect(evals).toEqual(["fixtures/button", "weather/brooklyn"]);
|
|
160
218
|
const button = selection.snapshots[0].evals.find((e) => e.id === "fixtures/button")!;
|
|
161
|
-
expect(button.attempts[0].result.
|
|
219
|
+
expect(button.attempts[0].result.verdict).toBe("failed"); // 新快照的判定赢
|
|
162
220
|
expect(selection.warnings).toEqual([]);
|
|
163
221
|
// 对照:results.latest() 的最新快照是残缺的(这正是宿主要合成的原因)
|
|
164
222
|
expect(results.latest().warnings.some((w) => w.kind === "partial-coverage")).toBe(true);
|
|
@@ -168,32 +226,28 @@ describe("榜单:跨 run 合成的现刻水位", () => {
|
|
|
168
226
|
// ───────────────────────── 前缀过滤 ─────────────────────────
|
|
169
227
|
|
|
170
228
|
describe("位置前缀收窄", () => {
|
|
171
|
-
it("
|
|
229
|
+
it("前缀收窄 Selection 覆盖的 eval;覆盖警告分母 = 已知并集 ∩ 范围", async () => {
|
|
172
230
|
const root = await makeRoot();
|
|
173
|
-
await
|
|
231
|
+
await writeSnapshot(
|
|
174
232
|
root,
|
|
175
233
|
"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
|
-
),
|
|
234
|
+
{
|
|
235
|
+
experimentId: "compare/bub",
|
|
236
|
+
startedAt: "2026-07-08T10:00:00.000Z",
|
|
237
|
+
// 已知并集包含一道从未落盘的题:algebra 范围外,不该刷 weather 范围的屏
|
|
238
|
+
knownEvalIds: ["weather/brooklyn", "weather/queens", "algebra/quadratic"],
|
|
239
|
+
},
|
|
240
|
+
[res("weather/brooklyn", "passed"), res("algebra/quadratic", "passed")],
|
|
187
241
|
);
|
|
188
242
|
const results = await openResults(root);
|
|
189
243
|
|
|
190
|
-
const weather =
|
|
244
|
+
const weather = selectCurrentResults(results, { patterns: ["weather"] });
|
|
191
245
|
expect(weather.snapshots[0].evals.map((e) => e.id)).toEqual(["weather/brooklyn"]);
|
|
192
246
|
// 分母 = {weather/brooklyn, weather/queens}:缺 queens → 1/2;algebra 的缺口不进来
|
|
193
247
|
const coverage = weather.warnings.find((w) => w.kind === "partial-coverage");
|
|
194
248
|
expect(coverage).toMatchObject({ covered: 1, total: 2 });
|
|
195
249
|
|
|
196
|
-
const algebra =
|
|
250
|
+
const algebra = selectCurrentResults(results, { patterns: ["algebra"] });
|
|
197
251
|
expect(algebra.warnings.filter((w) => w.kind === "partial-coverage")).toEqual([]);
|
|
198
252
|
});
|
|
199
253
|
|
|
@@ -211,52 +265,36 @@ describe("位置前缀收窄", () => {
|
|
|
211
265
|
describe("单 eval 详情", () => {
|
|
212
266
|
it("attempt 行 + 断言明细 + artifacts 路径 + 下钻提示", async () => {
|
|
213
267
|
const root = await makeRoot();
|
|
214
|
-
await
|
|
268
|
+
await writeSnapshot(
|
|
215
269
|
root,
|
|
216
270
|
"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
|
-
),
|
|
271
|
+
{ experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" },
|
|
272
|
+
[
|
|
273
|
+
res("weather/brooklyn", "failed", { attempt: 0, durationMs: 40_000 }),
|
|
274
|
+
res("weather/brooklyn", "failed", {
|
|
275
|
+
attempt: 1,
|
|
276
|
+
durationMs: 41_000,
|
|
277
|
+
usage: { inputTokens: 12_000, outputTokens: 300 },
|
|
278
|
+
estimatedCostUSD: 0.04,
|
|
279
|
+
assertions: [
|
|
280
|
+
{
|
|
281
|
+
name: 'calledTool("get_weather")',
|
|
282
|
+
severity: "gate",
|
|
283
|
+
score: 0,
|
|
284
|
+
passed: false,
|
|
285
|
+
detail: "tool was never called",
|
|
286
|
+
},
|
|
287
|
+
{ name: "succeeded()", severity: "gate", score: 1, passed: true },
|
|
288
|
+
{
|
|
289
|
+
name: 'judge("回答基于实时数据")',
|
|
290
|
+
severity: "soft",
|
|
291
|
+
score: 0.2,
|
|
292
|
+
passed: false,
|
|
293
|
+
detail: "reply invents a temperature without any tool call",
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
}),
|
|
297
|
+
],
|
|
260
298
|
);
|
|
261
299
|
const { out, code } = await show(root, ["weather/brooklyn"]);
|
|
262
300
|
expect(code).toBe(0);
|
|
@@ -269,63 +307,44 @@ describe("单 eval 详情", () => {
|
|
|
269
307
|
expect(out).toContain('✗ gate calledTool("get_weather") — tool was never called');
|
|
270
308
|
expect(out).toContain("✓ gate succeeded()");
|
|
271
309
|
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");
|
|
310
|
+
// artifacts 路径 = <experiment-dir>/<snapshot-dir>/<evalId>/a<n>(root 相对)
|
|
311
|
+
expect(out).toContain("compare_codex/2026-07-09T10-00-00-000Z/weather/brooklyn/a1");
|
|
312
|
+
// 每 experiment 一行都带紧凑索引(locator + 失败原因),详情块再补一条精确的 attempt locator
|
|
313
|
+
expect(out).toMatch(/✗ failed\s+2 attempts.*@1[0-9a-z]{7}.*gate calledTool/);
|
|
314
|
+
expect(out).toMatch(/attempt locator: @1[0-9a-z]{7}/);
|
|
315
|
+
expect(out).toMatch(/next: niceeval show @1[0-9a-z]{7} \[--eval\|--execution\|--diff\]/);
|
|
281
316
|
});
|
|
282
317
|
});
|
|
283
318
|
|
|
284
319
|
// ───────────────────────── --history:复印件不占行 ─────────────────────────
|
|
285
320
|
|
|
286
321
|
describe("--history 时间轴", () => {
|
|
287
|
-
/**
|
|
322
|
+
/** 快照1 真实执行;快照2 resume 携带同一判定(身份键相同的复印件)+ 新题真实执行。 */
|
|
288
323
|
async function seedHistoryRoot(): Promise<string> {
|
|
289
324
|
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(
|
|
325
|
+
await writeSnapshot(
|
|
299
326
|
root,
|
|
300
327
|
"2026-07-07T09-00-00-000Z",
|
|
301
|
-
|
|
328
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-07T09:00:00.000Z" },
|
|
329
|
+
[res("weather/brooklyn", "passed", { estimatedCostUSD: 0.03 })],
|
|
302
330
|
);
|
|
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(
|
|
331
|
+
// 复印件:同 id / attempt / startedAt(锚定原快照的 startedAt),artifactBase 指回原快照。
|
|
332
|
+
await writeSnapshot(
|
|
310
333
|
root,
|
|
311
334
|
"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
|
-
),
|
|
335
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-09T10:00:00.000Z" },
|
|
336
|
+
[
|
|
337
|
+
res("weather/brooklyn", "passed", {
|
|
338
|
+
estimatedCostUSD: 0.03,
|
|
339
|
+
startedAt: "2026-07-07T09:00:00.000Z",
|
|
340
|
+
artifactBase: "compare_bub/2026-07-07T09-00-00-000Z/weather/brooklyn/a0",
|
|
341
|
+
}),
|
|
342
|
+
res("weather/brooklyn", "failed", {
|
|
343
|
+
attempt: 1,
|
|
344
|
+
estimatedCostUSD: 0.04,
|
|
345
|
+
assertions: [{ name: 'calledTool("get_weather")', severity: "gate", score: 0, passed: false }],
|
|
346
|
+
}),
|
|
347
|
+
],
|
|
329
348
|
);
|
|
330
349
|
return root;
|
|
331
350
|
}
|
|
@@ -335,18 +354,18 @@ describe("--history 时间轴", () => {
|
|
|
335
354
|
const results = await openResults(root);
|
|
336
355
|
const exp = results.experiments.find((e) => e.id === "compare/bub")!;
|
|
337
356
|
const rows = evalHistory(exp, "weather/brooklyn");
|
|
338
|
-
//
|
|
357
|
+
// 快照2 里复印件被识别,真实执行只有:快照1 的 passed + 快照2 的 failed(新 attempt)
|
|
339
358
|
expect(rows).toHaveLength(2);
|
|
340
|
-
expect(rows[0]).toMatchObject({
|
|
359
|
+
expect(rows[0]).toMatchObject({ verdict: "failed", attempts: 1, costUSD: 0.04 });
|
|
341
360
|
expect(rows[0].failedAssertion).toBe('gate calledTool("get_weather")');
|
|
342
|
-
expect(rows[1]).toMatchObject({
|
|
361
|
+
expect(rows[1]).toMatchObject({ verdict: "passed", attempts: 1, costUSD: 0.03 });
|
|
343
362
|
|
|
344
363
|
const { out, code } = await show(root, ["weather/brooklyn"], { history: true });
|
|
345
364
|
expect(code).toBe(0);
|
|
346
365
|
expect(out).toContain("compare/bub · 2 runs · passed 1/2");
|
|
347
366
|
expect(out).toContain("2026-07-09T10-00");
|
|
348
367
|
expect(out).toContain("2026-07-07T09-00");
|
|
349
|
-
// 复印件那份 passed
|
|
368
|
+
// 复印件那份 passed 判定只出现一行(快照1),不在快照2 再占一行
|
|
350
369
|
expect(out.match(/✓ passed/g)).toHaveLength(1);
|
|
351
370
|
});
|
|
352
371
|
|
|
@@ -355,7 +374,7 @@ describe("--history 时间轴", () => {
|
|
|
355
374
|
const { out, code } = await show(root, [], { history: true });
|
|
356
375
|
expect(code).toBe(0);
|
|
357
376
|
expect(out).toContain("compare/bub · 2 runs");
|
|
358
|
-
// 每个快照一行(
|
|
377
|
+
// 每个快照一行(快照2 折叠含携带的 passed 复印件,任一轮通过 → passed)
|
|
359
378
|
expect(out.match(/1\/1 passed/g)).toHaveLength(2);
|
|
360
379
|
});
|
|
361
380
|
});
|
|
@@ -373,7 +392,7 @@ describe("--report 装载", () => {
|
|
|
373
392
|
"const Custom = () => null;",
|
|
374
393
|
"Custom[FACES] = {",
|
|
375
394
|
" web: () => null,",
|
|
376
|
-
" text: (props, ctx) => `CUSTOM ${props.evals} · drill ${ctx.attemptCommand(props.
|
|
395
|
+
" text: (props, ctx) => `CUSTOM ${props.evals} · drill ${ctx.attemptCommand(props.locator)}`,",
|
|
377
396
|
"};",
|
|
378
397
|
"export default {",
|
|
379
398
|
' [Symbol.for("niceeval.report.definition")]: true,',
|
|
@@ -381,7 +400,7 @@ describe("--report 装载", () => {
|
|
|
381
400
|
" type: Custom,",
|
|
382
401
|
" props: {",
|
|
383
402
|
" evals: ctx.selection.snapshots.flatMap((s) => s.evals.map((e) => e.id)).sort().join(\",\"),",
|
|
384
|
-
"
|
|
403
|
+
" locator: ctx.selection.snapshots[0].evals[0].attempts[0].locator,",
|
|
385
404
|
" },",
|
|
386
405
|
" }),",
|
|
387
406
|
"};",
|
|
@@ -392,7 +411,7 @@ describe("--report 装载", () => {
|
|
|
392
411
|
return path;
|
|
393
412
|
}
|
|
394
413
|
|
|
395
|
-
it("装载 +
|
|
414
|
+
it("装载 + 注入 Selection + attemptCommand 下钻命令", async () => {
|
|
396
415
|
const root = await seedComposedRoot();
|
|
397
416
|
const report = await writeReportFile(root);
|
|
398
417
|
const { out, code } = await show(root, [], { report });
|
|
@@ -401,7 +420,7 @@ describe("--report 装载", () => {
|
|
|
401
420
|
expect(out).toContain("drill niceeval show");
|
|
402
421
|
});
|
|
403
422
|
|
|
404
|
-
it("位置前缀对 --report
|
|
423
|
+
it("位置前缀对 --report 生效:收窄注入 Selection 覆盖的 eval", async () => {
|
|
405
424
|
const root = await seedComposedRoot();
|
|
406
425
|
const report = await writeReportFile(root);
|
|
407
426
|
const { out, code } = await show(root, ["weather"], { report });
|
|
@@ -409,18 +428,16 @@ describe("--report 装载", () => {
|
|
|
409
428
|
expect(out).toContain("CUSTOM weather/brooklyn ·");
|
|
410
429
|
});
|
|
411
430
|
|
|
412
|
-
it("--experiment
|
|
431
|
+
it("--experiment 让 Selection 只留该实验", async () => {
|
|
413
432
|
const root = await seedComposedRoot();
|
|
414
|
-
await
|
|
433
|
+
await writeSnapshot(
|
|
415
434
|
root,
|
|
416
435
|
"2026-07-09T11-00-00-000Z",
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
{ agent: "codex", startedAt: "2026-07-09T11:00:00.000Z" },
|
|
420
|
-
),
|
|
436
|
+
{ experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T11:00:00.000Z" },
|
|
437
|
+
[res("weather/brooklyn", "passed")],
|
|
421
438
|
);
|
|
422
439
|
const results = await openResults(root);
|
|
423
|
-
const selection =
|
|
440
|
+
const selection = selectCurrentResults(results, { experiment: "compare/codex" });
|
|
424
441
|
expect(selection.snapshots.map((s) => s.experimentId)).toEqual(["compare/codex"]);
|
|
425
442
|
});
|
|
426
443
|
|
|
@@ -454,52 +471,254 @@ describe("--report 装载", () => {
|
|
|
454
471
|
const { out, code } = await show(root, ["fixtures/button"], { report, diff: true });
|
|
455
472
|
expect(code).toBe(0);
|
|
456
473
|
expect(out).not.toContain("CUSTOM");
|
|
457
|
-
expect(out).toContain("no diff recorded"); // fixture 没有 diff
|
|
474
|
+
expect(out).toContain("no diff recorded"); // fixture 没有 diff artifact:如实说缺
|
|
458
475
|
});
|
|
459
476
|
});
|
|
460
477
|
|
|
461
|
-
// ───────────────────────── 证据切面:--
|
|
478
|
+
// ───────────────────────── 证据切面:--execution ─────────────────────────
|
|
462
479
|
|
|
463
|
-
describe("--
|
|
464
|
-
it("
|
|
480
|
+
describe("--execution", () => {
|
|
481
|
+
it("标准事件流(消息);没有 OTel 时如实标 timing unavailable", async () => {
|
|
465
482
|
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
|
-
},
|
|
483
|
+
const dir = await writeSnapshot(
|
|
484
|
+
root,
|
|
485
|
+
"2026-07-09T10-00-00-000Z",
|
|
486
|
+
{ experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" },
|
|
487
|
+
[res("weather/brooklyn", "failed", { attempt: 2, durationMs: 41_000, hasEvents: true })],
|
|
486
488
|
);
|
|
487
|
-
await
|
|
489
|
+
await writeFile(
|
|
490
|
+
join(dir, "weather/brooklyn/a2/events.json"),
|
|
491
|
+
JSON.stringify([
|
|
492
|
+
{ type: "message", role: "user", text: "布鲁克林今天天气怎么样?" },
|
|
493
|
+
{ type: "message", role: "assistant", text: "布鲁克林今天大约 24°C,晴。" },
|
|
494
|
+
]),
|
|
495
|
+
"utf-8",
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { execution: true });
|
|
499
|
+
expect(code).toBe(0);
|
|
500
|
+
expect(out).toMatch(/^@1[0-9a-z]{7} · weather\/brooklyn · compare\/codex · failed/m);
|
|
501
|
+
expect(out).toMatch(/USER\s+布鲁克林今天天气怎么样?/);
|
|
502
|
+
expect(out).toMatch(/ASSISTANT\s+布鲁克林今天大约 24°C,晴。/);
|
|
503
|
+
expect(out).toContain("timing unavailable · OTel trace was not collected");
|
|
504
|
+
expect(out).toContain("full events: ");
|
|
505
|
+
expect(out).toContain("events.json");
|
|
506
|
+
});
|
|
488
507
|
|
|
489
|
-
|
|
508
|
+
it("execution === null 时如实说没有事件记录", async () => {
|
|
509
|
+
const root = await makeRoot();
|
|
510
|
+
await writeSnapshot(
|
|
511
|
+
root,
|
|
512
|
+
"2026-07-08T10-00-00-000Z",
|
|
513
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
|
|
514
|
+
[res("weather/brooklyn", "passed")],
|
|
515
|
+
);
|
|
516
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { execution: true });
|
|
490
517
|
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)");
|
|
518
|
+
expect(out).toContain("no events recorded for this attempt");
|
|
497
519
|
});
|
|
498
520
|
|
|
499
|
-
it("
|
|
521
|
+
it("不把未关联的 telemetry spans 混进 Agent 执行记录", async () => {
|
|
522
|
+
const root = await makeRoot();
|
|
523
|
+
const dir = await writeSnapshot(root, "2026-07-09T10-00-00-000Z", { experimentId: "compare/codex", startedAt: "2026-07-09T10:00:00.000Z" }, [
|
|
524
|
+
res("weather/brooklyn", "failed", { hasEvents: true, hasTrace: true }),
|
|
525
|
+
]);
|
|
526
|
+
await writeFile(join(dir, "weather/brooklyn/a0/events.json"), JSON.stringify([{ type: "message", role: "user", text: "hello" }]), "utf-8");
|
|
527
|
+
await writeFile(join(dir, "weather/brooklyn/a0/trace.json"), JSON.stringify([{ traceId: "t", spanId: "s", name: "sdk.internal", startMs: 1, endMs: 2 }]), "utf-8");
|
|
528
|
+
const { out } = await show(root, ["weather/brooklyn"], { execution: true });
|
|
529
|
+
expect(out).toContain("USER");
|
|
530
|
+
expect(out).not.toMatch(/telemetry\s+sdk\.internal/);
|
|
531
|
+
expect(out).toContain("unlinked telemetry spans omitted");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("多个 eval 匹配时证据切面报错:给紧凑索引(locator + 失败原因)而不是只报个数", async () => {
|
|
500
535
|
const root = await seedComposedRoot();
|
|
501
|
-
const { err, code } = await show(root, [], {
|
|
536
|
+
const { err, code } = await show(root, [], { execution: true });
|
|
537
|
+
expect(code).toBe(1);
|
|
538
|
+
expect(err).toContain("matched 2 evals");
|
|
539
|
+
expect(err).toMatch(/✗ fixtures\/button\s+@1[0-9a-z]{7}/);
|
|
540
|
+
expect(err).toContain('gate fileChanged("src/components/Button.tsx")');
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// ───────────────────────── 证据切面:--eval ─────────────────────────
|
|
545
|
+
|
|
546
|
+
describe("--eval", () => {
|
|
547
|
+
it("evalSource === null 时如实说源码未捕获", async () => {
|
|
548
|
+
const root = await makeRoot();
|
|
549
|
+
await writeSnapshot(
|
|
550
|
+
root,
|
|
551
|
+
"2026-07-08T10-00-00-000Z",
|
|
552
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
|
|
553
|
+
[res("weather/brooklyn", "failed", { assertions: [{ name: "succeeded()", severity: "gate", score: 0, passed: false }] })],
|
|
554
|
+
);
|
|
555
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { eval: true });
|
|
556
|
+
expect(code).toBe(0);
|
|
557
|
+
expect(out).toContain("eval source unavailable for this attempt");
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("有捕获的源码时:源码行标回断言,unmapped 断言单独成段,断言计票摘要", async () => {
|
|
561
|
+
const root = await makeRoot();
|
|
562
|
+
const dir = await writeSnapshot(
|
|
563
|
+
root,
|
|
564
|
+
"2026-07-08T10-00-00-000Z",
|
|
565
|
+
{ experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
|
|
566
|
+
[
|
|
567
|
+
res("weather/brooklyn", "failed", {
|
|
568
|
+
assertions: [
|
|
569
|
+
{
|
|
570
|
+
name: 'calledTool("get_weather")',
|
|
571
|
+
severity: "gate",
|
|
572
|
+
score: 0,
|
|
573
|
+
passed: false,
|
|
574
|
+
detail: "tool was never called",
|
|
575
|
+
loc: { file: "evals/weather/brooklyn.eval.ts", line: 2 },
|
|
576
|
+
},
|
|
577
|
+
{ name: "unlocated()", severity: "soft", score: 0.5, passed: false, detail: "no loc on this one" },
|
|
578
|
+
],
|
|
579
|
+
}),
|
|
580
|
+
],
|
|
581
|
+
);
|
|
582
|
+
const attemptDir = join(dir, "weather/brooklyn/a0");
|
|
583
|
+
const content = 'defineEval({\n turn.calledTool("get_weather");\n});\n';
|
|
584
|
+
const sha = createHash("sha256").update(content).digest("hex");
|
|
585
|
+
await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/weather/brooklyn.eval.ts", sha256: sha }]), "utf-8");
|
|
586
|
+
await mkdir(join(dir, "sources"), { recursive: true });
|
|
587
|
+
await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
|
|
588
|
+
|
|
589
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { eval: true });
|
|
590
|
+
expect(code).toBe(0);
|
|
591
|
+
expect(out).toContain(`eval source: evals/weather/brooklyn.eval.ts · sha256:${sha.slice(0, 8)}`);
|
|
592
|
+
// 源码行的缩进是语义的一部分,渲染必须原样保留(不按词重排折叠掉这两个空格)。
|
|
593
|
+
expect(out).toMatch(/2✗ {3}turn\.calledTool\("get_weather"\);/);
|
|
594
|
+
expect(out).toContain('gate · calledTool("get_weather") · tool was never called');
|
|
595
|
+
expect(out).toContain("unmapped assertions (1, no source location):");
|
|
596
|
+
expect(out).toContain("unlocated()");
|
|
597
|
+
expect(out).toContain("assertions: 1 gate failed · 1 soft below target");
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("源码行标注保留 group 与值断言的 expected/received", async () => {
|
|
601
|
+
const root = await makeRoot();
|
|
602
|
+
const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
603
|
+
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 } }] }),
|
|
604
|
+
]);
|
|
605
|
+
const attemptDir = join(dir, "manager/a0");
|
|
606
|
+
const content = "t.check(actual, equals(expected));\n";
|
|
607
|
+
const sha = createHash("sha256").update(content).digest("hex");
|
|
608
|
+
await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/manager.eval.ts", sha256: sha }]), "utf-8");
|
|
609
|
+
await mkdir(join(dir, "sources"), { recursive: true });
|
|
610
|
+
await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
|
|
611
|
+
const { out } = await show(root, ["manager"], { eval: true });
|
|
612
|
+
expect(out).toContain("gate · Issue 15193 · equals(4) · expected 4 · received 1");
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// ───────────────────────── @<locator> ─────────────────────────
|
|
617
|
+
|
|
618
|
+
describe("show @<locator>", () => {
|
|
619
|
+
it("默认页直接解释失败断言的 group、期望值、实际值和源码位置", async () => {
|
|
620
|
+
const root = await makeRoot();
|
|
621
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
622
|
+
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 } }] }),
|
|
623
|
+
]);
|
|
624
|
+
const results = await openResults(root);
|
|
625
|
+
const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
|
|
626
|
+
const { out } = await show(root, [locator]);
|
|
627
|
+
expect(out).toContain("gate · Issue 15193");
|
|
628
|
+
expect(out).toContain("assertion: equals(4)");
|
|
629
|
+
expect(out).toContain("expected: 4");
|
|
630
|
+
expect(out).toContain("received: 1");
|
|
631
|
+
expect(out).toContain("source: evals/manager.eval.ts:40:11");
|
|
632
|
+
});
|
|
633
|
+
it("解析到对应 attempt,渲染紧凑全景(不当成 eval id 前缀匹配)", async () => {
|
|
634
|
+
const root = await makeRoot();
|
|
635
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
636
|
+
res("weather/brooklyn", "passed"),
|
|
637
|
+
]);
|
|
638
|
+
const results = await openResults(root);
|
|
639
|
+
const attempt = results.experiments[0]!.latest.evals[0]!.attempts[0]!;
|
|
640
|
+
const locator = attempt.locator!;
|
|
641
|
+
expect(locator).toMatch(/^@1[0-9a-z]{7}$/);
|
|
642
|
+
|
|
643
|
+
const { out, code } = await show(root, [locator]);
|
|
644
|
+
expect(code).toBe(0);
|
|
645
|
+
expect(out).toContain(`${locator} · weather/brooklyn · compare/bub · passed`);
|
|
646
|
+
expect(out).toContain("attempt 1 · ");
|
|
647
|
+
expect(out).toContain("execution: unavailable (no events recorded for this attempt)");
|
|
648
|
+
expect(out).toContain("changes: diff unavailable");
|
|
649
|
+
expect(out).not.toContain("evidence:");
|
|
650
|
+
expect(out).not.toContain("available:");
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("available 只逐行列出实际存在的证据命令,不打印能力字母或合并式 next", async () => {
|
|
654
|
+
const root = await makeRoot();
|
|
655
|
+
const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
656
|
+
res("weather/brooklyn", "failed", { hasEvents: true }),
|
|
657
|
+
]);
|
|
658
|
+
const attemptDir = join(dir, "weather/brooklyn/a0");
|
|
659
|
+
const content = "t.check(false, equals(true));\n";
|
|
660
|
+
const sha = createHash("sha256").update(content).digest("hex");
|
|
661
|
+
await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/weather.eval.ts", sha256: sha }]), "utf-8");
|
|
662
|
+
await mkdir(join(dir, "sources"), { recursive: true });
|
|
663
|
+
await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
|
|
664
|
+
await writeFile(join(attemptDir, "events.json"), JSON.stringify([{ type: "message", role: "user", text: "hello" }]), "utf-8");
|
|
665
|
+
const results = await openResults(root);
|
|
666
|
+
const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
|
|
667
|
+
|
|
668
|
+
const { out, code } = await show(root, [locator]);
|
|
669
|
+
expect(code).toBe(0);
|
|
670
|
+
expect(out).toContain(`available:\n niceeval show ${locator} --eval\n niceeval show ${locator} --execution`);
|
|
671
|
+
expect(out).not.toContain(`niceeval show ${locator} --diff`);
|
|
672
|
+
expect(out).not.toContain("evidence:");
|
|
673
|
+
expect(out).not.toContain("next:");
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("配 --execution 走证据切面,不是紧凑全景", async () => {
|
|
677
|
+
const root = await makeRoot();
|
|
678
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
679
|
+
res("weather/brooklyn", "passed"),
|
|
680
|
+
]);
|
|
681
|
+
const results = await openResults(root);
|
|
682
|
+
const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
|
|
683
|
+
|
|
684
|
+
const { out, code } = await show(root, [locator], { execution: true });
|
|
685
|
+
expect(code).toBe(0);
|
|
686
|
+
expect(out).toContain(`${locator} · weather/brooklyn · compare/bub · passed`);
|
|
687
|
+
expect(out).toContain("no events recorded for this attempt");
|
|
688
|
+
// 紧凑全景独有的行不应该出现在证据切面输出里
|
|
689
|
+
expect(out).not.toContain("available:");
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("语法不对的 locator 报「not a valid attempt locator」,退出码 1,不崩", async () => {
|
|
693
|
+
const root = await makeRoot();
|
|
694
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
695
|
+
res("weather/brooklyn", "passed"),
|
|
696
|
+
]);
|
|
697
|
+
const { err, code } = await show(root, ["@not-valid"]);
|
|
698
|
+
expect(code).toBe(1);
|
|
699
|
+
expect(err).toContain("not a valid attempt locator");
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("语法合法但索引里没有的 locator 报「No attempt found」,退出码 1,不崩", async () => {
|
|
703
|
+
const root = await makeRoot();
|
|
704
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
705
|
+
res("weather/brooklyn", "passed"),
|
|
706
|
+
]);
|
|
707
|
+
const { err, code } = await show(root, ["@1nosuch1"]);
|
|
708
|
+
expect(code).toBe(1);
|
|
709
|
+
expect(err).toContain("No attempt found");
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("locator 与其它位置参数混用时报错,不静默只取第一个", async () => {
|
|
713
|
+
const root = await makeRoot();
|
|
714
|
+
await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
|
|
715
|
+
res("weather/brooklyn", "passed"),
|
|
716
|
+
]);
|
|
717
|
+
const results = await openResults(root);
|
|
718
|
+
const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
|
|
719
|
+
|
|
720
|
+
const { err, code } = await show(root, [locator, "weather/brooklyn"]);
|
|
502
721
|
expect(code).toBe(1);
|
|
503
|
-
expect(err).toContain("
|
|
722
|
+
expect(err).toContain("must be the only positional argument");
|
|
504
723
|
});
|
|
505
724
|
});
|