niceeval 0.4.5 → 0.5.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/docs-site/zh/concepts/adapter.mdx +6 -6
- package/docs-site/zh/concepts/experiment.mdx +7 -7
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +63 -20
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +243 -0
- package/docs-site/zh/guides/experiments.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +252 -0
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +224 -0
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/viewing-results.mdx +147 -77
- package/docs-site/zh/guides/write-experiment.mdx +9 -9
- package/docs-site/zh/guides/write-send.mdx +6 -6
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/introduction.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +10 -2
- package/docs-site/zh/reference/define-agent.mdx +5 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/package.json +3 -2
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/ai-sdk.ts +2 -2
- package/src/agents/bub.ts +14 -4
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +4 -4
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +104 -14
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +3 -3
- package/src/context/session.ts +2 -2
- package/src/context/types.ts +2 -2
- package/src/i18n/en.ts +29 -7
- package/src/i18n/zh-CN.ts +24 -2
- package/src/report/aggregate.ts +79 -50
- package/src/report/components.tsx +190 -0
- package/src/report/compute.ts +149 -84
- package/src/report/default-report.tsx +222 -0
- package/src/report/dual-face.test.tsx +527 -0
- package/src/report/format.ts +29 -0
- package/src/report/index.ts +79 -24
- package/src/report/load.ts +67 -0
- package/src/report/metrics.ts +2 -1
- package/src/report/param.ts +18 -0
- package/src/report/primitives.tsx +117 -0
- package/src/report/react/CaseList.tsx +2 -2
- package/src/report/react/DeltaTable.tsx +2 -2
- package/src/report/react/MetricBars.tsx +109 -0
- package/src/report/react/MetricLine.tsx +200 -0
- package/src/report/react/MetricMatrix.tsx +1 -1
- package/src/report/react/MetricScatter.tsx +1 -1
- package/src/report/react/MetricTable.tsx +1 -1
- package/src/report/react/RunOverview.tsx +4 -4
- package/src/report/react/Scoreboard.tsx +3 -3
- package/src/report/react/cell.tsx +1 -1
- package/src/report/react/fixtures.ts +98 -36
- package/src/report/react/format.ts +3 -28
- package/src/report/react/index.tsx +41 -24
- package/src/report/react/render.test.tsx +8 -8
- package/src/report/react/styles.css +77 -0
- package/src/report/report.test.ts +248 -128
- package/src/report/report.ts +69 -0
- package/src/report/text/faces.ts +356 -0
- package/src/report/text/layout.ts +125 -0
- package/src/report/text/plot.ts +127 -0
- package/src/report/tree.ts +220 -0
- package/src/report/types.ts +75 -27
- package/src/report/web.ts +40 -0
- package/src/results/copy.ts +95 -41
- package/src/results/format.ts +12 -11
- package/src/results/index.ts +34 -17
- package/src/results/open.ts +201 -81
- package/src/results/results.test.ts +595 -191
- package/src/results/select.ts +107 -55
- package/src/results/types.ts +137 -45
- package/src/results/writer.ts +258 -0
- package/src/runner/attempt.ts +3 -3
- package/src/runner/fingerprint.ts +1 -1
- package/src/runner/reporters/artifacts.ts +38 -78
- package/src/runner/reporters/braintrust.test.ts +2 -2
- package/src/runner/reporters/braintrust.ts +3 -3
- package/src/runner/run.ts +1 -1
- package/src/runner/types.ts +18 -12
- package/src/show/compose.ts +215 -0
- package/src/show/index.ts +263 -0
- package/src/show/render.ts +456 -0
- package/src/show/show.test.ts +505 -0
- package/src/view/app/App.tsx +85 -24
- package/src/view/app/components/CostScoreChart.tsx +29 -10
- package/src/view/app/components/ExperimentTable.tsx +27 -9
- package/src/view/app/components/GroupSelector.tsx +1 -1
- package/src/view/app/i18n.ts +18 -3
- package/src/view/app/lib/attempt-route.test.ts +9 -9
- package/src/view/app/lib/attempt-route.ts +5 -5
- package/src/view/app/lib/outcome.ts +1 -3
- package/src/view/app/lib/rows.ts +95 -15
- package/src/view/app/main.tsx +18 -6
- package/src/view/app/pages/RunsPage.tsx +4 -7
- package/src/view/app/pages/TracesPage.tsx +3 -8
- package/src/view/app/types.ts +50 -6
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.test.ts +254 -0
- package/src/view/data.ts +336 -0
- package/src/view/index.ts +64 -10
- package/src/view/server.ts +31 -9
- package/src/view/shared/types.ts +47 -41
- package/src/view/styles.css +5 -0
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +308 -0
- package/docs-site/.mintignore +0 -13
- package/docs-site/AGENTS.md +0 -46
- package/docs-site/concepts/adapter.mdx +0 -287
- package/docs-site/concepts/assert.mdx +0 -243
- package/docs-site/concepts/drive.mdx +0 -118
- package/docs-site/concepts/evals.mdx +0 -108
- package/docs-site/concepts/experiment.mdx +0 -37
- package/docs-site/concepts/hitl.mdx +0 -83
- package/docs-site/concepts/judge.mdx +0 -129
- package/docs-site/concepts/overview.mdx +0 -98
- package/docs-site/concepts/tier.mdx +0 -42
- package/docs-site/docs-ref/00-index.md +0 -23
- package/docs-site/docs-ref/01-page-types.md +0 -43
- package/docs-site/docs-ref/03-style-rules.md +0 -45
- package/docs-site/docs-ref/04-code-examples.md +0 -37
- package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
- package/docs-site/docs-ref/06-checklists.md +0 -53
- package/docs-site/docs.json +0 -256
- package/docs-site/example/ai-agent-application.mdx +0 -148
- package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
- package/docs-site/example/claude-code-codex-skill.mdx +0 -147
- package/docs-site/example/showcase.mdx +0 -39
- package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
- package/docs-site/example/tier1-claude-sdk.mdx +0 -119
- package/docs-site/example/tier1-codex-sdk.mdx +0 -111
- package/docs-site/example/tier1-langgraph.mdx +0 -119
- package/docs-site/example/tier1-pi-sdk.mdx +0 -119
- package/docs-site/favicon.svg +0 -5
- package/docs-site/github-diff.css +0 -135
- package/docs-site/github-diff.js +0 -11
- package/docs-site/guides/authoring.mdx +0 -129
- package/docs-site/guides/ci-integration.mdx +0 -97
- package/docs-site/guides/connect-otel.mdx +0 -209
- package/docs-site/guides/connect-your-agent.mdx +0 -221
- package/docs-site/guides/dataset-fanout.mdx +0 -98
- package/docs-site/guides/experiments.mdx +0 -71
- package/docs-site/guides/fixtures.mdx +0 -147
- package/docs-site/guides/reporters.mdx +0 -113
- package/docs-site/guides/runner.mdx +0 -79
- package/docs-site/guides/sandbox-agent.mdx +0 -138
- package/docs-site/guides/sandbox-backends.mdx +0 -64
- package/docs-site/guides/scoring-guide.mdx +0 -92
- package/docs-site/guides/viewing-results.mdx +0 -195
- package/docs-site/guides/write-experiment.mdx +0 -81
- package/docs-site/guides/write-send.mdx +0 -347
- package/docs-site/index.mdx +0 -181
- package/docs-site/introduction.mdx +0 -141
- package/docs-site/quickstart.mdx +0 -136
- package/docs-site/reference/builtin-agents.mdx +0 -161
- package/docs-site/reference/capabilities.mdx +0 -76
- package/docs-site/reference/cli.mdx +0 -120
- package/docs-site/reference/define-agent.mdx +0 -168
- package/docs-site/reference/define-config.mdx +0 -41
- package/docs-site/reference/define-eval.mdx +0 -160
- package/docs-site/reference/events.mdx +0 -131
- package/docs-site/reference/expect.mdx +0 -112
- package/docs-site/tracker.js +0 -8
- package/src/report/react/data.ts +0 -17
- package/src/view/aggregate.ts +0 -103
- package/src/view/loader.test.ts +0 -61
- package/src/view/loader.ts +0 -257
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// niceeval show 终端宿主的测试(行为规范:docs-site/zh/guides/viewing-results.mdx;
|
|
2
|
+
// 组合语义:docs/reports.md「宿主输入的组合语义」)。覆盖:
|
|
3
|
+
// - 榜单合成口径:每 experiment × eval 取最新判决,局部重跑从更早 run 补齐,头部标注合成自几个 run;
|
|
4
|
+
// - 前缀过滤收窄选集,覆盖警告分母 = 已知并集 ∩ 范围;
|
|
5
|
+
// - --history 时间轴只列真实执行,resume 携带的复印件不占行;
|
|
6
|
+
// - --report 装载(合法 / 非法默认导出 / 文件缺失)、位置前缀收窄注入选集、attemptCommand 下钻;
|
|
7
|
+
// - 互斥:--history 与 --report;
|
|
8
|
+
// - 单 eval 详情与 --transcript 证据切面的输出形态。
|
|
9
|
+
|
|
10
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
11
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
RESULTS_FORMAT,
|
|
16
|
+
RESULTS_SCHEMA_VERSION,
|
|
17
|
+
createRunWriter,
|
|
18
|
+
openResults,
|
|
19
|
+
type EvalResult,
|
|
20
|
+
type RunSummary,
|
|
21
|
+
} from "../results/index.ts";
|
|
22
|
+
import { composeShowSelection, evalHistory } from "./compose.ts";
|
|
23
|
+
import { runShow, type ShowFlags } from "./index.ts";
|
|
24
|
+
|
|
25
|
+
// ───────────────────────── fixture 工具 ─────────────────────────
|
|
26
|
+
|
|
27
|
+
const roots: string[] = [];
|
|
28
|
+
async function makeRoot(): Promise<string> {
|
|
29
|
+
const root = await mkdtemp(join(tmpdir(), "niceeval-show-"));
|
|
30
|
+
roots.push(root);
|
|
31
|
+
return root;
|
|
32
|
+
}
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await Promise.all(roots.splice(0).map((r) => rm(r, { recursive: true, force: true })));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function res(over: Partial<EvalResult> & Pick<EvalResult, "id">): EvalResult {
|
|
38
|
+
return {
|
|
39
|
+
agent: "bub",
|
|
40
|
+
outcome: "passed",
|
|
41
|
+
attempt: 0,
|
|
42
|
+
durationMs: 1000,
|
|
43
|
+
assertions: [],
|
|
44
|
+
...over,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function summaryOf(results: EvalResult[], over: Partial<RunSummary> = {}): RunSummary {
|
|
49
|
+
const count = (o: EvalResult["outcome"]) => results.filter((r) => r.outcome === o).length;
|
|
50
|
+
return {
|
|
51
|
+
format: RESULTS_FORMAT,
|
|
52
|
+
schemaVersion: RESULTS_SCHEMA_VERSION,
|
|
53
|
+
producer: { name: "niceeval", version: "0.4.6" },
|
|
54
|
+
agent: results[0]?.agent ?? "bub",
|
|
55
|
+
startedAt: "2026-07-08T10:00:00.000Z",
|
|
56
|
+
completedAt: "2026-07-08T10:10:00.000Z",
|
|
57
|
+
passed: count("passed"),
|
|
58
|
+
failed: count("failed"),
|
|
59
|
+
skipped: count("skipped"),
|
|
60
|
+
errored: count("errored"),
|
|
61
|
+
durationMs: 60_000,
|
|
62
|
+
results,
|
|
63
|
+
...over,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function writeRun(root: string, dirName: string, summary: RunSummary): Promise<void> {
|
|
68
|
+
const dir = join(root, dirName);
|
|
69
|
+
await mkdir(dir, { recursive: true });
|
|
70
|
+
await writeFile(join(dir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface Captured {
|
|
74
|
+
out: string;
|
|
75
|
+
err: string;
|
|
76
|
+
code: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function show(root: string, patterns: string[], flags: ShowFlags = {}): Promise<Captured> {
|
|
80
|
+
let out = "";
|
|
81
|
+
let err = "";
|
|
82
|
+
const code = await runShow(root, patterns, { run: root, ...flags }, {
|
|
83
|
+
out: (s) => (out += s),
|
|
84
|
+
err: (s) => (err += s),
|
|
85
|
+
width: 100,
|
|
86
|
+
now: Date.parse("2026-07-09T10:01:00.000Z"),
|
|
87
|
+
});
|
|
88
|
+
return { out, err, code };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 两个 run:老 run 全量(a ✓ b ✓),新 run 只重跑 b(✗)—— 榜单该跨 run 合成。 */
|
|
92
|
+
async function seedComposedRoot(): Promise<string> {
|
|
93
|
+
const root = await makeRoot();
|
|
94
|
+
await writeRun(
|
|
95
|
+
root,
|
|
96
|
+
"2026-07-08T10-00-00-000Z",
|
|
97
|
+
summaryOf(
|
|
98
|
+
[
|
|
99
|
+
res({ id: "weather/brooklyn", experimentId: "compare/bub", startedAt: "2026-07-08T10:00:01.000Z" }),
|
|
100
|
+
res({ id: "fixtures/button", experimentId: "compare/bub", startedAt: "2026-07-08T10:00:02.000Z" }),
|
|
101
|
+
],
|
|
102
|
+
{ startedAt: "2026-07-08T10:00:00.000Z" },
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
await writeRun(
|
|
106
|
+
root,
|
|
107
|
+
"2026-07-09T10-00-00-000Z",
|
|
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
|
+
}),
|
|
125
|
+
],
|
|
126
|
+
{ startedAt: "2026-07-09T10:00:00.000Z" },
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
return root;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ───────────────────────── 榜单合成口径 ─────────────────────────
|
|
133
|
+
|
|
134
|
+
describe("榜单:跨 run 合成的现刻水位", () => {
|
|
135
|
+
it("局部重跑不撕榜单:另一题从更早 run 补齐,头部如实标注合成自 2 个 run", async () => {
|
|
136
|
+
const root = await seedComposedRoot();
|
|
137
|
+
const { out, code } = await show(root, []);
|
|
138
|
+
expect(code).toBe(0);
|
|
139
|
+
expect(out).toContain("Current verdicts · 1 experiment · composed from 2 runs");
|
|
140
|
+
expect(out).toContain("latest 2026-07-09T10-00-00-000Z");
|
|
141
|
+
// eval 级折叠计票:2 题里 1 题通过;两题都在(没有 partial-coverage 撕榜)
|
|
142
|
+
expect(out).toContain("1/2");
|
|
143
|
+
expect(out).toContain("50%");
|
|
144
|
+
expect(out).not.toContain("verdicts cover");
|
|
145
|
+
// Failing 清单:新判决的 fixtures/button,带失败断言与下钻命令
|
|
146
|
+
expect(out).toContain("Failing:");
|
|
147
|
+
expect(out).toContain("✗ fixtures/button");
|
|
148
|
+
expect(out).toContain('gate fileChanged("src/components/Button.tsx")');
|
|
149
|
+
expect(out).toContain("→ niceeval show fixtures/button");
|
|
150
|
+
expect(out).not.toContain("✗ weather/brooklyn");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("合成选集:每 experiment × eval 取最新判决;compose 不产生残缺警告", async () => {
|
|
154
|
+
const root = await seedComposedRoot();
|
|
155
|
+
const results = await openResults(root);
|
|
156
|
+
const selection = composeShowSelection(results);
|
|
157
|
+
expect(selection.snapshots).toHaveLength(1);
|
|
158
|
+
const evals = selection.snapshots[0].evals.map((e) => e.id).sort();
|
|
159
|
+
expect(evals).toEqual(["fixtures/button", "weather/brooklyn"]);
|
|
160
|
+
const button = selection.snapshots[0].evals.find((e) => e.id === "fixtures/button")!;
|
|
161
|
+
expect(button.attempts[0].result.outcome).toBe("failed"); // 新 run 的判决赢
|
|
162
|
+
expect(selection.warnings).toEqual([]);
|
|
163
|
+
// 对照:results.latest() 的最新快照是残缺的(这正是宿主要合成的原因)
|
|
164
|
+
expect(results.latest().warnings.some((w) => w.kind === "partial-coverage")).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ───────────────────────── 前缀过滤 ─────────────────────────
|
|
169
|
+
|
|
170
|
+
describe("位置前缀收窄", () => {
|
|
171
|
+
it("前缀收窄选集覆盖的 eval;覆盖警告分母 = 已知并集 ∩ 范围", async () => {
|
|
172
|
+
const root = await makeRoot();
|
|
173
|
+
await writeRun(
|
|
174
|
+
root,
|
|
175
|
+
"2026-07-08T10-00-00-000Z",
|
|
176
|
+
summaryOf(
|
|
177
|
+
[
|
|
178
|
+
res({ id: "weather/brooklyn", experimentId: "compare/bub", startedAt: "2026-07-08T10:00:01.000Z" }),
|
|
179
|
+
res({ id: "algebra/quadratic", experimentId: "compare/bub", startedAt: "2026-07-08T10:00:02.000Z" }),
|
|
180
|
+
],
|
|
181
|
+
{
|
|
182
|
+
startedAt: "2026-07-08T10:00:00.000Z",
|
|
183
|
+
// 已知并集包含一道从未落盘的题:algebra 范围外,不该刷 weather 范围的屏
|
|
184
|
+
snapshots: { "compare/bub": { knownEvalIds: ["weather/brooklyn", "weather/queens", "algebra/quadratic"] } },
|
|
185
|
+
},
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
const results = await openResults(root);
|
|
189
|
+
|
|
190
|
+
const weather = composeShowSelection(results, { patterns: ["weather"] });
|
|
191
|
+
expect(weather.snapshots[0].evals.map((e) => e.id)).toEqual(["weather/brooklyn"]);
|
|
192
|
+
// 分母 = {weather/brooklyn, weather/queens}:缺 queens → 1/2;algebra 的缺口不进来
|
|
193
|
+
const coverage = weather.warnings.find((w) => w.kind === "partial-coverage");
|
|
194
|
+
expect(coverage).toMatchObject({ covered: 1, total: 2 });
|
|
195
|
+
|
|
196
|
+
const algebra = composeShowSelection(results, { patterns: ["algebra"] });
|
|
197
|
+
expect(algebra.warnings.filter((w) => w.kind === "partial-coverage")).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("前缀匹配不到任何结果:直说 + 列出有结果的 eval", async () => {
|
|
201
|
+
const root = await seedComposedRoot();
|
|
202
|
+
const { err, code } = await show(root, ["nosuch"]);
|
|
203
|
+
expect(code).toBe(1);
|
|
204
|
+
expect(err).toContain("No results matched: nosuch");
|
|
205
|
+
expect(err).toContain("weather/brooklyn");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ───────────────────────── 单 eval 详情 ─────────────────────────
|
|
210
|
+
|
|
211
|
+
describe("单 eval 详情", () => {
|
|
212
|
+
it("attempt 行 + 断言明细 + artifacts 路径 + 下钻提示", async () => {
|
|
213
|
+
const root = await makeRoot();
|
|
214
|
+
await writeRun(
|
|
215
|
+
root,
|
|
216
|
+
"2026-07-09T10-00-00-000Z",
|
|
217
|
+
summaryOf(
|
|
218
|
+
[
|
|
219
|
+
res({
|
|
220
|
+
id: "weather/brooklyn",
|
|
221
|
+
experimentId: "compare/codex",
|
|
222
|
+
agent: "codex",
|
|
223
|
+
outcome: "failed",
|
|
224
|
+
attempt: 0,
|
|
225
|
+
startedAt: "2026-07-09T10:00:01.000Z",
|
|
226
|
+
durationMs: 40_000,
|
|
227
|
+
}),
|
|
228
|
+
res({
|
|
229
|
+
id: "weather/brooklyn",
|
|
230
|
+
experimentId: "compare/codex",
|
|
231
|
+
agent: "codex",
|
|
232
|
+
outcome: "failed",
|
|
233
|
+
attempt: 1,
|
|
234
|
+
startedAt: "2026-07-09T10:00:42.000Z",
|
|
235
|
+
durationMs: 41_000,
|
|
236
|
+
usage: { inputTokens: 12_000, outputTokens: 300 },
|
|
237
|
+
estimatedCostUSD: 0.04,
|
|
238
|
+
artifactsDir: "artifacts/compare__codex__weather__brooklyn__1",
|
|
239
|
+
assertions: [
|
|
240
|
+
{
|
|
241
|
+
name: 'calledTool("get_weather")',
|
|
242
|
+
severity: "gate",
|
|
243
|
+
score: 0,
|
|
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
|
+
),
|
|
260
|
+
);
|
|
261
|
+
const { out, code } = await show(root, ["weather/brooklyn"]);
|
|
262
|
+
expect(code).toBe(0);
|
|
263
|
+
expect(out).toContain("weather/brooklyn");
|
|
264
|
+
expect(out).toContain("compare/codex");
|
|
265
|
+
expect(out).toContain("✗ failed");
|
|
266
|
+
expect(out).toContain("2 attempts");
|
|
267
|
+
// 详情块默认挑最新一次失败的 attempt(人看 1 计:attempt 2)
|
|
268
|
+
expect(out).toContain("attempt 2 · compare/codex · failed · 41.0s · 12.3k tokens · $0.04");
|
|
269
|
+
expect(out).toContain('✗ gate calledTool("get_weather") — tool was never called');
|
|
270
|
+
expect(out).toContain("✓ gate succeeded()");
|
|
271
|
+
expect(out).toContain('✗ soft judge("回答基于实时数据") — 0.2/1: reply invents a temperature');
|
|
272
|
+
expect(out).toContain("artifacts/compare__codex__weather__brooklyn__1/");
|
|
273
|
+
expect(out).toContain("next: niceeval show weather/brooklyn --transcript | --trace | --diff");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("--attempt 指定不存在的 attempt:直说可用序号", async () => {
|
|
277
|
+
const root = await seedComposedRoot();
|
|
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");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ───────────────────────── --history:复印件不占行 ─────────────────────────
|
|
285
|
+
|
|
286
|
+
describe("--history 时间轴", () => {
|
|
287
|
+
/** run1 真实执行;run2 resume 携带同一判决(身份键相同的复印件)+ 新题真实执行。 */
|
|
288
|
+
async function seedHistoryRoot(): Promise<string> {
|
|
289
|
+
const root = await makeRoot();
|
|
290
|
+
const original = res({
|
|
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(
|
|
299
|
+
root,
|
|
300
|
+
"2026-07-07T09-00-00-000Z",
|
|
301
|
+
summaryOf([original], { startedAt: "2026-07-07T09:00:00.000Z" }),
|
|
302
|
+
);
|
|
303
|
+
// 复印件:同 id / attempt / startedAt,artifactBase 指回原 run(resume 合入的形状)
|
|
304
|
+
const { artifactsDir: _dropped, ...carriedBase } = original;
|
|
305
|
+
const carried: EvalResult = {
|
|
306
|
+
...carriedBase,
|
|
307
|
+
artifactBase: "2026-07-07T09-00-00-000Z/artifacts/compare__bub__weather__brooklyn__0",
|
|
308
|
+
};
|
|
309
|
+
await writeRun(
|
|
310
|
+
root,
|
|
311
|
+
"2026-07-09T10-00-00-000Z",
|
|
312
|
+
summaryOf(
|
|
313
|
+
[
|
|
314
|
+
carried,
|
|
315
|
+
res({
|
|
316
|
+
id: "weather/brooklyn",
|
|
317
|
+
experimentId: "compare/bub",
|
|
318
|
+
outcome: "failed",
|
|
319
|
+
attempt: 1,
|
|
320
|
+
startedAt: "2026-07-09T10:00:05.000Z",
|
|
321
|
+
estimatedCostUSD: 0.04,
|
|
322
|
+
assertions: [
|
|
323
|
+
{ name: 'calledTool("get_weather")', severity: "gate", score: 0, passed: false },
|
|
324
|
+
],
|
|
325
|
+
}),
|
|
326
|
+
],
|
|
327
|
+
{ startedAt: "2026-07-09T10:00:00.000Z" },
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
return root;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
it("时间轴只列真实执行:复印件不占行,新失败带断言", async () => {
|
|
334
|
+
const root = await seedHistoryRoot();
|
|
335
|
+
const results = await openResults(root);
|
|
336
|
+
const exp = results.experiments.find((e) => e.id === "compare/bub")!;
|
|
337
|
+
const rows = evalHistory(exp, "weather/brooklyn");
|
|
338
|
+
// run2 里复印件被识别,真实执行只有:run1 的 passed + run2 的 failed(新 attempt)
|
|
339
|
+
expect(rows).toHaveLength(2);
|
|
340
|
+
expect(rows[0]).toMatchObject({ outcome: "failed", attempts: 1, costUSD: 0.04 });
|
|
341
|
+
expect(rows[0].failedAssertion).toBe('gate calledTool("get_weather")');
|
|
342
|
+
expect(rows[1]).toMatchObject({ outcome: "passed", attempts: 1, costUSD: 0.03 });
|
|
343
|
+
|
|
344
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { history: true });
|
|
345
|
+
expect(code).toBe(0);
|
|
346
|
+
expect(out).toContain("compare/bub · 2 runs · passed 1/2");
|
|
347
|
+
expect(out).toContain("2026-07-09T10-00");
|
|
348
|
+
expect(out).toContain("2026-07-07T09-00");
|
|
349
|
+
// 复印件那份 passed 判决只出现一行(run1),不在 run2 再占一行
|
|
350
|
+
expect(out.match(/✓ passed/g)).toHaveLength(1);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("裸 --history:每个 experiment 的 per-run 通过率序列", async () => {
|
|
354
|
+
const root = await seedHistoryRoot();
|
|
355
|
+
const { out, code } = await show(root, [], { history: true });
|
|
356
|
+
expect(code).toBe(0);
|
|
357
|
+
expect(out).toContain("compare/bub · 2 runs");
|
|
358
|
+
// 每个快照一行(run2 折叠含携带的 passed 复印件,任一轮通过 → passed)
|
|
359
|
+
expect(out.match(/1\/1 passed/g)).toHaveLength(2);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ───────────────────────── --report 装载与组合语义 ─────────────────────────
|
|
364
|
+
|
|
365
|
+
describe("--report 装载", () => {
|
|
366
|
+
/** 不经 niceeval 包也能造出合法报告:判别与双面组件都锚在 Symbol.for 上。 */
|
|
367
|
+
async function writeReportFile(dir: string): Promise<string> {
|
|
368
|
+
const path = join(dir, "report.mjs");
|
|
369
|
+
await writeFile(
|
|
370
|
+
path,
|
|
371
|
+
[
|
|
372
|
+
'const FACES = Symbol.for("niceeval.report.faces");',
|
|
373
|
+
"const Custom = () => null;",
|
|
374
|
+
"Custom[FACES] = {",
|
|
375
|
+
" web: () => null,",
|
|
376
|
+
" text: (props, ctx) => `CUSTOM ${props.evals} · drill ${ctx.attemptCommand(props.ref)}`,",
|
|
377
|
+
"};",
|
|
378
|
+
"export default {",
|
|
379
|
+
' [Symbol.for("niceeval.report.definition")]: true,',
|
|
380
|
+
" build: (ctx) => ({",
|
|
381
|
+
" type: Custom,",
|
|
382
|
+
" props: {",
|
|
383
|
+
" evals: ctx.selection.snapshots.flatMap((s) => s.evals.map((e) => e.id)).sort().join(\",\"),",
|
|
384
|
+
" ref: ctx.selection.snapshots[0].evals[0].attempts[0].ref,",
|
|
385
|
+
" },",
|
|
386
|
+
" }),",
|
|
387
|
+
"};",
|
|
388
|
+
"",
|
|
389
|
+
].join("\n"),
|
|
390
|
+
"utf-8",
|
|
391
|
+
);
|
|
392
|
+
return path;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
it("装载 + 注入选集 + attemptCommand 下钻命令", async () => {
|
|
396
|
+
const root = await seedComposedRoot();
|
|
397
|
+
const report = await writeReportFile(root);
|
|
398
|
+
const { out, code } = await show(root, [], { report });
|
|
399
|
+
expect(code).toBe(0);
|
|
400
|
+
expect(out).toContain("CUSTOM fixtures/button,weather/brooklyn");
|
|
401
|
+
expect(out).toContain("drill niceeval show");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("位置前缀对 --report 生效:收窄注入选集覆盖的 eval", async () => {
|
|
405
|
+
const root = await seedComposedRoot();
|
|
406
|
+
const report = await writeReportFile(root);
|
|
407
|
+
const { out, code } = await show(root, ["weather"], { report });
|
|
408
|
+
expect(code).toBe(0);
|
|
409
|
+
expect(out).toContain("CUSTOM weather/brooklyn ·");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("--experiment 让选集只留该实验", async () => {
|
|
413
|
+
const root = await seedComposedRoot();
|
|
414
|
+
await writeRun(
|
|
415
|
+
root,
|
|
416
|
+
"2026-07-09T11-00-00-000Z",
|
|
417
|
+
summaryOf(
|
|
418
|
+
[res({ id: "weather/brooklyn", experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T11:00:01.000Z" })],
|
|
419
|
+
{ agent: "codex", startedAt: "2026-07-09T11:00:00.000Z" },
|
|
420
|
+
),
|
|
421
|
+
);
|
|
422
|
+
const results = await openResults(root);
|
|
423
|
+
const selection = composeShowSelection(results, { experiment: "compare/codex" });
|
|
424
|
+
expect(selection.snapshots.map((s) => s.experimentId)).toEqual(["compare/codex"]);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("--history 与 --report 互斥:报错直说", async () => {
|
|
428
|
+
const root = await seedComposedRoot();
|
|
429
|
+
const { err, code } = await show(root, [], { history: true, report: "reports/x.tsx" });
|
|
430
|
+
expect(code).toBe(1);
|
|
431
|
+
expect(err).toContain("mutually exclusive");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("非法报告文件:默认导出不是 defineReport 产物", async () => {
|
|
435
|
+
const root = await seedComposedRoot();
|
|
436
|
+
const bad = join(root, "bad.mjs");
|
|
437
|
+
await writeFile(bad, "export default {};\n", "utf-8");
|
|
438
|
+
const { err, code } = await show(root, [], { report: bad });
|
|
439
|
+
expect(code).toBe(1);
|
|
440
|
+
expect(err).toContain("does not default-export a report");
|
|
441
|
+
expect(err).toContain("defineReport");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("报告文件不存在:直说路径与下一步", async () => {
|
|
445
|
+
const root = await seedComposedRoot();
|
|
446
|
+
const { err, code } = await show(root, [], { report: join(root, "missing.tsx") });
|
|
447
|
+
expect(code).toBe(1);
|
|
448
|
+
expect(err).toContain("Report file not found");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("证据切面 flag 出现时走证据室,不渲染报告槽", async () => {
|
|
452
|
+
const root = await seedComposedRoot();
|
|
453
|
+
const report = await writeReportFile(root);
|
|
454
|
+
const { out, code } = await show(root, ["fixtures/button"], { report, diff: true });
|
|
455
|
+
expect(code).toBe(0);
|
|
456
|
+
expect(out).not.toContain("CUSTOM");
|
|
457
|
+
expect(out).toContain("no diff recorded"); // fixture 没有 diff 工件:如实说缺
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ───────────────────────── 证据切面:--transcript ─────────────────────────
|
|
462
|
+
|
|
463
|
+
describe("--transcript", () => {
|
|
464
|
+
it("逐轮对话 + 截断标注(事件数 · 工具调用数 · 原始工件路径)", async () => {
|
|
465
|
+
const root = await makeRoot();
|
|
466
|
+
const writer = await createRunWriter(root, { producer: { name: "niceeval", version: "0.0.0" } });
|
|
467
|
+
const snap = writer.snapshot({
|
|
468
|
+
experiment: "compare/codex",
|
|
469
|
+
agent: "codex",
|
|
470
|
+
startedAt: "2026-07-09T10:00:00.000Z",
|
|
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
|
+
},
|
|
486
|
+
);
|
|
487
|
+
await writer.finish();
|
|
488
|
+
|
|
489
|
+
const { out, code } = await show(root, ["weather/brooklyn"], { transcript: true });
|
|
490
|
+
expect(code).toBe(0);
|
|
491
|
+
expect(out).toContain("attempt 3 · compare/codex · failed");
|
|
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)");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("多个 eval 匹配时证据切面报错:先收窄到单个 eval", async () => {
|
|
500
|
+
const root = await seedComposedRoot();
|
|
501
|
+
const { err, code } = await show(root, [], { transcript: true });
|
|
502
|
+
expect(code).toBe(1);
|
|
503
|
+
expect(err).toContain("Narrow to a single eval id");
|
|
504
|
+
});
|
|
505
|
+
});
|