niceeval 0.4.6 → 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/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
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
// niceeval/results
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// niceeval/results 的单测:临时目录里构造最小 summary.json / 工件 fixture,覆盖定稿契约
|
|
2
|
+
// (docs/results-lib.md):分层读取、懒加载回退、skipped 三种原因、latest() 三种警告、
|
|
3
|
+
// Selection.filter 修剪、dedupeAttempts 身份键、writer roundtrip、copySnapshots 补记,
|
|
4
|
+
// 以及 Artifacts 报告器(writer 薄壳)与直写时代逐字节等价的守护。
|
|
5
|
+
// 读取面 fixture 的目录名/工件路径手写(不 import 库的路径函数),让测试独立于实现充当格式基准。
|
|
4
6
|
|
|
5
7
|
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
-
import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
7
9
|
import { tmpdir } from "node:os";
|
|
8
10
|
import { join } from "node:path";
|
|
9
11
|
import {
|
|
10
12
|
RESULTS_FORMAT,
|
|
11
13
|
RESULTS_SCHEMA_VERSION,
|
|
12
|
-
|
|
14
|
+
copySnapshots,
|
|
15
|
+
createRunWriter,
|
|
13
16
|
dedupeAttempts,
|
|
14
|
-
latestPerExperiment,
|
|
15
17
|
openResults,
|
|
16
18
|
type EvalResult,
|
|
17
19
|
type RunSummary,
|
|
18
20
|
} from "./index.ts";
|
|
21
|
+
import { Artifacts } from "../runner/reporters/artifacts.ts";
|
|
19
22
|
|
|
20
23
|
// ───────────────────────── fixture 工具 ─────────────────────────
|
|
21
24
|
|
|
@@ -76,81 +79,196 @@ async function exists(path: string): Promise<boolean> {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
// ─────────────────────────
|
|
82
|
+
// ───────────────────────── 分层读取 ─────────────────────────
|
|
80
83
|
|
|
81
|
-
describe("openResults ·
|
|
82
|
-
it("一个 run 装两个 experiment
|
|
84
|
+
describe("openResults · 实验 → 快照 → eval → attempt 分层", () => {
|
|
85
|
+
it("一个 run 装两个 experiment:切成两个快照;实验按 id 字典序,快照最新在前,latest = snapshots[0]", async () => {
|
|
83
86
|
const root = await makeRoot();
|
|
84
|
-
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
87
|
+
const monday = await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
85
88
|
res({ id: "algebra/q1", agent: "bub", model: "gpt-5", experimentId: "compare/bub", startedAt: "2026-07-01T08:01:00.000Z" }),
|
|
86
|
-
res({ id: "algebra/
|
|
89
|
+
res({ id: "algebra/q1", agent: "bub", model: "gpt-5", experimentId: "compare/bub", attempt: 2, outcome: "failed" }),
|
|
90
|
+
res({ id: "algebra/q2", agent: "bub", model: "gpt-5", experimentId: "compare/bub" }),
|
|
87
91
|
res({ id: "algebra/q1", agent: "codex", model: "o3", experimentId: "compare/codex" }),
|
|
88
|
-
]));
|
|
92
|
+
], { startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
93
|
+
const tuesday = await writeRun(root, "2026-07-02T08-00-00-000Z", summaryOf([
|
|
94
|
+
res({ id: "algebra/q1", agent: "bub", model: "gpt-5", experimentId: "compare/bub" }),
|
|
95
|
+
], { startedAt: "2026-07-02T08:00:00.000Z" }));
|
|
89
96
|
|
|
90
97
|
const results = await openResults(root);
|
|
91
|
-
expect(results.runs).toHaveLength(1);
|
|
92
98
|
expect(results.skipped).toHaveLength(0);
|
|
93
|
-
expect(results.
|
|
94
|
-
expect(results.
|
|
95
|
-
|
|
96
|
-
const bub = results.
|
|
97
|
-
expect(bub.
|
|
98
|
-
expect(bub.
|
|
99
|
-
expect(bub.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
expect(
|
|
106
|
-
expect(
|
|
107
|
-
expect(
|
|
99
|
+
expect(results.runDirs.map((r) => r.dir)).toEqual([tuesday, monday]); // 新→旧
|
|
100
|
+
expect(results.experiments.map((e) => e.id)).toEqual(["compare/bub", "compare/codex"]); // 字典序
|
|
101
|
+
|
|
102
|
+
const bub = results.experiments[0];
|
|
103
|
+
expect(bub.snapshots.map((s) => s.startedAt)).toEqual(["2026-07-02T08:00:00.000Z", "2026-07-01T08:00:00.000Z"]);
|
|
104
|
+
expect(bub.latest).toBe(bub.snapshots[0]);
|
|
105
|
+
expect(bub.evalIds).toEqual(["algebra/q1", "algebra/q2"]); // 本地历史并集
|
|
106
|
+
|
|
107
|
+
// eval 分组:attempt 挂在题下面;attempts 平铺 = evals 逐题展开。
|
|
108
|
+
const mondayBub = bub.snapshots[1];
|
|
109
|
+
expect(mondayBub.agent).toBe("bub");
|
|
110
|
+
expect(mondayBub.model).toBe("gpt-5");
|
|
111
|
+
expect(mondayBub.producer).toEqual({ name: "niceeval", version: "0.3.0" });
|
|
112
|
+
expect(mondayBub.schemaVersion).toBe(RESULTS_SCHEMA_VERSION);
|
|
113
|
+
expect(mondayBub.runDir.dir).toBe(monday);
|
|
114
|
+
expect(mondayBub.evals.map((e) => e.id)).toEqual(["algebra/q1", "algebra/q2"]);
|
|
115
|
+
expect(mondayBub.evals[0].attempts).toHaveLength(2);
|
|
116
|
+
expect(mondayBub.attempts).toHaveLength(3);
|
|
117
|
+
|
|
118
|
+
// attempt 直达字段与证据引用。
|
|
119
|
+
const attempt = mondayBub.evals[0].attempts[0];
|
|
120
|
+
expect(attempt.evalId).toBe("algebra/q1");
|
|
121
|
+
expect(attempt.experimentId).toBe("compare/bub");
|
|
122
|
+
expect(attempt.ref).toEqual({ run: "2026-07-01T08-00-00-000Z", result: 0 });
|
|
123
|
+
expect(attempt.result.startedAt).toBe("2026-07-01T08:01:00.000Z");
|
|
108
124
|
});
|
|
109
125
|
|
|
110
|
-
it("
|
|
126
|
+
it("summary.snapshots 元数据:快照级 startedAt 覆盖顶层,knownEvalIds 并进 exp.evalIds", async () => {
|
|
111
127
|
const root = await makeRoot();
|
|
112
128
|
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
113
|
-
res({ id: "q1", agent: "
|
|
114
|
-
res({ id: "
|
|
115
|
-
]
|
|
129
|
+
res({ id: "q1", agent: "bub", experimentId: "mid/a" }),
|
|
130
|
+
res({ id: "q1", agent: "codex", experimentId: "mid/b" }),
|
|
131
|
+
], {
|
|
132
|
+
startedAt: "2026-07-01T08:00:00.000Z",
|
|
133
|
+
snapshots: {
|
|
134
|
+
"mid/b": { startedAt: "2026-06-20T08:00:00.000Z", knownEvalIds: ["q1", "q2", "q3"] },
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
116
137
|
|
|
117
138
|
const results = await openResults(root);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
expect(
|
|
121
|
-
expect(
|
|
122
|
-
expect(
|
|
139
|
+
const a = results.experiments.find((e) => e.id === "mid/a")!;
|
|
140
|
+
const b = results.experiments.find((e) => e.id === "mid/b")!;
|
|
141
|
+
expect(a.latest.startedAt).toBe("2026-07-01T08:00:00.000Z");
|
|
142
|
+
expect(b.latest.startedAt).toBe("2026-06-20T08:00:00.000Z");
|
|
143
|
+
expect(b.latest.knownEvalIds).toEqual(["q1", "q2", "q3"]);
|
|
144
|
+
expect(b.evalIds).toEqual(["q1", "q2", "q3"]); // 本地覆盖 ∪ 携带的 knownEvalIds
|
|
145
|
+
expect(a.evalIds).toEqual(["q1"]);
|
|
123
146
|
});
|
|
124
147
|
});
|
|
125
148
|
|
|
126
|
-
|
|
127
|
-
|
|
149
|
+
// ───────────────────────── 懒加载与回退 ─────────────────────────
|
|
150
|
+
|
|
151
|
+
describe("AttemptHandle · 懒加载", () => {
|
|
152
|
+
it("缺文件返回 null 不抛错;读过一次即记忆化;artifactsDir 优先、artifactBase 回退;原 run 清理后如实 null", async () => {
|
|
128
153
|
const root = await makeRoot();
|
|
129
|
-
|
|
154
|
+
// 原 run:携带条目的工件真身。
|
|
155
|
+
const oldRun = await writeRun(root, "2026-06-30T08-00-00-000Z", summaryOf([
|
|
156
|
+
res({ id: "q3", agent: "bub", model: "gpt-5", experimentId: "e", artifactsDir: "q3/bub/gpt-5/e/a1", hasEvents: true }),
|
|
157
|
+
], { startedAt: "2026-06-30T08:00:00.000Z" }));
|
|
158
|
+
await writeArtifact(oldRun, "q3/bub/gpt-5/e/a1", "events.json", [{ type: "message", text: "old" }]);
|
|
159
|
+
|
|
160
|
+
const runDir = await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
161
|
+
res({ id: "q1", agent: "bub", model: "gpt-5", experimentId: "e", artifactsDir: "q1/bub/gpt-5/e/a1", hasEvents: true }),
|
|
162
|
+
res({ id: "q2", agent: "bub", model: "gpt-5", experimentId: "e" }),
|
|
163
|
+
// --resume 携带条目:本 run 没有工件,artifactBase(相对结果根)指向原 run。
|
|
164
|
+
res({ id: "q3", agent: "bub", model: "gpt-5", experimentId: "e", artifactBase: "2026-06-30T08-00-00-000Z/q3/bub/gpt-5/e/a1", hasEvents: true }),
|
|
165
|
+
], { startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
166
|
+
const eventsPath = await writeArtifact(runDir, "q1/bub/gpt-5/e/a1", "events.json", [{ type: "message", text: "hi" }]);
|
|
167
|
+
|
|
168
|
+
const results = await openResults(root);
|
|
169
|
+
const snap = results.experiments[0].latest;
|
|
170
|
+
const [q1, q2, q3] = snap.evals.map((e) => e.attempts[0]);
|
|
171
|
+
|
|
172
|
+
const events = await q1.events();
|
|
173
|
+
expect(events).toHaveLength(1);
|
|
174
|
+
// summary 只有 hasEvents/hasTrace/hasSources 三个标记,o11y/diff 没有标记 —— 全靠方法语义吸收。
|
|
175
|
+
expect(await q1.trace()).toBeNull();
|
|
176
|
+
expect(await q1.o11y()).toBeNull();
|
|
177
|
+
expect(await q1.diff()).toBeNull();
|
|
178
|
+
expect(await q1.sources()).toBeNull();
|
|
179
|
+
|
|
180
|
+
// 记忆化:同一 handle 读一次缓存,文件删掉后再读仍返回同一份数据。
|
|
181
|
+
await rm(eventsPath);
|
|
182
|
+
expect(await q1.events()).toBe(events);
|
|
183
|
+
|
|
184
|
+
// 条目没有 artifactsDir 也没有 artifactBase:不猜路径,全部 null。
|
|
185
|
+
expect(await q2.events()).toBeNull();
|
|
186
|
+
|
|
187
|
+
// 携带条目经 artifactBase 回退读到原 run 的工件;ref 指条目所在的落盘(新 run)。
|
|
188
|
+
expect(await q3.events()).toEqual([{ type: "message", text: "old" }]);
|
|
189
|
+
expect(q3.ref.run).toBe("2026-07-01T08-00-00-000Z");
|
|
190
|
+
|
|
191
|
+
// 原 run 被清理:回退落空,如实返回 null(新句柄,不吃上面的记忆化)。
|
|
192
|
+
await rm(oldRun, { recursive: true });
|
|
193
|
+
const reopened = await openResults(root);
|
|
194
|
+
const q3Again = reopened.experiments[0].latest.evals.find((e) => e.id === "q3")!.attempts[0];
|
|
195
|
+
expect(await q3Again.events()).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ───────────────────────── skipped 三种原因 ─────────────────────────
|
|
200
|
+
|
|
201
|
+
describe("openResults · skipped", () => {
|
|
202
|
+
it("版本不匹配带 schemaVersion 与完整 producer;坏 JSON 记 malformed;无 summary 有工件记 incomplete;无关 JSON 静默;legacy 无信封按 1 读", async () => {
|
|
203
|
+
const root = await makeRoot();
|
|
204
|
+
const incompatible = await writeRun(root, "2026-07-03T08-00-00-000Z", {
|
|
205
|
+
format: RESULTS_FORMAT,
|
|
206
|
+
schemaVersion: RESULTS_SCHEMA_VERSION + 1,
|
|
207
|
+
producer: { name: "other-harness", version: "9.9.9" },
|
|
208
|
+
startedAt: "2026-07-03T08:00:00.000Z",
|
|
209
|
+
results: [],
|
|
210
|
+
});
|
|
211
|
+
await writeRun(root, "2026-07-04T08-00-00-000Z", "not json {");
|
|
212
|
+
await writeRun(root, "unrelated", { hello: 1 });
|
|
213
|
+
await writeRun(root, "alien-results", { results: [] }); // 只沾一个键:无关 JSON,不进 skipped
|
|
214
|
+
// crash 没收尾:有 attempt 工件、没有 summary.json。
|
|
215
|
+
const crashed = join(root, "2026-07-05T08-00-00-000Z");
|
|
216
|
+
await writeArtifact(crashed, "q1/bub/default/a1", "events.json", [{ type: "message" }]);
|
|
217
|
+
// 空目录:既无 summary 也无工件,静默忽略。
|
|
218
|
+
await mkdir(join(root, "2026-07-06T08-00-00-000Z"), { recursive: true });
|
|
219
|
+
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([res({ id: "q1", agent: "bub", experimentId: "e" })]));
|
|
220
|
+
// legacy:引入版本号之前的存量报告,没有 format 信封,按 schemaVersion 1 读。
|
|
221
|
+
const legacy = summaryOf([res({ id: "q1", agent: "bub", experimentId: "old" })], { startedAt: "2026-06-01T08:00:00.000Z" });
|
|
222
|
+
delete legacy.format;
|
|
223
|
+
delete legacy.schemaVersion;
|
|
224
|
+
delete legacy.producer;
|
|
225
|
+
await writeRun(root, "2026-06-01T08-00-00-000Z", legacy);
|
|
226
|
+
|
|
227
|
+
const results = await openResults(root);
|
|
228
|
+
expect(results.runDirs).toHaveLength(2);
|
|
229
|
+
expect(results.skipped).toHaveLength(3);
|
|
230
|
+
|
|
231
|
+
const versionSkip = results.skipped.find((s) => s.reason === "incompatible-version")!;
|
|
232
|
+
expect(versionSkip.dir).toBe(incompatible);
|
|
233
|
+
expect(versionSkip.schemaVersion).toBe(RESULTS_SCHEMA_VERSION + 1);
|
|
234
|
+
// 完整 producer:第三方 harness 的名字如实报出,消费方才能做对「要不要拼 npx 提示」的分支。
|
|
235
|
+
expect(versionSkip.producer).toEqual({ name: "other-harness", version: "9.9.9" });
|
|
236
|
+
|
|
237
|
+
expect(results.skipped.find((s) => s.reason === "malformed")!.detail).toBe("invalid JSON");
|
|
238
|
+
expect(results.skipped.find((s) => s.reason === "incomplete")!.dir).toBe(crashed);
|
|
239
|
+
|
|
240
|
+
const legacyExp = results.experiments.find((e) => e.id === "old")!;
|
|
241
|
+
expect(legacyExp.latest.schemaVersion).toBe(1);
|
|
242
|
+
expect(legacyExp.latest.producer).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ───────────────────────── latest() 选集与警告 ─────────────────────────
|
|
247
|
+
|
|
248
|
+
describe("results.latest() · Selection", () => {
|
|
249
|
+
it("每个实验取最新快照;experiments 前缀过滤同 CLI 语义(尾斜杠等价)", async () => {
|
|
250
|
+
const root = await makeRoot();
|
|
251
|
+
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
130
252
|
res({ id: "q1", agent: "bub", experimentId: "mid/a" }),
|
|
131
|
-
res({ id: "q2", agent: "bub", experimentId: "mid/a" }),
|
|
132
253
|
res({ id: "q1", agent: "codex", experimentId: "mid/b" }),
|
|
133
|
-
res({ id: "q2", agent: "codex", experimentId: "mid/b" }),
|
|
134
254
|
], { startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
135
255
|
const tuesday = await writeRun(root, "2026-07-02T08-00-00-000Z", summaryOf([
|
|
136
256
|
res({ id: "q1", agent: "codex", experimentId: "mid/b" }),
|
|
137
|
-
res({ id: "q2", agent: "codex", experimentId: "mid/b" }),
|
|
138
257
|
], { startedAt: "2026-07-02T08:00:00.000Z" }));
|
|
139
258
|
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
expect(
|
|
143
|
-
expect(snapshots.
|
|
144
|
-
|
|
145
|
-
expect(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
expect(
|
|
149
|
-
expect(
|
|
150
|
-
expect(latestPerExperiment(all.snapshots, { experiments: "other" }).snapshots).toHaveLength(0);
|
|
259
|
+
const results = await openResults(root);
|
|
260
|
+
const latest = results.latest();
|
|
261
|
+
expect(latest.snapshots.map((s) => s.experimentId)).toEqual(["mid/a", "mid/b"]);
|
|
262
|
+
expect(latest.snapshots[1].runDir.dir).toBe(tuesday);
|
|
263
|
+
|
|
264
|
+
expect(results.latest({ experiments: "mid/a" }).snapshots).toHaveLength(1);
|
|
265
|
+
expect(results.latest({ experiments: "mid/" }).snapshots).toHaveLength(2);
|
|
266
|
+
expect(results.latest({ experiments: ["mid/a", "mid/b"] }).snapshots).toHaveLength(2);
|
|
267
|
+
expect(results.latest({ experiments: "other" }).snapshots).toHaveLength(0);
|
|
268
|
+
expect(results.latest({ experiments: "mid/a" }).snapshots[0].experimentId).toBe("mid/a"); // 不误配 "mid/ab"
|
|
151
269
|
});
|
|
152
270
|
|
|
153
|
-
it("
|
|
271
|
+
it("partial-coverage:最新快照覆盖 < 已知并集;结构化字段 + 渲染好的英文 message", async () => {
|
|
154
272
|
const root = await makeRoot();
|
|
155
273
|
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
156
274
|
res({ id: "algebra/q1", agent: "bub", experimentId: "midterm/bub-gpt-5.4" }),
|
|
@@ -161,199 +279,485 @@ describe("latestPerExperiment", () => {
|
|
|
161
279
|
res({ id: "algebra/q1", agent: "bub", experimentId: "midterm/bub-gpt-5.4" }),
|
|
162
280
|
], { startedAt: "2026-07-05T08:00:00.000Z" }));
|
|
163
281
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
expect(
|
|
168
|
-
expect(
|
|
169
|
-
|
|
170
|
-
|
|
282
|
+
const latest = (await openResults(root)).latest();
|
|
283
|
+
expect(latest.snapshots).toHaveLength(1);
|
|
284
|
+
const partial = latest.warnings.find((w) => w.kind === "partial-coverage")!;
|
|
285
|
+
expect(partial).toMatchObject({ experimentId: "midterm/bub-gpt-5.4", covered: 1, total: 3 });
|
|
286
|
+
expect(partial.message).toBe(
|
|
287
|
+
"snapshot covers 1 of 3 evals seen in history; re-run `niceeval exp midterm/bub-gpt-5.4` for a full snapshot",
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("stale-snapshot:早于选集中最新落盘即触发(无阈值),message 带人话时距", async () => {
|
|
292
|
+
const root = await makeRoot();
|
|
293
|
+
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
294
|
+
res({ id: "q1", agent: "bub", experimentId: "mid/a" }),
|
|
295
|
+
], { startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
296
|
+
await writeRun(root, "2026-07-05T08-00-00-000Z", summaryOf([
|
|
297
|
+
res({ id: "q1", agent: "codex", experimentId: "mid/b" }),
|
|
298
|
+
], { startedAt: "2026-07-05T08:00:00.000Z" }));
|
|
299
|
+
|
|
300
|
+
const latest = (await openResults(root)).latest();
|
|
301
|
+
const stale = latest.warnings.filter((w) => w.kind === "stale-snapshot");
|
|
302
|
+
expect(stale).toHaveLength(1);
|
|
303
|
+
expect(stale[0]).toMatchObject({
|
|
304
|
+
experimentId: "mid/a",
|
|
305
|
+
startedAt: "2026-07-01T08:00:00.000Z",
|
|
306
|
+
latestStartedAt: "2026-07-05T08:00:00.000Z",
|
|
307
|
+
});
|
|
308
|
+
expect(stale[0].message).toContain("predates the latest run in this selection by 4 days");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("synthetic-experiment-id:落盘缺 experimentId 以 <agent>/<model> 合成键(无 model 用 default)", async () => {
|
|
312
|
+
const root = await makeRoot();
|
|
313
|
+
const runDir = await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
314
|
+
res({ id: "q1", agent: "codex" }),
|
|
315
|
+
]));
|
|
316
|
+
|
|
317
|
+
const results = await openResults(root);
|
|
318
|
+
expect(results.experiments[0].id).toBe("codex/default");
|
|
319
|
+
expect(results.experiments[0].latest.synthetic).toBe(true);
|
|
320
|
+
|
|
321
|
+
const latest = results.latest();
|
|
322
|
+
const synthetic = latest.warnings.find((w) => w.kind === "synthetic-experiment-id")!;
|
|
323
|
+
expect(synthetic).toMatchObject({ experimentId: "codex/default", runDir });
|
|
324
|
+
expect(synthetic.message).toContain("without experimentId");
|
|
325
|
+
// 合成键拼不出可执行的 niceeval exp 命令:partial 提示退化成中性说法(此处无 partial,仅验证不炸)。
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("Selection.filter 只删不换:快照删减,幸存实验的警告保留、其余丢弃", async () => {
|
|
329
|
+
const root = await makeRoot();
|
|
330
|
+
// 两个实验都制造 partial-coverage。
|
|
331
|
+
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
332
|
+
res({ id: "q1", agent: "bub", experimentId: "mid/a" }),
|
|
333
|
+
res({ id: "q2", agent: "bub", experimentId: "mid/a" }),
|
|
334
|
+
res({ id: "q1", agent: "codex", experimentId: "mid/b" }),
|
|
335
|
+
res({ id: "q2", agent: "codex", experimentId: "mid/b" }),
|
|
336
|
+
], { startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
337
|
+
await writeRun(root, "2026-07-02T08-00-00-000Z", summaryOf([
|
|
338
|
+
res({ id: "q1", agent: "bub", experimentId: "mid/a" }),
|
|
339
|
+
res({ id: "q1", agent: "codex", experimentId: "mid/b" }),
|
|
340
|
+
], { startedAt: "2026-07-02T08:00:00.000Z" }));
|
|
341
|
+
|
|
342
|
+
const latest = (await openResults(root)).latest();
|
|
343
|
+
expect(latest.warnings.filter((w) => w.kind === "partial-coverage")).toHaveLength(2);
|
|
344
|
+
|
|
345
|
+
const filtered = latest.filter((s) => s.experimentId !== "mid/b");
|
|
346
|
+
expect(filtered.snapshots.map((s) => s.experimentId)).toEqual(["mid/a"]);
|
|
347
|
+
expect(filtered.warnings.map((w) => w.experimentId)).toEqual(["mid/a"]);
|
|
348
|
+
// 原选集不被改动。
|
|
349
|
+
expect(latest.snapshots).toHaveLength(2);
|
|
350
|
+
expect(latest.warnings).toHaveLength(2);
|
|
171
351
|
});
|
|
172
352
|
});
|
|
173
353
|
|
|
354
|
+
// ───────────────────────── 身份键去重 ─────────────────────────
|
|
355
|
+
|
|
174
356
|
describe("dedupeAttempts", () => {
|
|
175
|
-
it("
|
|
357
|
+
it("按 (experimentId, evalId, attempt, startedAt) 去重,保留最新 run 目录的那份;缺 startedAt 不去重并出 missing-startedAt", async () => {
|
|
176
358
|
const root = await makeRoot();
|
|
177
|
-
|
|
178
|
-
const monday = await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
359
|
+
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
179
360
|
res({ id: "q1", agent: "bub", experimentId: "e", startedAt: "2026-07-01T08:01:00.000Z" }),
|
|
180
361
|
res({ id: "q2", agent: "bub", experimentId: "e", outcome: "failed", startedAt: "2026-07-01T08:02:00.000Z" }),
|
|
181
362
|
res({ id: "q3", agent: "bub", experimentId: "e" }),
|
|
182
363
|
], { startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
183
|
-
// 周二 --resume:q1 原样合入(身份键完全相同),q2 重跑出新 startedAt,q3 依旧没有 startedAt。
|
|
184
364
|
const tuesday = await writeRun(root, "2026-07-02T08-00-00-000Z", summaryOf([
|
|
185
|
-
res({ id: "q1", agent: "bub", experimentId: "e", startedAt: "2026-07-01T08:01:00.000Z" }),
|
|
186
|
-
res({ id: "q2", agent: "bub", experimentId: "e", startedAt: "2026-07-02T08:02:00.000Z" }),
|
|
365
|
+
res({ id: "q1", agent: "bub", experimentId: "e", startedAt: "2026-07-01T08:01:00.000Z" }), // resume 原样合入
|
|
366
|
+
res({ id: "q2", agent: "bub", experimentId: "e", startedAt: "2026-07-02T08:02:00.000Z" }), // 重跑,新 startedAt
|
|
187
367
|
res({ id: "q3", agent: "bub", experimentId: "e" }),
|
|
188
368
|
], { startedAt: "2026-07-02T08:00:00.000Z" }));
|
|
189
369
|
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
expect(
|
|
370
|
+
const results = await openResults(root);
|
|
371
|
+
const all = results.experiments[0].snapshots.flatMap((s) => s.attempts);
|
|
372
|
+
expect(all).toHaveLength(6);
|
|
193
373
|
|
|
194
|
-
const { attempts
|
|
195
|
-
// q1
|
|
196
|
-
expect(
|
|
197
|
-
const q1 =
|
|
374
|
+
const { attempts, warnings } = dedupeAttempts(all);
|
|
375
|
+
// q1 合并成一条(取新 run 让 ref 落在最新落盘);q2 两条(两次真实运行);q3 两条(缺 startedAt 不敢去重)。
|
|
376
|
+
expect(attempts).toHaveLength(5);
|
|
377
|
+
const q1 = attempts.filter((a) => a.evalId === "q1");
|
|
198
378
|
expect(q1).toHaveLength(1);
|
|
199
|
-
expect(q1[0].
|
|
200
|
-
expect(
|
|
201
|
-
expect(
|
|
379
|
+
expect(q1[0].runDir.dir).toBe(tuesday);
|
|
380
|
+
expect(attempts.filter((a) => a.evalId === "q2")).toHaveLength(2);
|
|
381
|
+
expect(attempts.filter((a) => a.evalId === "q3")).toHaveLength(2);
|
|
382
|
+
|
|
202
383
|
expect(warnings).toHaveLength(2);
|
|
203
|
-
expect(warnings[0]).
|
|
204
|
-
|
|
384
|
+
expect(warnings[0]).toMatchObject({ kind: "missing-startedAt", experimentId: "e", evalId: "q3" });
|
|
385
|
+
expect(warnings[0].message).toContain("has no startedAt");
|
|
205
386
|
});
|
|
206
387
|
});
|
|
207
388
|
|
|
208
|
-
|
|
209
|
-
|
|
389
|
+
// ───────────────────────── writer roundtrip ─────────────────────────
|
|
390
|
+
|
|
391
|
+
describe("createRunWriter", () => {
|
|
392
|
+
it("writeAttempt + snapshot 声明写出 → openResults 读回逐字段相等;工件懒加载;knownEvalIds 进分母", async () => {
|
|
210
393
|
const root = await makeRoot();
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
394
|
+
const writer = await createRunWriter(root, { producer: { name: "my-harness", version: "1.0.0" } });
|
|
395
|
+
expect(writer.dir.startsWith(root)).toBe(true);
|
|
396
|
+
|
|
397
|
+
const snapA = writer.snapshot({
|
|
398
|
+
experiment: "compare/a",
|
|
399
|
+
agent: "bub",
|
|
400
|
+
model: "gpt-5",
|
|
401
|
+
startedAt: "2026-07-01T08:00:00.000Z",
|
|
402
|
+
knownEvalIds: ["q1", "q2", "q3"],
|
|
217
403
|
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
404
|
+
const events = [{ type: "message", role: "assistant", text: "hi" }] as never[];
|
|
405
|
+
const o11y = { toolCalls: 2 } as never;
|
|
406
|
+
await snapA.writeAttempt(
|
|
407
|
+
{
|
|
408
|
+
id: "q1",
|
|
409
|
+
outcome: "passed",
|
|
410
|
+
attempt: 1,
|
|
411
|
+
durationMs: 100,
|
|
412
|
+
assertions: [],
|
|
413
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
414
|
+
estimatedCostUSD: 0.25,
|
|
415
|
+
},
|
|
416
|
+
{ events, o11y },
|
|
417
|
+
);
|
|
418
|
+
await snapA.writeAttempt({ id: "q2", outcome: "failed", attempt: 1, durationMs: 50, assertions: [] });
|
|
419
|
+
|
|
420
|
+
const snapB = writer.snapshot({ experiment: "compare/b", agent: "codex", startedAt: "2026-07-02T09:00:00.000Z" });
|
|
421
|
+
await snapB.writeAttempt(
|
|
422
|
+
{ id: "q1", outcome: "passed", attempt: 1, durationMs: 80, assertions: [] },
|
|
423
|
+
{ diff: { generatedFiles: { "a.txt": "1" }, deletedFiles: [] } },
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const summary = await writer.finish();
|
|
427
|
+
// summary 从已写 attempt 推导:计数永远和条目一致;版本元数据注入。
|
|
428
|
+
expect(summary.format).toBe(RESULTS_FORMAT);
|
|
429
|
+
expect(summary.schemaVersion).toBe(RESULTS_SCHEMA_VERSION);
|
|
430
|
+
expect(summary.producer).toEqual({ name: "my-harness", version: "1.0.0" });
|
|
431
|
+
expect(summary.passed).toBe(2);
|
|
432
|
+
expect(summary.failed).toBe(1);
|
|
433
|
+
expect(summary.startedAt).toBe("2026-07-01T08:00:00.000Z"); // 最早的快照时刻
|
|
434
|
+
expect(summary.usage).toEqual({ inputTokens: 10, outputTokens: 5 });
|
|
435
|
+
expect(summary.estimatedCostUSD).toBe(0.25);
|
|
229
436
|
|
|
230
437
|
const results = await openResults(root);
|
|
231
|
-
expect(results.
|
|
232
|
-
expect(results.
|
|
438
|
+
expect(results.skipped).toHaveLength(0);
|
|
439
|
+
expect(results.experiments.map((e) => e.id)).toEqual(["compare/a", "compare/b"]);
|
|
440
|
+
|
|
441
|
+
const a = results.experiments[0].latest;
|
|
442
|
+
expect(a.agent).toBe("bub");
|
|
443
|
+
expect(a.model).toBe("gpt-5");
|
|
444
|
+
expect(a.startedAt).toBe("2026-07-01T08:00:00.000Z");
|
|
445
|
+
expect(a.producer).toEqual({ name: "my-harness", version: "1.0.0" });
|
|
446
|
+
expect(a.schemaVersion).toBe(RESULTS_SCHEMA_VERSION);
|
|
447
|
+
expect(a.knownEvalIds).toEqual(["q1", "q2", "q3"]);
|
|
448
|
+
expect(results.experiments[0].evalIds).toEqual(["q1", "q2", "q3"]);
|
|
449
|
+
|
|
450
|
+
// 快照级字段注入进条目(agent/model/experimentId/startedAt),attempt 级字段原样读回。
|
|
451
|
+
const q1 = a.evals.find((e) => e.id === "q1")!.attempts[0];
|
|
452
|
+
expect(q1.experimentId).toBe("compare/a");
|
|
453
|
+
expect(q1.result).toMatchObject({
|
|
454
|
+
id: "q1",
|
|
455
|
+
agent: "bub",
|
|
456
|
+
model: "gpt-5",
|
|
457
|
+
experimentId: "compare/a",
|
|
458
|
+
startedAt: "2026-07-01T08:00:00.000Z",
|
|
459
|
+
outcome: "passed",
|
|
460
|
+
durationMs: 100,
|
|
461
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
462
|
+
estimatedCostUSD: 0.25,
|
|
463
|
+
hasEvents: true,
|
|
464
|
+
hasTrace: false,
|
|
465
|
+
hasSources: false,
|
|
466
|
+
});
|
|
467
|
+
expect(await q1.events()).toEqual(events);
|
|
468
|
+
expect(await q1.o11y()).toEqual(o11y);
|
|
469
|
+
expect(await q1.trace()).toBeNull();
|
|
470
|
+
expect(await q1.diff()).toBeNull();
|
|
233
471
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
expect(
|
|
237
|
-
expect(
|
|
472
|
+
// 第二个快照:自己的 startedAt(≠ 顶层)经快照元数据读回;diff 工件可达。
|
|
473
|
+
const b = results.experiments[1].latest;
|
|
474
|
+
expect(b.agent).toBe("codex");
|
|
475
|
+
expect(b.model).toBeUndefined();
|
|
476
|
+
expect(b.startedAt).toBe("2026-07-02T09:00:00.000Z");
|
|
477
|
+
expect(await b.attempts[0].diff()).toEqual({ generatedFiles: { "a.txt": "1" }, deletedFiles: [] });
|
|
238
478
|
|
|
239
|
-
|
|
240
|
-
|
|
479
|
+
// knownEvalIds 是残缺检测的分母:compare/a 只写了 2/3 → partial-coverage。
|
|
480
|
+
const partial = results.latest().warnings.find((w) => w.kind === "partial-coverage")!;
|
|
481
|
+
expect(partial).toMatchObject({ experimentId: "compare/a", covered: 2, total: 3 });
|
|
241
482
|
});
|
|
242
|
-
});
|
|
243
483
|
|
|
244
|
-
|
|
245
|
-
it("缺文件返回 null 不抛错;读过一次即缓存(文件删了也还在);没有 artifactsDir 时全部为 null", async () => {
|
|
484
|
+
it("没走到 finish() 的目录 = skipped(\"incomplete\"):有工件、无 summary,reader 不读半份落盘", async () => {
|
|
246
485
|
const root = await makeRoot();
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
486
|
+
const writer = await createRunWriter(root, { producer: { name: "my-harness" } });
|
|
487
|
+
const snap = writer.snapshot({ experiment: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z" });
|
|
488
|
+
await snap.writeAttempt(
|
|
489
|
+
{ id: "q1", outcome: "passed", attempt: 1, durationMs: 10, assertions: [] },
|
|
490
|
+
{ events: [{ type: "message" }] as never[] },
|
|
491
|
+
);
|
|
492
|
+
// crash:没有 finish()。
|
|
255
493
|
const results = await openResults(root);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
expect(q1.ref).toEqual({ run: "2026-07-01T08-00-00-000Z", result: 0 });
|
|
260
|
-
expect(q2.ref).toEqual({ run: "2026-07-01T08-00-00-000Z", result: 1 });
|
|
261
|
-
|
|
262
|
-
const events = await q1.events();
|
|
263
|
-
expect(events).toHaveLength(1);
|
|
264
|
-
// summary 只有 hasEvents/hasTrace/hasSources 三个标记,o11y/diff 没有标记 —— 全靠方法语义吸收。
|
|
265
|
-
expect(await q1.trace()).toBeNull();
|
|
266
|
-
expect(await q1.o11y()).toBeNull();
|
|
267
|
-
expect(await q1.diff()).toBeNull();
|
|
268
|
-
expect(await q1.sources()).toBeNull();
|
|
269
|
-
|
|
270
|
-
// 记忆化:同一 handle 读一次缓存,文件删掉后再读仍返回同一份数据。
|
|
271
|
-
await rm(eventsPath);
|
|
272
|
-
expect(await q1.events()).toBe(events);
|
|
494
|
+
expect(results.experiments).toHaveLength(0);
|
|
495
|
+
expect(results.skipped).toEqual([{ dir: writer.dir, reason: "incomplete" }]);
|
|
496
|
+
});
|
|
273
497
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
498
|
+
it("snapshot() 的 startedAt 必填(运行时也拦):身份键与去重以它为锚", async () => {
|
|
499
|
+
const root = await makeRoot();
|
|
500
|
+
const writer = await createRunWriter(root, { producer: { name: "x" } });
|
|
501
|
+
expect(() => writer.snapshot({ experiment: "e", agent: "a", startedAt: "" })).toThrow(/startedAt/);
|
|
277
502
|
});
|
|
278
503
|
});
|
|
279
504
|
|
|
280
|
-
|
|
281
|
-
|
|
505
|
+
// ───────────────────────── copySnapshots ─────────────────────────
|
|
506
|
+
|
|
507
|
+
describe("copySnapshots", () => {
|
|
508
|
+
it("按指定工件复制;summary 重建保留版本元数据;补记 knownEvalIds 让发布目录重算出同样的残缺警告", async () => {
|
|
282
509
|
const root = await makeRoot();
|
|
283
510
|
const monday = await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
284
|
-
res({ id: "q1", agent: "bub", model: "gpt-5", experimentId: "compare/bub", artifactsDir: "q1/bub/gpt-5/a1", hasEvents: true, hasTrace: true, startedAt: "2026-07-01T08:01:00.000Z"
|
|
285
|
-
res({ id: "
|
|
511
|
+
res({ id: "q1", agent: "bub", model: "gpt-5", experimentId: "compare/bub", artifactsDir: "q1/bub/gpt-5/compare_bub/a1", hasEvents: true, hasTrace: true, startedAt: "2026-07-01T08:01:00.000Z" }),
|
|
512
|
+
res({ id: "q2", agent: "bub", model: "gpt-5", experimentId: "compare/bub", startedAt: "2026-07-01T08:02:00.000Z" }),
|
|
513
|
+
res({ id: "q1", agent: "codex", model: "o3", experimentId: "compare/codex", artifactsDir: "q1/codex/o3/compare_codex/a1", hasEvents: true, outcome: "failed", startedAt: "2026-07-01T08:03:00.000Z" }),
|
|
286
514
|
], { startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
287
|
-
await writeArtifact(monday, "q1/bub/gpt-5/a1", "events.json", [{
|
|
288
|
-
await writeArtifact(monday, "q1/bub/gpt-5/a1", "trace.json", [{ name: "turn" }]);
|
|
289
|
-
await writeArtifact(monday, "q1/bub/gpt-5/a1", "diff.json", { generatedFiles: {}, deletedFiles: [] });
|
|
290
|
-
await writeArtifact(monday, "q1/codex/o3/a1", "events.json", [{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
515
|
+
await writeArtifact(monday, "q1/bub/gpt-5/compare_bub/a1", "events.json", [{ n: 1 }]);
|
|
516
|
+
await writeArtifact(monday, "q1/bub/gpt-5/compare_bub/a1", "trace.json", [{ name: "turn" }]);
|
|
517
|
+
await writeArtifact(monday, "q1/bub/gpt-5/compare_bub/a1", "diff.json", { generatedFiles: {}, deletedFiles: [] });
|
|
518
|
+
await writeArtifact(monday, "q1/codex/o3/compare_codex/a1", "events.json", [{ n: 2 }]);
|
|
519
|
+
// 周五只重跑了 compare/bub 的 q1:它的最新快照残缺。
|
|
520
|
+
const friday = await writeRun(root, "2026-07-05T08-00-00-000Z", summaryOf([
|
|
521
|
+
res({ id: "q1", agent: "bub", model: "gpt-5", experimentId: "compare/bub", artifactsDir: "q1/bub/gpt-5/compare_bub/a1", hasEvents: true, startedAt: "2026-07-05T08:01:00.000Z" }),
|
|
522
|
+
], { startedAt: "2026-07-05T08:00:00.000Z" }));
|
|
523
|
+
await writeArtifact(friday, "q1/bub/gpt-5/compare_bub/a1", "events.json", [{ n: 1 }, { n: 2 }]);
|
|
295
524
|
|
|
296
|
-
const
|
|
297
|
-
const picked = latestPerExperiment(all.snapshots, { experiments: "compare/" });
|
|
525
|
+
const results = await openResults(root);
|
|
298
526
|
const dest = join(await makeRoot(), "site/data/run");
|
|
299
|
-
const copied = await
|
|
527
|
+
const copied = await copySnapshots(results.latest(), dest, { artifacts: ["events", "sources"] });
|
|
300
528
|
|
|
301
529
|
expect(copied.warnings).toHaveLength(0);
|
|
302
530
|
expect(copied.summary.format).toBe(RESULTS_FORMAT);
|
|
303
531
|
expect(copied.summary.schemaVersion).toBe(RESULTS_SCHEMA_VERSION);
|
|
304
532
|
expect(copied.summary.producer?.version).toBe("0.3.0");
|
|
305
533
|
expect(copied.summary.results).toHaveLength(2);
|
|
306
|
-
|
|
307
|
-
expect(copied.summary.
|
|
308
|
-
|
|
309
|
-
const bubEntry = copied.summary.results.find((r) => r.experimentId === "compare/bub")!;
|
|
310
|
-
expect(bubEntry.startedAt).toBe("2026-07-02T08:01:00.000Z");
|
|
311
|
-
expect(bubEntry.hasEvents).toBe(true);
|
|
312
|
-
expect(bubEntry.hasTrace).toBe(false); // trace 没被选中,存在标记按目标目录重算
|
|
534
|
+
// 补记的覆盖事实:复制时刻该实验已知的 eval 并集。
|
|
535
|
+
expect(copied.summary.snapshots?.["compare/bub"].knownEvalIds).toEqual(["q1", "q2"]);
|
|
536
|
+
expect(copied.summary.snapshots?.["compare/codex"].knownEvalIds).toEqual(["q1"]);
|
|
313
537
|
|
|
314
|
-
//
|
|
315
|
-
// 目标目录带 experiment 段(与 writer 的 attemptDir 同规则),源 fixture 的旧式路径靠 artifactsDir 定位
|
|
538
|
+
// 磁盘:只有选中的工件种类被复制;存在标记按目标目录重算。
|
|
316
539
|
expect(await exists(join(dest, "q1/bub/gpt-5/compare_bub/a1/events.json"))).toBe(true);
|
|
317
540
|
expect(await exists(join(dest, "q1/bub/gpt-5/compare_bub/a1/trace.json"))).toBe(false);
|
|
318
541
|
expect(await exists(join(dest, "q1/bub/gpt-5/compare_bub/a1/diff.json"))).toBe(false);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
542
|
+
const bubEntry = copied.summary.results.find((r) => r.experimentId === "compare/bub")!;
|
|
543
|
+
expect(bubEntry.startedAt).toBe("2026-07-05T08:01:00.000Z"); // 最新快照的那份
|
|
544
|
+
expect(bubEntry.hasEvents).toBe(true);
|
|
545
|
+
expect(bubEntry.hasTrace).toBe(false);
|
|
546
|
+
|
|
547
|
+
// 发布目录上重新 openResults().latest():残缺警告被同一套机制重新算出来,不靠发布者转述。
|
|
548
|
+
const republished = await openResults(dest);
|
|
549
|
+
expect(republished.experiments.find((e) => e.id === "compare/bub")!.evalIds).toEqual(["q1", "q2"]);
|
|
550
|
+
const partial = republished.latest().warnings.find((w) => w.kind === "partial-coverage")!;
|
|
551
|
+
expect(partial).toMatchObject({ experimentId: "compare/bub", covered: 1, total: 2 });
|
|
552
|
+
// 快照各自的 startedAt 也随行(compare/bub 周五、compare/codex 周一)。
|
|
553
|
+
expect(republished.experiments.find((e) => e.id === "compare/bub")!.latest.startedAt).toBe("2026-07-05T08:00:00.000Z");
|
|
554
|
+
expect(republished.experiments.find((e) => e.id === "compare/codex")!.latest.startedAt).toBe("2026-07-01T08:00:00.000Z");
|
|
555
|
+
// 工件懒加载在发布目录同样成立。
|
|
556
|
+
const bubAttempt = republished.experiments.find((e) => e.id === "compare/bub")!.latest.attempts[0];
|
|
326
557
|
expect(await bubAttempt.events()).toHaveLength(2);
|
|
327
558
|
expect(await bubAttempt.trace()).toBeNull();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("目标目录非空即报错(不静默覆盖、不合并);artifacts 非法值报错;同实验重复快照落同一目录出 warning", async () => {
|
|
562
|
+
const root = await makeRoot();
|
|
563
|
+
await writeRun(root, "2026-07-01T08-00-00-000Z", summaryOf([
|
|
564
|
+
res({ id: "q1", agent: "bub", model: "gpt-5", experimentId: "e", artifactsDir: "q1/bub/gpt-5/e/a1", startedAt: "2026-07-01T08:01:00.000Z" }),
|
|
565
|
+
], { startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
566
|
+
await writeRun(root, "2026-07-02T08-00-00-000Z", summaryOf([
|
|
567
|
+
res({ id: "q1", agent: "bub", model: "gpt-5", experimentId: "e", artifactsDir: "q1/bub/gpt-5/e/a1", startedAt: "2026-07-01T08:01:00.000Z" }),
|
|
568
|
+
], { startedAt: "2026-07-02T08:00:00.000Z" }));
|
|
569
|
+
const results = await openResults(root);
|
|
570
|
+
|
|
571
|
+
const occupied = await makeRoot();
|
|
572
|
+
await writeFile(join(occupied, "existing.txt"), "x", "utf-8");
|
|
573
|
+
await expect(copySnapshots(results.latest(), occupied)).rejects.toThrow(/not empty/);
|
|
328
574
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
575
|
+
await expect(
|
|
576
|
+
copySnapshots(results.latest(), join(await makeRoot(), "out"), { artifacts: ["evnets" as never] }),
|
|
577
|
+
).rejects.toThrow(/Unknown artifact kind/);
|
|
578
|
+
|
|
579
|
+
await expect(copySnapshots([], join(await makeRoot(), "out"))).rejects.toThrow(/no snapshots/);
|
|
580
|
+
|
|
581
|
+
// 同一 experiment 的两个快照(未走 latest 的手工数组):同键 attempt 落同一目录 → 保留最新 + warning。
|
|
582
|
+
const both = results.experiments[0].snapshots;
|
|
583
|
+
expect(both).toHaveLength(2);
|
|
332
584
|
const dest2 = join(await makeRoot(), "run2");
|
|
333
|
-
const collided = await
|
|
585
|
+
const collided = await copySnapshots(both, dest2);
|
|
334
586
|
expect(collided.warnings).toHaveLength(1);
|
|
335
|
-
expect(collided.warnings[0]).toMatch(/multiple attempts map to
|
|
587
|
+
expect(collided.warnings[0]).toMatch(/multiple attempts map to/);
|
|
336
588
|
expect(collided.summary.results).toHaveLength(1);
|
|
337
|
-
expect(collided.summary.results[0].startedAt).toBe("2026-07-02T08:01:00.000Z");
|
|
338
589
|
});
|
|
590
|
+
});
|
|
339
591
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
592
|
+
// ───────────────────────── Artifacts 报告器 = writer 薄壳 ─────────────────────────
|
|
593
|
+
|
|
594
|
+
describe("Artifacts reporter(writer 薄壳)", () => {
|
|
595
|
+
it("落盘行为与 runner 直写时代逐字节等价:summary.json 键序/瘦身/携带条目原样,工件文件按需紧凑写", async () => {
|
|
343
596
|
const root = await makeRoot();
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
597
|
+
const rep = Artifacts(root);
|
|
598
|
+
await rep.onRunStart?.([], {} as never);
|
|
599
|
+
|
|
600
|
+
const fresh: EvalResult = {
|
|
601
|
+
id: "algebra/q1",
|
|
602
|
+
experimentId: "compare/bub",
|
|
603
|
+
experiment: { id: "compare/bub", params: { style: "concise" } },
|
|
604
|
+
agent: "bub",
|
|
605
|
+
model: "gpt-5.4",
|
|
606
|
+
outcome: "passed",
|
|
607
|
+
fingerprint: "abc",
|
|
608
|
+
attempt: 1,
|
|
609
|
+
startedAt: "2026-07-01T08:01:00.000Z",
|
|
610
|
+
durationMs: 1234,
|
|
611
|
+
assertions: [{ kind: "contains", ok: true } as never],
|
|
612
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
613
|
+
estimatedCostUSD: 0.5,
|
|
614
|
+
events: [{ type: "message", role: "assistant", text: "hi" } as never],
|
|
615
|
+
sources: [{ path: "evals/a.ts", content: "x" }],
|
|
616
|
+
trace: [{ name: "turn", kind: "turn" } as never],
|
|
617
|
+
o11y: { toolCalls: 2 } as never,
|
|
618
|
+
diff: { generatedFiles: { "a.txt": "1" }, deletedFiles: [] },
|
|
619
|
+
rawTranscript: "raw",
|
|
620
|
+
};
|
|
621
|
+
const noArtifacts: EvalResult = {
|
|
622
|
+
id: "algebra/q2",
|
|
623
|
+
agent: "bub",
|
|
624
|
+
outcome: "failed",
|
|
625
|
+
attempt: 1,
|
|
626
|
+
durationMs: 10,
|
|
627
|
+
assertions: [],
|
|
628
|
+
events: [],
|
|
629
|
+
};
|
|
630
|
+
// --resume 携带条目:artifactBase 指向原 run,has* 真值原样携带,不得重算或编造 artifactsDir。
|
|
631
|
+
const carried: EvalResult = {
|
|
632
|
+
id: "algebra/q3",
|
|
633
|
+
experimentId: "compare/bub",
|
|
634
|
+
agent: "bub",
|
|
635
|
+
model: "gpt-5.4",
|
|
636
|
+
outcome: "passed",
|
|
637
|
+
attempt: 1,
|
|
638
|
+
startedAt: "2026-06-30T08:01:00.000Z",
|
|
639
|
+
durationMs: 99,
|
|
640
|
+
assertions: [],
|
|
641
|
+
artifactBase: "2026-06-30T08-00-00-000Z/algebra/q3/bub/gpt-5.4/compare_bub/a1",
|
|
642
|
+
hasEvents: true,
|
|
643
|
+
hasTrace: false,
|
|
644
|
+
hasSources: true,
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
await rep.onEvalComplete?.(fresh);
|
|
648
|
+
await rep.onEvalComplete?.(noArtifacts);
|
|
649
|
+
await rep.onRunComplete?.({
|
|
650
|
+
name: "demo",
|
|
651
|
+
agent: "bub",
|
|
652
|
+
startedAt: "2026-07-01T08:00:00.000Z",
|
|
653
|
+
completedAt: "2026-07-01T08:10:00.000Z",
|
|
654
|
+
passed: 2,
|
|
655
|
+
failed: 1,
|
|
656
|
+
skipped: 0,
|
|
657
|
+
errored: 0,
|
|
658
|
+
durationMs: 600000,
|
|
659
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
660
|
+
estimatedCostUSD: 0.5,
|
|
661
|
+
results: [carried, fresh, noArtifacts],
|
|
662
|
+
});
|
|
350
663
|
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
664
|
+
const dir = rep.outputDir();
|
|
665
|
+
const version = (JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf-8")) as { version: string }).version;
|
|
666
|
+
|
|
667
|
+
// 基线由改造前的实现捕获(runner 直写时代的真实输出),薄壳必须逐字节还原它。
|
|
668
|
+
const expected = `{
|
|
669
|
+
"format": "niceeval.results",
|
|
670
|
+
"schemaVersion": ${RESULTS_SCHEMA_VERSION},
|
|
671
|
+
"producer": {
|
|
672
|
+
"name": "niceeval",
|
|
673
|
+
"version": "${version}"
|
|
674
|
+
},
|
|
675
|
+
"name": "demo",
|
|
676
|
+
"agent": "bub",
|
|
677
|
+
"startedAt": "2026-07-01T08:00:00.000Z",
|
|
678
|
+
"completedAt": "2026-07-01T08:10:00.000Z",
|
|
679
|
+
"passed": 2,
|
|
680
|
+
"failed": 1,
|
|
681
|
+
"skipped": 0,
|
|
682
|
+
"errored": 0,
|
|
683
|
+
"durationMs": 600000,
|
|
684
|
+
"usage": {
|
|
685
|
+
"inputTokens": 10,
|
|
686
|
+
"outputTokens": 5
|
|
687
|
+
},
|
|
688
|
+
"estimatedCostUSD": 0.5,
|
|
689
|
+
"results": [
|
|
690
|
+
{
|
|
691
|
+
"id": "algebra/q3",
|
|
692
|
+
"experimentId": "compare/bub",
|
|
693
|
+
"agent": "bub",
|
|
694
|
+
"model": "gpt-5.4",
|
|
695
|
+
"outcome": "passed",
|
|
696
|
+
"attempt": 1,
|
|
697
|
+
"startedAt": "2026-06-30T08:01:00.000Z",
|
|
698
|
+
"durationMs": 99,
|
|
699
|
+
"assertions": [],
|
|
700
|
+
"artifactBase": "2026-06-30T08-00-00-000Z/algebra/q3/bub/gpt-5.4/compare_bub/a1",
|
|
701
|
+
"hasEvents": true,
|
|
702
|
+
"hasTrace": false,
|
|
703
|
+
"hasSources": true
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"id": "algebra/q1",
|
|
707
|
+
"experimentId": "compare/bub",
|
|
708
|
+
"experiment": {
|
|
709
|
+
"id": "compare/bub",
|
|
710
|
+
"params": {
|
|
711
|
+
"style": "concise"
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
"agent": "bub",
|
|
715
|
+
"model": "gpt-5.4",
|
|
716
|
+
"outcome": "passed",
|
|
717
|
+
"fingerprint": "abc",
|
|
718
|
+
"attempt": 1,
|
|
719
|
+
"startedAt": "2026-07-01T08:01:00.000Z",
|
|
720
|
+
"durationMs": 1234,
|
|
721
|
+
"assertions": [
|
|
722
|
+
{
|
|
723
|
+
"kind": "contains",
|
|
724
|
+
"ok": true
|
|
725
|
+
}
|
|
726
|
+
],
|
|
727
|
+
"usage": {
|
|
728
|
+
"inputTokens": 10,
|
|
729
|
+
"outputTokens": 5
|
|
730
|
+
},
|
|
731
|
+
"estimatedCostUSD": 0.5,
|
|
732
|
+
"artifactsDir": "algebra/q1/bub/gpt-5.4/compare_bub/a1",
|
|
733
|
+
"hasTrace": true,
|
|
734
|
+
"hasEvents": true,
|
|
735
|
+
"hasSources": true
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
"id": "algebra/q2",
|
|
739
|
+
"agent": "bub",
|
|
740
|
+
"outcome": "failed",
|
|
741
|
+
"attempt": 1,
|
|
742
|
+
"durationMs": 10,
|
|
743
|
+
"assertions": [],
|
|
744
|
+
"artifactsDir": "algebra/q2/bub/default/a1",
|
|
745
|
+
"hasTrace": false,
|
|
746
|
+
"hasEvents": false,
|
|
747
|
+
"hasSources": false
|
|
748
|
+
}
|
|
749
|
+
],
|
|
750
|
+
"outputDir": "${dir}"
|
|
751
|
+
}`;
|
|
752
|
+
expect(await readFile(join(dir, "summary.json"), "utf-8")).toBe(expected);
|
|
753
|
+
|
|
754
|
+
// 工件:紧凑 JSON,按需生成(q2 全空不落文件;q1 五类都在)。
|
|
755
|
+
const q1Dir = join(dir, "algebra/q1/bub/gpt-5.4/compare_bub/a1");
|
|
756
|
+
expect(await readFile(join(q1Dir, "events.json"), "utf-8")).toBe('[{"type":"message","role":"assistant","text":"hi"}]');
|
|
757
|
+
expect(await readFile(join(q1Dir, "sources.json"), "utf-8")).toBe('[{"path":"evals/a.ts","content":"x"}]');
|
|
758
|
+
expect(await readFile(join(q1Dir, "trace.json"), "utf-8")).toBe('[{"name":"turn","kind":"turn"}]');
|
|
759
|
+
expect(await readFile(join(q1Dir, "o11y.json"), "utf-8")).toBe('{"toolCalls":2}');
|
|
760
|
+
expect(await readFile(join(q1Dir, "diff.json"), "utf-8")).toBe('{"generatedFiles":{"a.txt":"1"},"deletedFiles":[]}');
|
|
761
|
+
expect(await exists(join(dir, "algebra/q2/bub/default/a1/events.json"))).toBe(false);
|
|
358
762
|
});
|
|
359
763
|
});
|