niceeval 0.6.2 → 0.7.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 +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +2 -2
- package/dist/i18n/zh-CN.d.ts +3 -3
- package/dist/report/aggregate.d.ts +32 -26
- package/dist/report/aggregate.js +157 -76
- package/dist/report/built-in/index.d.ts +2 -0
- package/dist/report/built-in/index.js +8 -0
- package/dist/report/components.d.ts +91 -164
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +86 -73
- package/dist/report/compute.js +592 -432
- package/dist/report/flag.d.ts +28 -17
- package/dist/report/flag.js +86 -16
- package/dist/report/format.d.ts +11 -11
- package/dist/report/format.js +17 -15
- package/dist/report/index.d.ts +16 -17
- package/dist/report/index.js +20 -22
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +49 -34
- package/dist/report/locale.js +106 -58
- package/dist/report/metrics.d.ts +10 -3
- package/dist/report/metrics.js +46 -12
- package/dist/report/primitives.d.ts +42 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +10 -8
- package/dist/report/react/AttemptList.js +18 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +3 -3
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +4 -2
- package/dist/report/react/ExperimentComparison.js +5 -4
- package/dist/report/react/ExperimentList.d.ts +3 -3
- package/dist/report/react/ExperimentList.js +16 -15
- package/dist/report/react/MetricBars.js +5 -4
- package/dist/report/react/MetricLine.js +12 -5
- package/dist/report/react/MetricMatrix.js +1 -1
- package/dist/report/react/MetricScatter.js +54 -17
- package/dist/report/react/MetricTable.js +2 -12
- package/dist/report/react/ScopeSummary.d.ts +10 -0
- package/dist/report/react/ScopeSummary.js +17 -0
- package/dist/report/react/Scoreboard.js +6 -6
- package/dist/report/react/cell.js +2 -2
- package/dist/report/react/fixtures.d.ts +5 -9
- package/dist/report/react/fixtures.js +105 -149
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -20
- package/dist/report/report.js +261 -34
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +225 -157
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +2 -2
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +245 -300
- package/dist/report/types.js +4 -3
- package/dist/report/web.d.ts +21 -5
- package/dist/report/web.js +42 -16
- package/dist/results/select.d.ts +38 -16
- package/dist/results/select.js +73 -25
- package/dist/results/types.d.ts +38 -14
- package/dist/shared/aggregate.d.ts +3 -2
- package/dist/shared/aggregate.js +5 -4
- package/docs-site/zh/README.md +44 -0
- package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
- package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
- package/docs-site/zh/examples/index.mdx +50 -0
- package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
- package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
- package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
- package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
- package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
- package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
- package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
- package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
- package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
- package/docs-site/zh/index.mdx +23 -25
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +5 -5
- package/docs-site/zh/reference/capabilities.mdx +6 -6
- package/docs-site/zh/reference/cli.mdx +9 -7
- package/docs-site/zh/reference/define-agent.mdx +1 -1
- package/docs-site/zh/reference/events.mdx +3 -3
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
- package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
- package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
- package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
- package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +6 -2
- package/src/agents/index.ts +2 -2
- package/src/agents/openai-compat.ts +1 -1
- package/src/agents/streaming.ts +2 -2
- package/src/agents/types.ts +3 -3
- package/src/cli.ts +42 -23
- package/src/context/context.ts +1 -1
- package/src/context/session.test.ts +1 -1
- package/src/context/session.ts +1 -1
- package/src/i18n/en.ts +18 -16
- package/src/i18n/zh-CN.ts +16 -15
- package/src/report/aggregate.ts +175 -87
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -285
- package/src/report/compute.ts +717 -515
- package/src/report/dual-render.test.tsx +738 -1148
- package/src/report/flag.ts +97 -33
- package/src/report/format.ts +18 -22
- package/src/report/index.ts +113 -58
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +120 -69
- package/src/report/metrics.ts +42 -12
- package/src/report/primitives.tsx +190 -45
- package/src/report/react/AttemptList.tsx +32 -20
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +12 -7
- package/src/report/react/ExperimentList.tsx +38 -26
- package/src/report/react/MetricBars.tsx +5 -4
- package/src/report/react/MetricLine.tsx +13 -8
- package/src/report/react/MetricMatrix.tsx +2 -2
- package/src/report/react/MetricScatter.tsx +74 -20
- package/src/report/react/MetricTable.tsx +4 -76
- package/src/report/react/ScopeSummary.tsx +86 -0
- package/src/report/react/Scoreboard.tsx +28 -10
- package/src/report/react/cell.tsx +2 -2
- package/src/report/react/enhance.js +57 -5
- package/src/report/react/fixtures.ts +109 -156
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +139 -104
- package/src/report/react/styles.css +181 -91
- package/src/report/report.test.ts +761 -1031
- package/src/report/report.ts +425 -47
- package/src/report/text/faces.ts +257 -164
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +2 -2
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +257 -287
- package/src/report/web.ts +63 -20
- package/src/results/attempt-evidence.test.ts +4 -4
- package/src/results/attempt-evidence.ts +5 -5
- package/src/results/copy.ts +6 -6
- package/src/results/host-equivalence.test.ts +26 -14
- package/src/results/index.ts +10 -4
- package/src/results/open.ts +8 -4
- package/src/results/results.test.ts +4 -3
- package/src/results/select.ts +104 -34
- package/src/results/types.ts +36 -14
- package/src/runner/feedback/human.test.ts +1 -1
- package/src/runner/run.ts +1 -1
- package/src/sandbox/cli-commands.ts +2 -2
- package/src/scoring/judge.test.ts +1 -1
- package/src/shared/aggregate.ts +5 -4
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +107 -56
- package/src/show/render.ts +43 -27
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +86 -36
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +324 -63
- package/src/view/app/components/CodeView.tsx +10 -4
- package/src/view/app/i18n.ts +31 -17
- package/src/view/app/main.tsx +13 -8
- package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
- package/src/view/app/types.ts +4 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +14 -14
- package/src/view/data.test.ts +9 -3
- package/src/view/data.ts +145 -49
- package/src/view/index.ts +48 -44
- package/src/view/server.ts +35 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +224 -0
- package/src/view/view-report.test.ts +161 -57
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
- package/dist/report/built-ins/experiment-comparison.js +0 -119
- package/dist/report/built-ins/index.d.ts +0 -2
- package/dist/report/built-ins/index.js +0 -2
- package/dist/report/react/GroupSummary.d.ts +0 -8
- package/dist/report/react/GroupSummary.js +0 -8
- package/dist/report/react/RunOverview.d.ts +0 -8
- package/dist/report/react/RunOverview.js +0 -12
- package/docs-site/zh/example/ai-agent-application.mdx +0 -152
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
- package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
- package/docs-site/zh/example/showcase.mdx +0 -39
- package/src/report/built-in-user-parity.test.tsx +0 -597
- package/src/report/built-ins/experiment-comparison.tsx +0 -179
- package/src/report/built-ins/index.ts +0 -7
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
- /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
|
@@ -1,1256 +1,846 @@
|
|
|
1
1
|
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// defineReport + renderReportToText / renderReportToStaticHtml 两个宿主入口。
|
|
2
|
+
// 管线与双面渲染的单元测试:resolve(spec/data 双形态、记忆化、组合组件、同层并行保序、
|
|
3
|
+
// 非法节点拒绝)、validate(裸字符串、单面组件、Tabs 配对)、装载规范化(defineReport 三种
|
|
4
|
+
// 写法、content/pages 互斥、外壳嵌套、page id、标题回退)、text/web 双面同源、
|
|
5
|
+
// Table 与文本排版原语、FailureList 等价、内建报告等价。
|
|
7
6
|
|
|
8
7
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
9
8
|
import { describe, expect, it } from "vitest";
|
|
10
9
|
|
|
11
|
-
import type { EvalResult } from "../types.ts";
|
|
12
|
-
import type { Results,
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
10
|
+
import type { EvalResult, Verdict } from "../types.ts";
|
|
11
|
+
import type { AttemptHandle, Results, Scope, ScopeWarning, Snapshot } from "../results/index.ts";
|
|
12
|
+
import { makeScope } from "../results/select.ts";
|
|
13
|
+
import {
|
|
14
|
+
createTextContext,
|
|
15
|
+
defineComponent,
|
|
16
|
+
renderNodeToText,
|
|
17
|
+
resolveReportTree,
|
|
18
|
+
validateReportTree,
|
|
19
|
+
ResolveMemo,
|
|
20
|
+
type ReportNode,
|
|
21
|
+
} from "./tree.ts";
|
|
22
|
+
import {
|
|
23
|
+
buildReportMeta,
|
|
24
|
+
defineReport,
|
|
25
|
+
FALLBACK_REPORT_TITLE,
|
|
26
|
+
pickReportPage,
|
|
27
|
+
renderReportToText,
|
|
28
|
+
ReportPageNotFoundError,
|
|
29
|
+
resolveReportTitle,
|
|
30
|
+
} from "./report.ts";
|
|
31
|
+
import { renderReportToStaticHtml } from "./web.ts";
|
|
16
32
|
import {
|
|
17
33
|
AttemptList,
|
|
18
|
-
|
|
19
|
-
DeltaTable,
|
|
20
|
-
EvalList,
|
|
34
|
+
ExperimentComparison,
|
|
21
35
|
ExperimentList,
|
|
22
|
-
|
|
36
|
+
FailureList,
|
|
23
37
|
MetricBars,
|
|
24
|
-
MetricLine,
|
|
25
38
|
MetricMatrix,
|
|
26
39
|
MetricScatter,
|
|
27
40
|
MetricTable,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
evalListItems,
|
|
53
|
-
experimentListItems,
|
|
54
|
-
groupSummaryData,
|
|
55
|
-
lineData,
|
|
56
|
-
matrixData,
|
|
57
|
-
overviewData,
|
|
58
|
-
scatterData,
|
|
59
|
-
scoreboardData,
|
|
60
|
-
tableData,
|
|
61
|
-
} from "./react/fixtures.ts";
|
|
62
|
-
|
|
63
|
-
const ctx = createTextContext({ width: 80 });
|
|
64
|
-
const text = (node: Parameters<typeof renderNodeToText>[0]) => renderNodeToText(node, ctx);
|
|
65
|
-
|
|
66
|
-
// ───────────────────────── 每组件双面一致 ─────────────────────────
|
|
67
|
-
|
|
68
|
-
describe("RunOverview 双面", () => {
|
|
69
|
-
const html = renderToStaticMarkup(<RunOverview data={overviewData} />);
|
|
70
|
-
const term = text(<RunOverview data={overviewData} />);
|
|
71
|
-
|
|
72
|
-
it("text 面形态:头行(含通过率)+ 判定行 + 警告行", () => {
|
|
73
|
-
expect(term).toMatchInlineSnapshot(`
|
|
74
|
-
"2 experiments · 12 evals · 48 attempts · End-to-end pass rate 70% 46/48 · composed from 2 runs · latest 2026-07-01T11:30:00Z
|
|
75
|
-
passed 36 · failed 8 · errored 2 · skipped 2 · no data · 4m 21s
|
|
76
|
-
! snapshot covers 9 of 12 evals seen in history; re-run \`niceeval exp compare/bub\` for a full snapshot"
|
|
77
|
-
`);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("两面同口径:通过率显示同一个 MetricCell.display(70%,不是 36/46 现算的 78%);成本全缺都是 no data 不编 $0;警告都在", () => {
|
|
81
|
-
for (const face of [html, term]) {
|
|
82
|
-
expect(face).toContain("70%");
|
|
83
|
-
expect(face).not.toContain("78%");
|
|
84
|
-
expect(face).toContain("46/48"); // samples < total:两级聚合的覆盖率角标,两面一致
|
|
85
|
-
expect(face).toContain("no data");
|
|
86
|
-
expect(face).not.toContain("$0");
|
|
87
|
-
expect(face).toContain("snapshot covers 9 of 12 evals");
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// fixtures.overviewData 是手工摆好的终值,只验证渲染面「原样显示 MetricCell,不重算」;
|
|
93
|
-
// 下面这组用真实 Selection 走一遍 RunOverview.data(= compute.ts 的 overviewData()),
|
|
94
|
-
// 专门验证 totals.passRate 本身的计算口径 —— 四种通过率公式在这个 fixture 上各不相同:
|
|
95
|
-
// 默认端到端两级聚合:eval a 题内 2/3、eval b 题内 1、eval c errored=0,
|
|
96
|
-
// 跨题均值 (2/3 + 1 + 0) / 3 = 5/9 ≈ 55.6%
|
|
97
|
-
// 条件 taskPassRate 排除 errored:(2/3 + 1) / 2 = 5/6 ≈ 83.3%
|
|
98
|
-
// 端到端 attempt 平铺:3 passed / (3 passed + 1 failed + 1 errored) = 60%
|
|
99
|
-
// eval 折叠投票:a、b passed,c errored → 2/3 ≈ 66.7%
|
|
100
|
-
// 四个数互不相同,任何一处偷懒复用另一个公式都会在这里露馅。
|
|
101
|
-
describe("RunOverview.data · endToEndPassRate 两级聚合口径", () => {
|
|
102
|
-
function fakeVaryingAttemptsContext(): { selection: Selection; attempts: { evalId: string; result: { verdict: EvalResult["verdict"] } }[] } {
|
|
103
|
-
const dir = "/results/compare_bub/snap-1";
|
|
104
|
-
const base = {
|
|
105
|
-
experimentId: "compare/bub",
|
|
106
|
-
startedAt: "2026-07-01T10:00:00Z",
|
|
107
|
-
completedAt: "2026-07-01T10:05:00Z",
|
|
108
|
-
agent: "bub",
|
|
109
|
-
schemaVersion: 1,
|
|
110
|
-
dir,
|
|
111
|
-
};
|
|
112
|
-
const mk = (evalId: string, verdict: "passed" | "failed" | "errored" | "skipped", attemptIndex: number, minute: number) => ({
|
|
113
|
-
evalId,
|
|
114
|
-
experimentId: "compare/bub",
|
|
115
|
-
result: {
|
|
116
|
-
id: evalId,
|
|
117
|
-
agent: "bub",
|
|
118
|
-
verdict,
|
|
119
|
-
attempt: attemptIndex,
|
|
120
|
-
startedAt: `2026-07-01T10:0${minute}:00Z`,
|
|
121
|
-
durationMs: 1000,
|
|
122
|
-
assertions: [],
|
|
123
|
-
},
|
|
124
|
-
ref: { snapshot: "compare_bub/snap-1", attempt: `${evalId}/a${attemptIndex}` },
|
|
125
|
-
snapshot: base,
|
|
126
|
-
events: async () => null,
|
|
127
|
-
trace: async () => null,
|
|
128
|
-
o11y: async () => null,
|
|
129
|
-
diff: async () => null,
|
|
130
|
-
sources: async () => null,
|
|
131
|
-
});
|
|
132
|
-
const attempts = [
|
|
133
|
-
// eval a:3 attempts,2 通过 1 失败 → 题内 partial credit 2/3
|
|
134
|
-
mk("algebra/a", "passed", 0, 0),
|
|
135
|
-
mk("algebra/a", "failed", 1, 1),
|
|
136
|
-
mk("algebra/a", "passed", 2, 2),
|
|
137
|
-
// eval b:1 attempt 通过 → 题内 1
|
|
138
|
-
mk("algebra/b", "passed", 0, 3),
|
|
139
|
-
// eval c:1 attempt errored —— 默认端到端记 0,条件 taskPassRate 才排除
|
|
140
|
-
mk("algebra/c", "errored", 0, 4),
|
|
141
|
-
// eval d:1 attempt 跳过 —— 两级聚合与 eval 折叠计票都要把它排除在分母外
|
|
142
|
-
mk("algebra/d", "skipped", 0, 5),
|
|
143
|
-
];
|
|
144
|
-
const evalIds = [...new Set(attempts.map((a) => a.evalId))];
|
|
145
|
-
const snapshot: Snapshot = {
|
|
146
|
-
...base,
|
|
147
|
-
evals: evalIds.map((id) => ({ id, attempts: attempts.filter((a) => a.evalId === id) })),
|
|
148
|
-
attempts,
|
|
149
|
-
} as unknown as Snapshot;
|
|
150
|
-
const selection: Selection = { snapshots: [snapshot], warnings: [], filter: () => selection };
|
|
151
|
-
return { selection, attempts };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
it("totals.passRate = 端到端两级聚合 55.6%,不排除 errored,也不复用平铺或 eval 折叠投票", async () => {
|
|
155
|
-
const { selection, attempts } = fakeVaryingAttemptsContext();
|
|
156
|
-
const data = await RunOverview.data(selection);
|
|
157
|
-
|
|
158
|
-
expect(data.totals.passRate.value).toBeCloseTo(5 / 9, 10);
|
|
159
|
-
expect(data.totals.passRate.display).toBe("55.6%");
|
|
160
|
-
expect(data.totals.passRate.samples).toBe(5); // 6 attempts - 1 skipped(errored 进桶且记 0)
|
|
161
|
-
expect(data.totals.passRate.total).toBe(6);
|
|
162
|
-
|
|
163
|
-
// attempt 原始占比(旧 bug 公式):必须与两级聚合不同,证明没有从 passed/failed/errored 现算
|
|
164
|
-
const attemptFraction = data.totals.passed / (data.totals.passed + data.totals.failed + data.totals.errored);
|
|
165
|
-
expect(attemptFraction).toBeCloseTo(0.6, 10);
|
|
166
|
-
expect(attemptFraction).not.toBeCloseTo(data.totals.passRate.value as number, 3);
|
|
167
|
-
|
|
168
|
-
// eval 折叠投票(evalLevelStats,GroupSummary 的口径):也必须与两级聚合不同
|
|
169
|
-
const stats = evalLevelStats(
|
|
170
|
-
attempts.map((a) => ({ verdict: a.result.verdict, key: a.evalId })),
|
|
171
|
-
(r) => r.key,
|
|
172
|
-
);
|
|
173
|
-
expect(stats.passRate).toBeCloseTo(2 / 3, 10);
|
|
174
|
-
expect(stats.passRate).not.toBeCloseTo(data.totals.passRate.value as number, 3);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ───────────────────────── GroupSummary ─────────────────────────
|
|
179
|
-
|
|
180
|
-
describe("GroupSummary 双面", () => {
|
|
181
|
-
const html = renderToStaticMarkup(<GroupSummary data={groupSummaryData} />);
|
|
182
|
-
const term = text(<GroupSummary data={groupSummaryData} />);
|
|
183
|
-
|
|
184
|
-
it("text 面形态:一行头(通过率 + experiment/eval 数 + failed/errored + 总成本)+ 最后运行时间", () => {
|
|
185
|
-
expect(term).toMatchInlineSnapshot(`
|
|
186
|
-
"End-to-end pass rate 60% 5/6 · 2 experiments · 6 evals · failed 1 · errored 1 · $1.50
|
|
187
|
-
latest 2026-07-01T11:30:00Z"
|
|
188
|
-
`);
|
|
189
|
-
});
|
|
41
|
+
ScopeSummary,
|
|
42
|
+
} from "./components.tsx";
|
|
43
|
+
import { Col, Row, Section, Style, Tab, Table, Tabs, Text } from "./primitives.tsx";
|
|
44
|
+
import { attemptListData, metricScatterData } from "./compute.ts";
|
|
45
|
+
import { costUSD, defineMetric, endToEndPassRate } from "./metrics.ts";
|
|
46
|
+
import builtInReport from "./built-in/index.tsx";
|
|
47
|
+
|
|
48
|
+
// ───────────────────────── fake 数据 ─────────────────────────
|
|
49
|
+
|
|
50
|
+
let seq = 0;
|
|
51
|
+
|
|
52
|
+
function res(id: string, verdict: Verdict, extra: Partial<EvalResult> = {}): EvalResult {
|
|
53
|
+
seq += 1;
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
agent: "agent-x",
|
|
57
|
+
verdict,
|
|
58
|
+
attempt: 0,
|
|
59
|
+
startedAt: `2026-07-01T00:00:00.${String(seq).padStart(6, "0")}Z`,
|
|
60
|
+
durationMs: 1000,
|
|
61
|
+
assertions: [],
|
|
62
|
+
...extra,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
190
65
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
66
|
+
let runSeq = 0;
|
|
67
|
+
|
|
68
|
+
function snap(spec: {
|
|
69
|
+
experimentId: string;
|
|
70
|
+
results: EvalResult[];
|
|
71
|
+
agent?: string;
|
|
72
|
+
model?: string;
|
|
73
|
+
name?: Snapshot["name"];
|
|
74
|
+
runStartedAt?: string;
|
|
75
|
+
}): Snapshot {
|
|
76
|
+
runSeq += 1;
|
|
77
|
+
const startedAt = spec.runStartedAt ?? `2026-06-01T00:00:00.${String(runSeq).padStart(3, "0")}Z`;
|
|
78
|
+
const snapshot = {
|
|
79
|
+
experimentId: spec.experimentId,
|
|
80
|
+
startedAt,
|
|
81
|
+
completedAt: startedAt,
|
|
82
|
+
agent: spec.agent ?? "agent-x",
|
|
83
|
+
model: spec.model,
|
|
84
|
+
name: spec.name,
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
dir: `/results/exp/snap-${runSeq}`,
|
|
87
|
+
} as Snapshot;
|
|
88
|
+
const attempts: AttemptHandle[] = spec.results.map((r) => ({
|
|
89
|
+
evalId: r.id,
|
|
90
|
+
experimentId: spec.experimentId,
|
|
91
|
+
result: r,
|
|
92
|
+
ref: { snapshot: `exp/snap-${runSeq}`, attempt: `${r.id}/a${r.attempt}` },
|
|
93
|
+
snapshot,
|
|
94
|
+
events: async () => null,
|
|
95
|
+
trace: async () => null,
|
|
96
|
+
o11y: async () => null,
|
|
97
|
+
agentSetup: async () => null,
|
|
98
|
+
diff: async () => null,
|
|
99
|
+
sources: async () => null,
|
|
100
|
+
}));
|
|
101
|
+
const evals = new Map<string, AttemptHandle[]>();
|
|
102
|
+
for (const attempt of attempts) evals.set(attempt.evalId, [...(evals.get(attempt.evalId) ?? []), attempt]);
|
|
103
|
+
snapshot.evals = [...evals.entries()].map(([id, list]) => ({ id, attempts: list }));
|
|
104
|
+
snapshot.attempts = attempts;
|
|
105
|
+
return snapshot;
|
|
106
|
+
}
|
|
201
107
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const t = text(<GroupSummary data={missingCost} />);
|
|
206
|
-
for (const face of [h, t]) {
|
|
207
|
-
expect(face).toContain("no data");
|
|
208
|
-
expect(face).not.toContain("$0");
|
|
209
|
-
}
|
|
210
|
-
});
|
|
108
|
+
function scopeOf(snapshots: Snapshot[], warnings: ScopeWarning[] = []): Scope {
|
|
109
|
+
return makeScope("current-evals", snapshots, warnings);
|
|
110
|
+
}
|
|
211
111
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
112
|
+
function resultsOf(snapshots: Snapshot[]): Results {
|
|
113
|
+
const byId = new Map<string, Snapshot[]>();
|
|
114
|
+
for (const s of snapshots) byId.set(s.experimentId, [...(byId.get(s.experimentId) ?? []), s]);
|
|
115
|
+
const experiments = [...byId.entries()].map(([id, snaps]) => {
|
|
116
|
+
const sorted = [...snaps].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
117
|
+
return {
|
|
118
|
+
id,
|
|
119
|
+
snapshots: sorted,
|
|
120
|
+
latest: sorted[0]!,
|
|
121
|
+
evalIds: [...new Set(sorted.flatMap((s) => s.evals.map((e) => e.id)))].sort(),
|
|
217
122
|
};
|
|
218
|
-
const h = renderToStaticMarkup(<GroupSummary data={allPassed} />);
|
|
219
|
-
const t = text(<GroupSummary data={allPassed} />);
|
|
220
|
-
expect(h).not.toContain("nre-verdict-errored");
|
|
221
|
-
expect(h).not.toContain(">errored<");
|
|
222
|
-
expect(t).not.toContain("errored");
|
|
223
|
-
// failed 计数依旧渲染,哪怕是 0 —— 数据字段本身从不省略,只是 errored 的展示片段在 0 时省略
|
|
224
|
-
expect(h).toContain(">failed<");
|
|
225
|
-
expect(t).toContain("failed 0");
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("存在错误:两面都显示 errored 计数(与 groupSummaryData 基础 fixture 的 errored:1 一致)", () => {
|
|
229
|
-
expect(html).toContain("nre-verdict-errored");
|
|
230
|
-
expect(html).toContain(">errored<");
|
|
231
|
-
expect(term).toContain("errored 1");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("zh-CN locale:web 面走中文字典(端到端成功率/失败/错误/总成本/实验数),text 面同理;display 数字不本地化", () => {
|
|
235
|
-
const zhHtml = renderToStaticMarkup(<GroupSummary data={groupSummaryData} locale="zh-CN" />);
|
|
236
|
-
expect(zhHtml).toContain("端到端成功率");
|
|
237
|
-
expect(zhHtml).toContain("失败");
|
|
238
|
-
expect(zhHtml).toContain("错误");
|
|
239
|
-
expect(zhHtml).toContain("总成本");
|
|
240
|
-
expect(zhHtml).toContain("实验数");
|
|
241
|
-
expect(zhHtml).toContain("60%"); // display 本身不随 locale 变
|
|
242
|
-
|
|
243
|
-
const zhCtx = createTextContext({ width: 80, locale: "zh-CN" });
|
|
244
|
-
const zhTerm = renderNodeToText(<GroupSummary data={groupSummaryData} />, zhCtx);
|
|
245
|
-
expect(zhTerm).toContain("端到端成功率");
|
|
246
|
-
expect(zhTerm).toContain("失败 1");
|
|
247
|
-
expect(zhTerm).toContain("错误 1");
|
|
248
|
-
expect(zhTerm).toContain("60%");
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe("MetricTable 双面", () => {
|
|
253
|
-
const html = renderToStaticMarkup(<MetricTable data={tableData} />);
|
|
254
|
-
const term = text(<MetricTable data={tableData} />);
|
|
255
|
-
|
|
256
|
-
it("text 面形态:对齐列 + 覆盖率角标 + 缺数据 —", () => {
|
|
257
|
-
expect(term).toMatchInlineSnapshot(`
|
|
258
|
-
"agent pass rate code lines
|
|
259
|
-
codex 50% —
|
|
260
|
-
bub 87% 120 lines 5/6"
|
|
261
|
-
`);
|
|
262
123
|
});
|
|
124
|
+
return {
|
|
125
|
+
experiments,
|
|
126
|
+
skipped: [],
|
|
127
|
+
latest: () => makeScope("latest-snapshots", experiments.map((e) => e.latest), []),
|
|
128
|
+
current: () => makeScope("current-evals", experiments.map((e) => e.latest), []),
|
|
129
|
+
} as unknown as Results;
|
|
130
|
+
}
|
|
263
131
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
describe("MetricMatrix 双面", () => {
|
|
278
|
-
const html = renderToStaticMarkup(<MetricMatrix data={matrixData} />);
|
|
279
|
-
const term = text(<MetricMatrix data={matrixData} />);
|
|
280
|
-
|
|
281
|
-
it("text 面形态:稀疏格子 — + 下钻命令指向最值得看的一行", () => {
|
|
282
|
-
expect(term).toMatchInlineSnapshot(`
|
|
283
|
-
"eval bub codex
|
|
284
|
-
algebra/quadratic 100% 0%
|
|
285
|
-
geometry/angles 50% —
|
|
286
|
-
|
|
287
|
-
next: niceeval show geometry/angles"
|
|
288
|
-
`);
|
|
289
|
-
});
|
|
132
|
+
/** 管线便捷入口:resolve + validate + text 渲染,报告声明用最小 meta。 */
|
|
133
|
+
async function renderTreeText(node: ReportNode, scope: Scope, width = 100): Promise<string> {
|
|
134
|
+
const definition = defineReport(node);
|
|
135
|
+
const resolved = await resolveReportTree(pickReportPage(definition).content, {
|
|
136
|
+
scope,
|
|
137
|
+
results: resultsOf(scope.snapshots),
|
|
138
|
+
report: buildReportMeta(definition, scope, "report"),
|
|
139
|
+
memo: new ResolveMemo(),
|
|
140
|
+
});
|
|
141
|
+
validateReportTree(resolved);
|
|
142
|
+
return renderNodeToText(resolved, createTextContext({ width }));
|
|
143
|
+
}
|
|
290
144
|
|
|
291
|
-
|
|
292
|
-
for (const value of ["100%", "0%", "50%"]) {
|
|
293
|
-
expect(html).toContain(value);
|
|
294
|
-
expect(term).toContain(value);
|
|
295
|
-
}
|
|
296
|
-
expect(html.match(/nre-td-empty/g)).toHaveLength(1); // web 空格子
|
|
297
|
-
});
|
|
298
|
-
});
|
|
145
|
+
// ───────────────────────── spec / data 双形态 ─────────────────────────
|
|
299
146
|
|
|
300
|
-
describe("
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
147
|
+
describe("spec 形态与 data 形态", () => {
|
|
148
|
+
const scatterScope = () =>
|
|
149
|
+
scopeOf([
|
|
150
|
+
snap({
|
|
151
|
+
experimentId: "cmp/a",
|
|
152
|
+
agent: "bub",
|
|
153
|
+
results: [res("q", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } })],
|
|
154
|
+
}),
|
|
155
|
+
snap({
|
|
156
|
+
experimentId: "cmp/b",
|
|
157
|
+
agent: "codex",
|
|
158
|
+
results: [res("q", "failed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.1 } })],
|
|
159
|
+
}),
|
|
160
|
+
]);
|
|
314
161
|
|
|
315
|
-
it("
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
162
|
+
it("spec 形态与「先手工调 *Data 再传 data」严格等价:两棵树渲染深等", async () => {
|
|
163
|
+
const scope = scatterScope();
|
|
164
|
+
const options = { points: "experiment", series: "agent", x: costUSD, y: endToEndPassRate } as const;
|
|
165
|
+
const specText = await renderTreeText(
|
|
166
|
+
<MetricScatter points="experiment" series="agent" x={costUSD} y={endToEndPassRate} />,
|
|
167
|
+
scope,
|
|
168
|
+
);
|
|
169
|
+
const data = await metricScatterData(scope, options);
|
|
170
|
+
const dataText = await renderTreeText(<MetricScatter data={data} />, scope);
|
|
171
|
+
expect(specText).toBe(dataText);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("同一组件同时给 data 与 spec 字段报完整用户反馈,不静默取一边", async () => {
|
|
175
|
+
const scope = scatterScope();
|
|
176
|
+
const data = await metricScatterData(scope, { points: "experiment", x: costUSD, y: endToEndPassRate });
|
|
177
|
+
await expect(
|
|
178
|
+
renderTreeText(
|
|
179
|
+
// @ts-expect-error data 与 spec 字段互斥,类型层已拒绝;这里模拟无类型 JS 输入
|
|
180
|
+
<MetricScatter data={data} points="experiment" x={costUSD} y={endToEndPassRate} />,
|
|
181
|
+
scope,
|
|
182
|
+
),
|
|
183
|
+
).rejects.toThrow(/both `data` and spec/);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("input 省略时取宿主注入的 Scope;显式 input 覆盖数据来源", async () => {
|
|
187
|
+
const a = snap({ experimentId: "in/a", results: [res("q", "passed")] });
|
|
188
|
+
const b = snap({ experimentId: "in/b", results: [res("q", "failed")] });
|
|
189
|
+
const scope = scopeOf([a, b]);
|
|
190
|
+
const all = await renderTreeText(<ScopeSummary />, scope);
|
|
191
|
+
expect(all).toContain("2 experiments");
|
|
192
|
+
const narrowed = await renderTreeText(
|
|
193
|
+
<ScopeSummary input={scope.filter((s) => s.experimentId === "in/a")} />,
|
|
194
|
+
scope,
|
|
195
|
+
);
|
|
196
|
+
expect(narrowed).toContain("1 experiment ·");
|
|
197
|
+
// input 也可以是手挑的 Snapshot[](按快照出行)
|
|
198
|
+
const snapshotsOnly = await renderTreeText(
|
|
199
|
+
<MetricTable input={[a]} rows="snapshot" columns={[endToEndPassRate]} />,
|
|
200
|
+
scope,
|
|
201
|
+
140,
|
|
202
|
+
);
|
|
203
|
+
expect(snapshotsOnly).toContain("in/a @");
|
|
204
|
+
expect(snapshotsOnly).not.toContain("in/b");
|
|
322
205
|
});
|
|
323
|
-
});
|
|
324
206
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"agent total algebra geometry
|
|
332
|
-
bub 78.5/100 14/16 (1 missing) 3/4
|
|
333
|
-
codex 52/100 9/16 1.4/4 (2 missing)
|
|
334
|
-
weights: algebra/ ×2 · others ×1"
|
|
335
|
-
`);
|
|
336
|
-
});
|
|
207
|
+
it("data 结构校验:字段改名前的旧 JSON 报错且文案含版本漂移提示;round-trip 的同版本 JSON 照常渲染", async () => {
|
|
208
|
+
const scope = scatterScope();
|
|
209
|
+
const table = { dimension: "agent", columns: [], rows: [] }; // 旧形状:dimension 而非 rowDimension
|
|
210
|
+
await expect(renderTreeText(<MetricTable data={table as never} />, scope)).rejects.toThrow(
|
|
211
|
+
/does not match the current TableData shape[\s\S]*different niceeval version/,
|
|
212
|
+
);
|
|
337
213
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
expect(html).toContain("
|
|
342
|
-
expect(term).toContain("algebra/ ×2");
|
|
214
|
+
const fresh = await metricScatterData(scope, { points: "experiment", x: costUSD, y: endToEndPassRate });
|
|
215
|
+
const roundTrip = JSON.parse(JSON.stringify(fresh));
|
|
216
|
+
const html = renderToStaticMarkup(<MetricScatter data={roundTrip} />);
|
|
217
|
+
expect(html).toContain("nre-metric-scatter");
|
|
343
218
|
});
|
|
344
219
|
|
|
345
|
-
it("
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
220
|
+
it("text 面散点轴方向跟随 better:成本轴反向(便宜在右)、提示恒为右上;x 无 better 时整图无提示", async () => {
|
|
221
|
+
// cmp/a:cost $0.20、通过;cmp/b:cost $0.10、失败。cost better:"lower" → 轴反向,
|
|
222
|
+
// 便宜的 cmp/b(标记 B)落在贵的 cmp/a(标记 A)右侧;刻度仍显示真实值(右端是更小的 $0.10)。
|
|
223
|
+
const scope = scatterScope();
|
|
224
|
+
const text = await renderTreeText(
|
|
225
|
+
<MetricScatter points="experiment" x={costUSD} y={endToEndPassRate} />,
|
|
226
|
+
scope,
|
|
227
|
+
);
|
|
228
|
+
const markCol = (mark: string): number => {
|
|
229
|
+
const line = text.split("\n").find((l) => l.includes(mark));
|
|
230
|
+
expect(line, `plot row with mark ${mark}`).toBeTruthy();
|
|
231
|
+
return line!.indexOf(mark);
|
|
232
|
+
};
|
|
233
|
+
expect(markCol("B")).toBeGreaterThan(markCol("A"));
|
|
234
|
+
expect(text).toContain("better → upper right");
|
|
235
|
+
expect(text).toContain("$0.10");
|
|
236
|
+
expect(text).toContain("$0.20");
|
|
237
|
+
|
|
238
|
+
// x 未声明 better:轴正向、整图无方向提示(组件不猜「更好」朝哪边)。
|
|
239
|
+
const rawCost = defineMetric({
|
|
240
|
+
name: "rawCost",
|
|
241
|
+
unit: "$",
|
|
242
|
+
value: (attempt) => attempt.result.usage?.costUSD ?? null,
|
|
243
|
+
});
|
|
244
|
+
const noHint = await renderTreeText(
|
|
245
|
+
<MetricScatter points="experiment" x={rawCost} y={endToEndPassRate} />,
|
|
246
|
+
scope,
|
|
247
|
+
);
|
|
248
|
+
expect(noHint).not.toContain("better →");
|
|
249
|
+
// 正向轴:贵的 cmp/a(A)在右。
|
|
250
|
+
const noHintCol = (mark: string): number => {
|
|
251
|
+
const line = noHint.split("\n").find((l) => l.includes(mark));
|
|
252
|
+
expect(line, `plot row with mark ${mark}`).toBeTruthy();
|
|
253
|
+
return line!.indexOf(mark);
|
|
352
254
|
};
|
|
353
|
-
|
|
354
|
-
const [, , soloLine] = soloTerm.split("\n");
|
|
355
|
-
expect(soloLine).toContain("—"); // geometry 列:solo 完全没有这一科,cells.subject1 = null → renderTableText 补 —
|
|
356
|
-
expect(soloLine).not.toContain("0/4"); // 不补 0,与 <Table> null 契约一致
|
|
255
|
+
expect(noHintCol("A")).toBeGreaterThan(noHintCol("B"));
|
|
357
256
|
});
|
|
358
257
|
});
|
|
359
258
|
|
|
360
|
-
|
|
361
|
-
const html = renderToStaticMarkup(<MetricScatter data={scatterData} />);
|
|
362
|
-
const term = text(<MetricScatter data={scatterData} />);
|
|
363
|
-
|
|
364
|
-
it("text 面形态:字符坐标图 + 图例 + 缺数据注脚", () => {
|
|
365
|
-
expect(term).toMatchInlineSnapshot(`
|
|
366
|
-
" pass rate ↑
|
|
367
|
-
90%│B····
|
|
368
|
-
│ ·········
|
|
369
|
-
│ ··········
|
|
370
|
-
│ ·········
|
|
371
|
-
│ ·········
|
|
372
|
-
│ ·········
|
|
373
|
-
│ C ··········
|
|
374
|
-
│ ·········
|
|
375
|
-
50%│ ····A
|
|
376
|
-
└───────────────────────────────────────────────────────────────────────────→ cost (axis reversed: right = better)
|
|
377
|
-
$10.00 $5.00
|
|
378
|
-
|
|
379
|
-
better → upper right
|
|
380
|
-
A compare/bub-low B compare/bub-high C compare/codex-mid
|
|
381
|
-
1 point missing data"
|
|
382
|
-
`);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("两面同口径:x 轴反向(lower 好的一端在右)、缺数据点都不画、注脚同数", () => {
|
|
386
|
-
// web:$5(便宜)在 $10 右边(cx 更大)——render.test.tsx 已细测,这里只对注脚
|
|
387
|
-
expect(html).toContain("1 point missing data");
|
|
388
|
-
expect(term).toContain("1 point missing data");
|
|
389
|
-
expect(html).not.toContain('data-key="compare/codex-broken"'); // 注脚 title 如实列缺数据的点,但不画
|
|
390
|
-
// text 图例里也没有缺数据的点
|
|
391
|
-
expect(term).not.toContain("codex-broken");
|
|
392
|
-
});
|
|
259
|
+
// ───────────────────────── resolve 记忆化 ─────────────────────────
|
|
393
260
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
261
|
+
describe("resolve 记忆化", () => {
|
|
262
|
+
it("Matrix 与 Bars 同 spec 时计算只发生一次;不同 spec 各自计算", async () => {
|
|
263
|
+
let calls = 0;
|
|
264
|
+
const counted = defineMetric({
|
|
265
|
+
name: "counted",
|
|
266
|
+
value: () => {
|
|
267
|
+
calls += 1;
|
|
268
|
+
return 1;
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const scope = scopeOf([snap({ experimentId: "memo/a", results: [res("q", "passed")] })]);
|
|
272
|
+
await renderTreeText(
|
|
273
|
+
<Col>
|
|
274
|
+
<MetricMatrix rows="eval" columns="agent" cell={counted} />
|
|
275
|
+
<MetricBars rows="eval" columns="agent" cell={counted} />
|
|
276
|
+
</Col>,
|
|
277
|
+
scope,
|
|
278
|
+
);
|
|
279
|
+
expect(calls).toBe(1); // 一个 attempt,矩阵只算一遍
|
|
401
280
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
│ ··················B
|
|
412
|
-
│
|
|
413
|
-
│
|
|
414
|
-
│
|
|
415
|
-
│A··················
|
|
416
|
-
│ ·····································
|
|
417
|
-
30%│ ··················A
|
|
418
|
-
└───────────────────────────────────────────────────────────────────────────→ Simulated latency
|
|
419
|
-
100ms 300ms
|
|
420
|
-
|
|
421
|
-
A 1 agents B 16 agents
|
|
422
|
-
1 point missing data"
|
|
423
|
-
`);
|
|
281
|
+
calls = 0;
|
|
282
|
+
await renderTreeText(
|
|
283
|
+
<Col>
|
|
284
|
+
<MetricMatrix rows="eval" columns="agent" cell={counted} />
|
|
285
|
+
<MetricMatrix rows="eval" columns="model" cell={counted} />
|
|
286
|
+
</Col>,
|
|
287
|
+
scope,
|
|
288
|
+
);
|
|
289
|
+
expect(calls).toBe(2); // spec 不同(columns 维度不同):各自计算
|
|
424
290
|
});
|
|
425
291
|
|
|
426
|
-
it("
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
292
|
+
it("不同 input 各自计算;字段相同但实例不同的 Metric 各自计算、不报错", async () => {
|
|
293
|
+
let calls = 0;
|
|
294
|
+
const value = () => {
|
|
295
|
+
calls += 1;
|
|
296
|
+
return 1;
|
|
297
|
+
};
|
|
298
|
+
const m1 = defineMetric({ name: "twin", value });
|
|
299
|
+
const m2 = defineMetric({ name: "twin", value: (attempt) => value.call(null) }); // 引用不同的等价定义
|
|
300
|
+
const a = snap({ experimentId: "memo/in-a", results: [res("q", "passed")] });
|
|
301
|
+
const b = snap({ experimentId: "memo/in-b", results: [res("q", "passed")] });
|
|
302
|
+
const scope = scopeOf([a, b]);
|
|
303
|
+
await renderTreeText(
|
|
304
|
+
<Col>
|
|
305
|
+
<MetricMatrix input={[a]} rows="eval" columns="agent" cell={m1} />
|
|
306
|
+
<MetricMatrix input={[b]} rows="eval" columns="agent" cell={m1} />
|
|
307
|
+
<MetricMatrix input={[a]} rows="eval" columns="agent" cell={m2} />
|
|
308
|
+
</Col>,
|
|
309
|
+
scope,
|
|
310
|
+
);
|
|
311
|
+
expect(calls).toBe(3); // 两个 input 各一次 + 不同实例的 Metric 再一次
|
|
430
312
|
});
|
|
431
313
|
});
|
|
432
314
|
|
|
433
|
-
|
|
434
|
-
const html = renderToStaticMarkup(<DeltaTable data={deltaData} />);
|
|
435
|
-
const term = text(<DeltaTable data={deltaData} />);
|
|
315
|
+
// ───────────────────────── 组合组件与树形状 ─────────────────────────
|
|
436
316
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
317
|
+
describe("组合组件(函数形态)", () => {
|
|
318
|
+
it("resolve 阶段以 (props, ctx) 调用并递归展开;与手写等价树渲染相同;async 可用", async () => {
|
|
319
|
+
const scope = scopeOf([snap({ experimentId: "compose/a", results: [res("q", "passed")] })]);
|
|
320
|
+
const Composed = defineComponent(async (_props: Record<never, never>, ctx) => (
|
|
321
|
+
<Section title="wrapped">
|
|
322
|
+
<ScopeSummary input={ctx.scope} />
|
|
323
|
+
</Section>
|
|
324
|
+
));
|
|
325
|
+
const composed = await renderTreeText(<Composed />, scope);
|
|
326
|
+
const manual = await renderTreeText(
|
|
327
|
+
<Section title="wrapped">
|
|
328
|
+
<ScopeSummary />
|
|
329
|
+
</Section>,
|
|
330
|
+
scope,
|
|
331
|
+
);
|
|
332
|
+
expect(composed).toBe(manual);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("ctx.results 可自行挑 Snapshot[] 喂 input;ctx.report 携带走完回退链的 title 与当前 pageId", async () => {
|
|
336
|
+
const named = snap({ experimentId: "hist/a", name: "Memory Evals", results: [res("q", "passed")] });
|
|
337
|
+
const scope = scopeOf([named]);
|
|
338
|
+
const Meta = defineComponent((_props: Record<never, never>, ctx) => {
|
|
339
|
+
const history = ctx.results.experiments[0]!.snapshots;
|
|
340
|
+
return (
|
|
341
|
+
<Col>
|
|
342
|
+
<Text>{`title=${typeof ctx.report.title === "string" ? ctx.report.title : "?"} page=${ctx.report.pageId}`}</Text>
|
|
343
|
+
<ScopeSummary input={history} />
|
|
344
|
+
</Col>
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
const definition = defineReport({ pages: [{ id: "meta", title: "Meta", content: <Meta /> }] });
|
|
348
|
+
const text = await renderReportToText(definition, { scope, results: resultsOf([named]) }, { pageId: "meta" });
|
|
349
|
+
// 声明没给 title → 唯一快照 name 回退
|
|
350
|
+
expect(text).toContain("title=Memory Evals page=meta");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("同层 sibling 并行取数且输出保持声明顺序:慢 resolve 在前不换位", async () => {
|
|
354
|
+
const order: string[] = [];
|
|
355
|
+
const Slow = defineComponent<{ label: string }, { label: string }>({
|
|
356
|
+
resolve: async (props) => {
|
|
357
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
358
|
+
order.push("slow-resolved");
|
|
359
|
+
return props;
|
|
360
|
+
},
|
|
361
|
+
web: ({ label }) => <p>{label}</p>,
|
|
362
|
+
text: ({ label }) => label,
|
|
363
|
+
});
|
|
364
|
+
const Fast = defineComponent<{ label: string }, { label: string }>({
|
|
365
|
+
resolve: (props) => {
|
|
366
|
+
order.push("fast-resolved");
|
|
367
|
+
return props;
|
|
368
|
+
},
|
|
369
|
+
web: ({ label }) => <p>{label}</p>,
|
|
370
|
+
text: ({ label }) => label,
|
|
371
|
+
});
|
|
372
|
+
const scope = scopeOf([]);
|
|
373
|
+
const text = await renderTreeText(
|
|
374
|
+
<Col>
|
|
375
|
+
<Slow label="first" />
|
|
376
|
+
<Fast label="second" />
|
|
377
|
+
</Col>,
|
|
378
|
+
scope,
|
|
379
|
+
);
|
|
380
|
+
expect(order[0]).toBe("fast-resolved"); // 真并行:快的先完成
|
|
381
|
+
expect(text.indexOf("first")).toBeLessThan(text.indexOf("second")); // 输出仍按声明序
|
|
452
382
|
});
|
|
453
383
|
});
|
|
454
384
|
|
|
455
|
-
describe("
|
|
456
|
-
const
|
|
457
|
-
const term = text(<AttemptList items={attemptListItems} />);
|
|
458
|
-
|
|
459
|
-
it("两面同口径:判定符 + locator + 主失败/error 摘要一致", () => {
|
|
460
|
-
for (const piece of ["roots-correct", "expected x=2", "received x=3", "TypeError", "@1a4a4a4", "@1c1c1c1"]) {
|
|
461
|
-
expect(html).toContain(piece);
|
|
462
|
-
expect(term).toContain(piece);
|
|
463
|
-
}
|
|
464
|
-
// 结构化 error 只显示一层摘要:stack / cause / diagnostics 属于 locator 下钻详情,
|
|
465
|
-
// 随数据携带但不塞进比较列表(docs/feature/reports/library.md「AttemptList」)。
|
|
466
|
-
for (const face of [html, term]) {
|
|
467
|
-
expect(face).not.toContain("adapter.ts:42"); // error.stack
|
|
468
|
-
expect(face).not.toContain("sandbox teardown timed out"); // diagnostics[].message
|
|
469
|
-
}
|
|
470
|
-
// web 面走证据室路由(#/attempt/@<locator>);text 面只列 locator 本身,不重复整条命令
|
|
471
|
-
// (docs/feature/reports/architecture.md「text 输出只在整份报告末尾给一次命令模板」)。
|
|
472
|
-
expect(html).toContain('href="#/attempt/@1a4a4a4"');
|
|
473
|
-
expect(term).not.toMatch(/\[[EXD⏱,]+\]/);
|
|
474
|
-
expect(html).not.toMatch(/\[[EXD⏱,]+\]/);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it("total > items.length 时两面都如实报剩余数量", () => {
|
|
478
|
-
const htmlTrunc = renderToStaticMarkup(<AttemptList items={attemptListItems} total={attemptListItems.length + 2} />);
|
|
479
|
-
const termTrunc = text(<AttemptList items={attemptListItems} total={attemptListItems.length + 2} />);
|
|
480
|
-
expect(htmlTrunc).toContain("2 more not shown");
|
|
481
|
-
expect(termTrunc).toContain("2 more not shown");
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
it("比较列表不内联 assertion evidence,并保留 locator 作为完整证据入口", () => {
|
|
485
|
-
const longEvidence = "x".repeat(2_000);
|
|
486
|
-
const item = {
|
|
487
|
-
...attemptListItems[0],
|
|
488
|
-
assertions: [{ ...attemptListItems[0].assertions[0], evidence: longEvidence }],
|
|
489
|
-
};
|
|
490
|
-
const out = text(<AttemptList items={[item]} />);
|
|
491
|
-
expect(out).toContain(item.locator);
|
|
492
|
-
expect(out).not.toContain(longEvidence);
|
|
493
|
-
expect(out.length).toBeLessThan(1_000);
|
|
494
|
-
});
|
|
385
|
+
describe("ReportNode 形状与非法节点", () => {
|
|
386
|
+
const scope = () => scopeOf([snap({ experimentId: "node/a", results: [res("q", "passed")] })]);
|
|
495
387
|
|
|
496
|
-
it("
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
388
|
+
it("数组 / Fragment 展平保序;null / undefined / boolean 渲染为空", async () => {
|
|
389
|
+
const groups = ["g1", "g2"];
|
|
390
|
+
const text = await renderTreeText(
|
|
391
|
+
<Col>
|
|
392
|
+
{groups.map((g) => (
|
|
393
|
+
<Section key={g} title={g}>
|
|
394
|
+
<Text>{`body of ${g}`}</Text>
|
|
395
|
+
</Section>
|
|
396
|
+
))}
|
|
397
|
+
<>
|
|
398
|
+
<Text>in fragment</Text>
|
|
399
|
+
</>
|
|
400
|
+
{false && <Text>hidden</Text>}
|
|
401
|
+
{null}
|
|
402
|
+
</Col>,
|
|
403
|
+
scope(),
|
|
404
|
+
);
|
|
405
|
+
expect(text.indexOf("g1")).toBeLessThan(text.indexOf("g2"));
|
|
406
|
+
expect(text).toContain("in fragment");
|
|
407
|
+
expect(text).not.toContain("hidden");
|
|
503
408
|
});
|
|
504
|
-
});
|
|
505
409
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
it("两面同口径:身份、判定、展开到 Attempt 的 locator 徽标一致", () => {
|
|
511
|
-
for (const piece of ["algebra/quadratic", "compare/bub", "geometry/angles", "compare/codex", "@1a4a4a4", "@1c1c1c1"]) {
|
|
512
|
-
expect(html).toContain(piece);
|
|
513
|
-
expect(term).toContain(piece);
|
|
514
|
-
}
|
|
410
|
+
it("裸字符串在树校验时按完整用户反馈拒绝并指引包 <Text>", async () => {
|
|
411
|
+
await expect(renderTreeText(<Col>{"free text" as unknown as ReportNode}</Col>, scope())).rejects.toThrow(
|
|
412
|
+
/bare string[\s\S]*<Text>/,
|
|
413
|
+
);
|
|
515
414
|
});
|
|
516
415
|
|
|
517
|
-
it("
|
|
518
|
-
|
|
519
|
-
expect(
|
|
416
|
+
it("React 组件 / 未包装函数与 HTML intrinsic 在展开遇到时拒绝", async () => {
|
|
417
|
+
const Plain = ({ label }: { label: string }) => <p>{label}</p>;
|
|
418
|
+
await expect(renderTreeText(<Plain label="x" />, scope())).rejects.toThrow(
|
|
419
|
+
/not a report component[\s\S]*defineComponent/,
|
|
420
|
+
);
|
|
421
|
+
await expect(renderTreeText(<div>x</div>, scope())).rejects.toThrow(/raw HTML <div>/);
|
|
520
422
|
});
|
|
521
423
|
|
|
522
|
-
it("
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
424
|
+
it("validateReportTree 拒绝缺任一渲染面的组件(无类型 JS 绕过 defineComponent 时)", () => {
|
|
425
|
+
const single = Object.assign(() => null, {
|
|
426
|
+
[Symbol.for("niceeval.report.faces")]: { web: () => null }, // 缺 text 面
|
|
427
|
+
});
|
|
428
|
+
expect(() => validateReportTree({ type: single, props: {} })).toThrow(/missing its text face/);
|
|
429
|
+
expect(() => validateReportTree({ type: "div", props: {} })).toThrow(/raw HTML <div>/);
|
|
526
430
|
});
|
|
527
431
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const reason = "roots-correct · expected x=2 · received x=3";
|
|
531
|
-
for (const face of [html, term]) {
|
|
532
|
-
expect(face.split(reason)).toHaveLength(2);
|
|
533
|
-
expect(face).toContain("32.0s avg");
|
|
534
|
-
expect(face).toContain("$0.12 avg");
|
|
535
|
-
}
|
|
432
|
+
it("defineComponent 对象形态缺 text 或 web 在定义时报完整用户反馈", () => {
|
|
433
|
+
expect(() => defineComponent({ web: () => null } as never)).toThrow(/both faces/);
|
|
536
434
|
});
|
|
537
435
|
});
|
|
538
436
|
|
|
539
|
-
|
|
540
|
-
const html = renderToStaticMarkup(<ExperimentList items={experimentListItems} />);
|
|
541
|
-
const term = text(<ExperimentList items={experimentListItems} />);
|
|
542
|
-
|
|
543
|
-
it("两面同口径:身份、Eval 判定构成、展开到 Eval 的 locator 徽标一致", () => {
|
|
544
|
-
for (const piece of ["compare/bub", "compare/codex", "algebra/quadratic", "geometry/angles", "@1a4a4a4", "@1c1c1c1"]) {
|
|
545
|
-
expect(html).toContain(piece);
|
|
546
|
-
expect(term).toContain(piece);
|
|
547
|
-
}
|
|
548
|
-
// 官方两级聚合 endToEndPassRate.display 两面同一个数字,不各自重算
|
|
549
|
-
expect(html).toContain("50%");
|
|
550
|
-
expect(term).toContain("50%");
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it("text 面先给比较表,再按 experiment 给 Eval / Attempt 父子表", () => {
|
|
554
|
-
const blocks = term.split("\n\n");
|
|
555
|
-
expect(blocks).toHaveLength(experimentListItems.length + 1);
|
|
556
|
-
expect(blocks[0]).toContain("Model");
|
|
557
|
-
expect(blocks[0]).toContain("Avg");
|
|
558
|
-
expect(blocks[1]!.match(/algebra\/quadratic/g)).toHaveLength(1);
|
|
559
|
-
expect(blocks[1]).toMatch(/✗ failed\s+algebra\/quadratic[\s\S]*├─ @1a4a4a4[\s\S]*└─ @1b5b5b5/);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
// bug: memory/eval-parent-repeats-attempt-failure.md
|
|
563
|
-
it("web 的 Eval 父行使用固定题级汇总,失败摘要只在 Attempt 子行出现", () => {
|
|
564
|
-
const reason = "roots-correct · expected x=2 · received x=3";
|
|
565
|
-
expect(html.split(reason)).toHaveLength(3); // 两个 failed Attempt,各显示一次
|
|
566
|
-
expect(html).toContain('<span class="nre-eval-rollup">32.0s avg · $0.12 avg</span>');
|
|
567
|
-
expect(term).toMatch(/✗ failed\s+algebra\/quadratic\s+32\.0s avg\s+\$0\.12 avg/);
|
|
568
|
-
});
|
|
437
|
+
// ───────────────────────── 双面同源 ─────────────────────────
|
|
569
438
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
received: `\n// next.config.ts\n${"import type { NextConfig } from 'next';\n\n".repeat(50)}`,
|
|
581
|
-
}],
|
|
439
|
+
describe("text/web 双面同源", () => {
|
|
440
|
+
it("双面显示同一份解析终值、覆盖率与 warning;宿主在报告树外统一显示 warning", async () => {
|
|
441
|
+
const s = snap({ experimentId: "dual/a", results: [res("q1", "passed"), res("q2", "skipped")] });
|
|
442
|
+
const warning: ScopeWarning = {
|
|
443
|
+
kind: "unfinished-snapshot",
|
|
444
|
+
experimentId: "dual/a",
|
|
445
|
+
startedAt: s.startedAt,
|
|
446
|
+
dir: s.dir,
|
|
447
|
+
message: "snapshot is incomplete",
|
|
448
|
+
command: "niceeval exp dual/a",
|
|
582
449
|
};
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
expect(
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
450
|
+
const scope = scopeOf([s], [warning]);
|
|
451
|
+
const definition = defineReport(<ScopeSummary />);
|
|
452
|
+
const ctx = { scope, results: resultsOf([s]) };
|
|
453
|
+
const text = await renderReportToText(definition, ctx);
|
|
454
|
+
const html = await renderReportToStaticHtml(definition, ctx);
|
|
455
|
+
for (const face of [text, html]) {
|
|
456
|
+
expect(face).toContain("100%"); // endToEndPassRate:skipped 不稀释
|
|
457
|
+
expect(face).toContain("1/2"); // samples/total 覆盖率
|
|
458
|
+
expect(face).toContain("snapshot is incomplete");
|
|
459
|
+
}
|
|
460
|
+
// web 面把警告的 command 渲染为可复制命令
|
|
461
|
+
expect(html).toContain("niceeval exp dual/a");
|
|
462
|
+
expect(html).toContain("nre-warning-command");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("web 排序/过滤只改变浏览状态:有无 filter prop 数值与行集合相同", async () => {
|
|
466
|
+
const scope = scopeOf([snap({ experimentId: "f/a", results: [res("q", "passed")] })]);
|
|
467
|
+
const definition = (filter: boolean) =>
|
|
468
|
+
defineReport(<MetricTable rows="experiment" columns={[endToEndPassRate]} filter={filter} />);
|
|
469
|
+
const ctx = { scope, results: resultsOf(scope.snapshots) };
|
|
470
|
+
const plain = await renderReportToStaticHtml(definition(false), ctx);
|
|
471
|
+
const filtered = await renderReportToStaticHtml(definition(true), ctx);
|
|
472
|
+
const values = (html: string) => html.match(/data-sort-value="[^"]*"/g);
|
|
473
|
+
expect(values(filtered)).toEqual(values(plain));
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("ExperimentList text 面保持实体层级:一题两 attempt 只出现一次 Eval 标题;失败摘要只在 Attempt 子行", async () => {
|
|
477
|
+
const s = snap({
|
|
478
|
+
experimentId: "grp/exp-a",
|
|
479
|
+
results: [
|
|
480
|
+
res("algebra/retry", "failed", {
|
|
481
|
+
attempt: 0,
|
|
482
|
+
assertions: [
|
|
483
|
+
{
|
|
484
|
+
name: "equals",
|
|
485
|
+
severity: "gate",
|
|
486
|
+
outcome: "failed",
|
|
487
|
+
score: 0,
|
|
488
|
+
detail: "equals(42)",
|
|
489
|
+
expected: "42",
|
|
490
|
+
received: "41",
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
}),
|
|
494
|
+
res("algebra/retry", "passed", { attempt: 1 }),
|
|
495
|
+
],
|
|
496
|
+
});
|
|
497
|
+
const text = await renderTreeText(<ExperimentList relativeTo="grp" />, scopeOf([s]), 160);
|
|
498
|
+
expect(text.match(/algebra\/retry/g)).toHaveLength(1); // Eval 父行只出现一次
|
|
499
|
+
expect(text).toContain("├─");
|
|
500
|
+
expect(text).toContain("└─");
|
|
501
|
+
// 失败摘要只在 Attempt 子行(父行不复述)
|
|
502
|
+
expect(text.match(/equals\(42\)/g)).toHaveLength(1);
|
|
503
|
+
// relativeTo:行标签只显示末段
|
|
504
|
+
expect(text).toContain("exp-a");
|
|
601
505
|
});
|
|
602
506
|
});
|
|
603
507
|
|
|
604
|
-
// ─────────────────────────
|
|
605
|
-
|
|
606
|
-
describe("
|
|
607
|
-
it("
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
expect(
|
|
634
|
-
expect(
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
<Text>left column body</Text>
|
|
642
|
-
<Text>right column body</Text>
|
|
643
|
-
</Row>
|
|
644
|
-
);
|
|
645
|
-
const narrow = renderNodeToText(node, createTextContext({ width: 30 }));
|
|
646
|
-
expect(narrow).not.toContain("│");
|
|
647
|
-
expect(narrow).toContain("left column body\n\nright column body");
|
|
508
|
+
// ───────────────────────── FailureList ─────────────────────────
|
|
509
|
+
|
|
510
|
+
describe("FailureList", () => {
|
|
511
|
+
it("与手写组合严格等价:failed/errored、开始时间降序、limit 截断且 total 报截断前总数", async () => {
|
|
512
|
+
const s = snap({
|
|
513
|
+
experimentId: "fail/a",
|
|
514
|
+
results: [
|
|
515
|
+
res("q1", "failed", { startedAt: "2026-07-01T01:00:00.000Z" }),
|
|
516
|
+
res("q2", "errored", {
|
|
517
|
+
startedAt: "2026-07-01T03:00:00.000Z",
|
|
518
|
+
error: { code: "x", message: "boom", phase: "eval.run" },
|
|
519
|
+
}),
|
|
520
|
+
res("q3", "failed", { startedAt: "2026-07-01T02:00:00.000Z" }),
|
|
521
|
+
res("q4", "passed", { startedAt: "2026-07-01T04:00:00.000Z" }),
|
|
522
|
+
],
|
|
523
|
+
});
|
|
524
|
+
const scope = scopeOf([s]);
|
|
525
|
+
const failureText = await renderTreeText(<FailureList limit={2} />, scope);
|
|
526
|
+
|
|
527
|
+
// 手写组合:attemptListData → 过滤 → 排序 → AttemptList data 形态
|
|
528
|
+
const all = await attemptListData(scope);
|
|
529
|
+
const startedAt = new Map(s.attempts.map((a) => [a.evalId, a.result.startedAt ?? ""]));
|
|
530
|
+
const failures = all
|
|
531
|
+
.filter((x) => x.verdict === "failed" || x.verdict === "errored")
|
|
532
|
+
.sort((a, b) => (startedAt.get(b.evalId) ?? "").localeCompare(startedAt.get(a.evalId) ?? ""));
|
|
533
|
+
const manualText = await renderTreeText(<AttemptList data={failures.slice(0, 2)} total={failures.length} />, scope);
|
|
534
|
+
expect(failureText).toBe(manualText);
|
|
535
|
+
expect(failureText).toContain("(1 more not shown)"); // 3 条失败,截断到 2
|
|
536
|
+
// 最近的失败在前
|
|
537
|
+
expect(failureText.indexOf("q2")).toBeLessThan(failureText.indexOf("q3"));
|
|
538
|
+
expect(failureText).not.toContain("q4"); // passed 不进失败清单
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("失败数少于 limit 时 total 等于 data 长度,不产出截断文案", async () => {
|
|
542
|
+
const s = snap({ experimentId: "fail/few", results: [res("q1", "failed")] });
|
|
543
|
+
const text = await renderTreeText(<FailureList />, scopeOf([s]));
|
|
544
|
+
expect(text).not.toContain("more not shown");
|
|
648
545
|
});
|
|
649
546
|
});
|
|
650
547
|
|
|
651
|
-
// ───────────────────────── Table
|
|
652
|
-
//
|
|
653
|
-
// 官方的 MetricTable / MetricMatrix / Scoreboard / DeltaTable 的 text 面就建在它上面
|
|
654
|
-
// (上面那几个 describe 的内联快照即它的输出);这里验收作者直接用它时的四条契约:
|
|
655
|
-
// 显示宽度对齐(中文不撕歪)、右对齐、缺数据 —、超宽折行。
|
|
656
|
-
|
|
657
|
-
/** 某个片段的右边缘落在第几显示列 —— 右对齐验收看的就是它。 */
|
|
658
|
-
function displayEndOf(line: string, piece: string): number {
|
|
659
|
-
const at = line.indexOf(piece);
|
|
660
|
-
expect(at).toBeGreaterThanOrEqual(0);
|
|
661
|
-
return stringWidth(line.slice(0, at + piece.length));
|
|
662
|
-
}
|
|
548
|
+
// ───────────────────────── Table 与排版原语 ─────────────────────────
|
|
663
549
|
|
|
664
|
-
describe("Table
|
|
665
|
-
const
|
|
666
|
-
<Table
|
|
667
|
-
columns={[
|
|
668
|
-
{ key: "eval", header: "题目" },
|
|
669
|
-
{ key: "pass", header: "通过率", align: "right" },
|
|
670
|
-
{ key: "cost", header: "成本", align: "right" },
|
|
671
|
-
]}
|
|
672
|
-
rows={[
|
|
673
|
-
{
|
|
674
|
-
key: "记忆/写缓存",
|
|
675
|
-
locator: "@160iuj3h" as AttemptLocator,
|
|
676
|
-
cells: { eval: "记忆/写缓存", pass: "87%", cost: "$0.09" },
|
|
677
|
-
},
|
|
678
|
-
{
|
|
679
|
-
key: "浏览/表单填写",
|
|
680
|
-
locator: "@1qrdcfq8" as AttemptLocator,
|
|
681
|
-
cells: { eval: "浏览/表单填写", pass: null, cost: null },
|
|
682
|
-
},
|
|
683
|
-
]}
|
|
684
|
-
/>
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
it("中文列宽:按显示宽度对齐(CJK 记 2 列),不是 .length —— String.padEnd 撕歪表的那一步", () => {
|
|
688
|
-
const term = text(cjkTable);
|
|
689
|
-
expect(term).toMatchInlineSnapshot(`
|
|
690
|
-
"题目 通过率 成本 attempt
|
|
691
|
-
记忆/写缓存 87% $0.09 @160iuj3h
|
|
692
|
-
浏览/表单填写 — — @1qrdcfq8"
|
|
693
|
-
`);
|
|
694
|
-
|
|
695
|
-
const [header, first, second] = term.split("\n");
|
|
696
|
-
// 每列的右边缘落在同一显示列:中文格子多一倍显示宽度,补齐必须按显示宽度算
|
|
697
|
-
expect(displayEndOf(first, "87%")).toBe(displayEndOf(header, "通过率"));
|
|
698
|
-
expect(displayEndOf(second, "—")).toBe(displayEndOf(header, "通过率"));
|
|
699
|
-
expect(displayEndOf(first, "$0.09")).toBe(displayEndOf(header, "成本"));
|
|
700
|
-
expect(displayEndOf(first, "@160iuj3h")).toBe(displayEndOf(second, "@1qrdcfq8"));
|
|
701
|
-
// 上面那几条不是巧合:UTF-16 码元数与显示宽度在中文上就是不同的两个数,
|
|
702
|
-
// 拿 .length 补齐(或 String.prototype.padEnd)必然歪
|
|
703
|
-
expect("记忆/写缓存".length).toBe(6);
|
|
704
|
-
expect(stringWidth("记忆/写缓存")).toBe(11);
|
|
705
|
-
});
|
|
550
|
+
describe("Table 与文本排版原语", () => {
|
|
551
|
+
const ctx = () => createTextContext({ width: 100 });
|
|
706
552
|
|
|
707
|
-
it("
|
|
708
|
-
const
|
|
553
|
+
it("null 单元格与 cells 缺键都渲染成 —,不补 0", () => {
|
|
554
|
+
const text = renderNodeToText(
|
|
709
555
|
<Table
|
|
710
556
|
columns={[
|
|
711
|
-
{ key: "
|
|
712
|
-
{ key: "
|
|
557
|
+
{ key: "a", header: "A" },
|
|
558
|
+
{ key: "b", header: "B", align: "right" },
|
|
713
559
|
]}
|
|
714
560
|
rows={[
|
|
715
|
-
{ key: "
|
|
716
|
-
{ key: "
|
|
561
|
+
{ key: "r1", cells: { a: "x", b: null } },
|
|
562
|
+
{ key: "r2", cells: { a: "y" } },
|
|
717
563
|
]}
|
|
718
564
|
/>,
|
|
565
|
+
ctx(),
|
|
719
566
|
);
|
|
720
|
-
expect(
|
|
721
|
-
|
|
722
|
-
bub $9.00
|
|
723
|
-
codex $123.45"
|
|
724
|
-
`);
|
|
725
|
-
const [, first, second] = term.split("\n");
|
|
726
|
-
expect(displayEndOf(first, "$9.00")).toBe(displayEndOf(second, "$123.45"));
|
|
727
|
-
// 左对齐列的左边缘对齐(不传 align 就是 left)
|
|
728
|
-
expect(first.startsWith("bub")).toBe(true);
|
|
729
|
-
expect(second.startsWith("codex")).toBe(true);
|
|
567
|
+
expect(text.match(/—/g)!.length).toBe(2);
|
|
568
|
+
expect(text).not.toMatch(/\b0\b/);
|
|
730
569
|
});
|
|
731
570
|
|
|
732
|
-
it("
|
|
733
|
-
|
|
571
|
+
it("cells 出现未声明的 key 以完整用户反馈报错;列 key 重复报错", () => {
|
|
572
|
+
expect(() =>
|
|
573
|
+
renderNodeToText(
|
|
574
|
+
<Table columns={[{ key: "a", header: "A" }]} rows={[{ key: "r", cells: { ghost: "x" } }]} />,
|
|
575
|
+
ctx(),
|
|
576
|
+
),
|
|
577
|
+
).toThrow(/no column declares/);
|
|
578
|
+
expect(() =>
|
|
579
|
+
renderNodeToText(
|
|
580
|
+
<Table
|
|
581
|
+
columns={
|
|
582
|
+
[
|
|
583
|
+
{ key: "a", header: "A" },
|
|
584
|
+
{ key: "a", header: "A2" },
|
|
585
|
+
] as never
|
|
586
|
+
}
|
|
587
|
+
rows={[]}
|
|
588
|
+
/>,
|
|
589
|
+
ctx(),
|
|
590
|
+
),
|
|
591
|
+
).toThrow(/declared twice/);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("带 locator 的行多出 attempt 列;全部无 locator 时不出该列", () => {
|
|
595
|
+
const withLocator = renderNodeToText(
|
|
734
596
|
<Table
|
|
735
|
-
columns={[
|
|
736
|
-
{ key: "agent", header: "agent" },
|
|
737
|
-
{ key: "pass", header: "pass", align: "right" },
|
|
738
|
-
{ key: "cost", header: "cost", align: "right" },
|
|
739
|
-
]}
|
|
597
|
+
columns={[{ key: "a", header: "A" }]}
|
|
740
598
|
rows={[
|
|
741
|
-
{ key: "
|
|
742
|
-
{ key: "
|
|
599
|
+
{ key: "r1", cells: { a: "x" }, locator: "@1abc0def" as never },
|
|
600
|
+
{ key: "r2", cells: { a: "y" } },
|
|
743
601
|
]}
|
|
744
|
-
|
|
602
|
+
/>,
|
|
603
|
+
ctx(),
|
|
745
604
|
);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
"
|
|
750
|
-
|
|
751
|
-
claude — —"
|
|
752
|
-
`);
|
|
753
|
-
for (const face of [term, html]) {
|
|
754
|
-
expect(face).toContain("—");
|
|
755
|
-
expect(face).not.toContain("$0.00");
|
|
756
|
-
expect(face).not.toContain("0%");
|
|
757
|
-
}
|
|
758
|
-
expect(html).toContain('<td class="nre-align-right"><span class="nre-missing">—</span></td>');
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
it("超宽:先折最宽的左对齐列(数字列不折),压到下限仍放不下就丢列并如实报数", () => {
|
|
762
|
-
const wide = (
|
|
763
|
-
<Table
|
|
764
|
-
columns={[
|
|
765
|
-
{ key: "eval", header: "eval" },
|
|
766
|
-
{ key: "reason", header: "reason" },
|
|
767
|
-
{ key: "cost", header: "cost", align: "right" },
|
|
768
|
-
]}
|
|
769
|
-
rows={[
|
|
770
|
-
{
|
|
771
|
-
key: "weather/brooklyn",
|
|
772
|
-
cells: {
|
|
773
|
-
eval: "weather/brooklyn",
|
|
774
|
-
reason: "gate calledTool(\"get_weather\") — the tool was never called by the agent",
|
|
775
|
-
cost: "$0.04",
|
|
776
|
-
},
|
|
777
|
-
},
|
|
778
|
-
]}
|
|
779
|
-
/>
|
|
605
|
+
expect(withLocator).toContain("@1abc0def");
|
|
606
|
+
expect(withLocator).toContain("attempt");
|
|
607
|
+
const without = renderNodeToText(
|
|
608
|
+
<Table columns={[{ key: "a", header: "A" }]} rows={[{ key: "r1", cells: { a: "x" } }]} />,
|
|
609
|
+
ctx(),
|
|
780
610
|
);
|
|
781
|
-
|
|
782
|
-
// 折行:一条逻辑行铺成多条物理行,没有一行溢出 48 列
|
|
783
|
-
const lines = narrow.split("\n");
|
|
784
|
-
expect(lines.length).toBeGreaterThan(2);
|
|
785
|
-
for (const line of lines) expect(stringWidth(line)).toBeLessThanOrEqual(48);
|
|
786
|
-
// 数字列没被折:$0.04 完整出现在某一行上
|
|
787
|
-
expect(narrow).toContain("$0.04");
|
|
788
|
-
expect(narrow).toContain("never called");
|
|
789
|
-
|
|
790
|
-
// 压到下限还是放不下 → 从右侧丢列,如实报剩余列数(不静默截断)
|
|
791
|
-
const tiny = renderNodeToText(wide, createTextContext({ width: 20 }));
|
|
792
|
-
expect(tiny).toContain("(1 more column not shown)");
|
|
793
|
-
expect(tiny).not.toContain("$0.04");
|
|
611
|
+
expect(without).not.toContain("attempt");
|
|
794
612
|
});
|
|
795
613
|
|
|
796
|
-
it("
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
});
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
// ───────────────────────── 树校验与自定义组件 ─────────────────────────
|
|
806
|
-
|
|
807
|
-
describe("渲染前树校验", () => {
|
|
808
|
-
it("字符串 intrinsic(<div>)报错,错误指名组件路径", () => {
|
|
809
|
-
const bad = (
|
|
810
|
-
<Col>
|
|
811
|
-
<Section title="x">
|
|
812
|
-
<div>raw</div>
|
|
813
|
-
</Section>
|
|
814
|
-
</Col>
|
|
815
|
-
);
|
|
816
|
-
expect(() => validateReportTree(bad)).toThrow(
|
|
817
|
-
/Raw HTML <div> has no terminal face; use <Text>, layout primitives, or a defineComponent component\. \(in <Col> > <Section>\)/,
|
|
614
|
+
it("Row text 面:宽度装得下按显示宽度并排,装不下整块纵向堆叠、内容完整", () => {
|
|
615
|
+
const tree = (
|
|
616
|
+
<Row>
|
|
617
|
+
<Text>left block</Text>
|
|
618
|
+
<Text>right block</Text>
|
|
619
|
+
</Row>
|
|
818
620
|
);
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const
|
|
621
|
+
const wide = renderNodeToText(tree, createTextContext({ width: 80 }));
|
|
622
|
+
expect(wide.split("\n")[0]).toContain("left block");
|
|
623
|
+
expect(wide.split("\n")[0]).toContain("right block");
|
|
624
|
+
const narrow = renderNodeToText(tree, createTextContext({ width: 20 }));
|
|
625
|
+
expect(narrow).toContain("left block");
|
|
626
|
+
expect(narrow).toContain("right block");
|
|
627
|
+
expect(narrow.split("\n")[0]).not.toContain("right block"); // 纵向堆叠
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("Style 在 text 面零输出,web 面吐 <style>(页级全局)", async () => {
|
|
631
|
+
const scope = scopeOf([]);
|
|
632
|
+
const definition = defineReport(
|
|
823
633
|
<Col>
|
|
824
|
-
<
|
|
825
|
-
|
|
634
|
+
<Style>{`.nre .x { color: red; }`}</Style>
|
|
635
|
+
<Text>visible</Text>
|
|
636
|
+
</Col>,
|
|
826
637
|
);
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
expect(
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
describe("defineComponent", () => {
|
|
834
|
-
it("faces 两键必填,少一个面在运行时也拦住(编译期本就不过)", () => {
|
|
835
|
-
// @ts-expect-error 缺 text 面编译不过;运行时同样报错
|
|
836
|
-
expect(() => defineComponent({ web: () => null })).toThrow(/requires both faces/);
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
// custom-reports.mdx「换形态」的 PassBars 示例,逐字同源:text 面用公开的文本排版工具箱
|
|
840
|
-
// (stringWidth / padEnd / bar),不用 String.prototype.padEnd —— 中文名的那一行是护栏。
|
|
841
|
-
it("自定义双面组件:同一份数据两面判读一致,text 面拿到宽度;中文行不撕歪", () => {
|
|
842
|
-
interface BarRow {
|
|
843
|
-
key: string;
|
|
844
|
-
ratio: number | null;
|
|
845
|
-
display: string;
|
|
846
|
-
}
|
|
847
|
-
const PassBars = defineComponent<{ rows: BarRow[] }>({
|
|
848
|
-
web({ rows }) {
|
|
849
|
-
return (
|
|
850
|
-
<ul className="passbars">
|
|
851
|
-
{rows.map((r) => (
|
|
852
|
-
<li key={r.key}>
|
|
853
|
-
<span>{r.key}</span>
|
|
854
|
-
<b>{r.ratio === null ? "—" : r.display}</b>
|
|
855
|
-
</li>
|
|
856
|
-
))}
|
|
857
|
-
</ul>
|
|
858
|
-
);
|
|
859
|
-
},
|
|
860
|
-
text({ rows }, { width }) {
|
|
861
|
-
expect(width).toBe(80);
|
|
862
|
-
const label = Math.max(...rows.map((r) => stringWidth(r.key)));
|
|
863
|
-
const barWidth = Math.min(20, width - label - 8);
|
|
864
|
-
return rows
|
|
865
|
-
.map((r) => {
|
|
866
|
-
const chart = r.ratio === null ? padEnd("—", barWidth) : bar(r.ratio, barWidth);
|
|
867
|
-
return `${padEnd(r.key, label)} ${chart} ${r.display}`;
|
|
868
|
-
})
|
|
869
|
-
.join("\n");
|
|
870
|
-
},
|
|
871
|
-
});
|
|
872
|
-
const rows: BarRow[] = [
|
|
873
|
-
{ key: "bub", ratio: 0.87, display: "87%" },
|
|
874
|
-
{ key: "codex", ratio: 0.8, display: "80%" },
|
|
875
|
-
{ key: "克劳德", ratio: null, display: "—" },
|
|
876
|
-
];
|
|
877
|
-
const term = text(<PassBars rows={rows} />);
|
|
878
|
-
expect(term).toMatchInlineSnapshot(`
|
|
879
|
-
"bub █████████████████░░░ 87%
|
|
880
|
-
codex ████████████████░░░░ 80%
|
|
881
|
-
克劳德 — —"
|
|
882
|
-
`);
|
|
883
|
-
// 三行的条形都从同一显示列开始:"克劳德" 是 3 个码元、6 个显示列,
|
|
884
|
-
// 拿 .padEnd(label) 补齐会把这一行整体左移 3 列
|
|
885
|
-
const starts = term.split("\n").map((line, i) => stringWidth(line.slice(0, line.indexOf(["█", "█", "—"][i]))));
|
|
886
|
-
expect(new Set(starts).size).toBe(1);
|
|
887
|
-
|
|
888
|
-
const html = renderToStaticMarkup(<PassBars rows={rows} />);
|
|
889
|
-
expect(html).toContain("87%");
|
|
890
|
-
expect(html).toContain("—"); // 缺数据两面都是 —,不补 0
|
|
638
|
+
const hostCtx = { scope, results: resultsOf([]) };
|
|
639
|
+
const text = await renderReportToText(definition, hostCtx);
|
|
640
|
+
expect(text).toBe("visible");
|
|
641
|
+
const html = await renderReportToStaticHtml(definition, hostCtx);
|
|
642
|
+
expect(html).toContain("<style>");
|
|
891
643
|
});
|
|
892
644
|
});
|
|
893
645
|
|
|
894
|
-
// ─────────────────────────
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
evalId: id,
|
|
909
|
-
experimentId: "compare/bub",
|
|
910
|
-
result: {
|
|
911
|
-
id,
|
|
912
|
-
agent: "bub",
|
|
913
|
-
verdict,
|
|
914
|
-
attempt: 0,
|
|
915
|
-
startedAt: `2026-07-01T10:0${index}:00Z`,
|
|
916
|
-
durationMs: 1000,
|
|
917
|
-
assertions: [],
|
|
918
|
-
},
|
|
919
|
-
ref: { snapshot: "compare_bub/snap-1", attempt: `${id}/a0` },
|
|
920
|
-
snapshot: base,
|
|
921
|
-
events: async () => null,
|
|
922
|
-
trace: async () => null,
|
|
923
|
-
o11y: async () => null,
|
|
924
|
-
diff: async () => null,
|
|
925
|
-
sources: async () => null,
|
|
926
|
-
});
|
|
927
|
-
const attempts = [mk("algebra/x", "passed", 0), mk("algebra/y", "failed", 1)];
|
|
928
|
-
return {
|
|
929
|
-
...base,
|
|
930
|
-
evals: attempts.map((a) => ({ id: a.evalId, attempts: [a] })),
|
|
931
|
-
attempts,
|
|
932
|
-
} as unknown as Snapshot;
|
|
933
|
-
})();
|
|
934
|
-
const selection: Selection = {
|
|
935
|
-
snapshots: [snapshot],
|
|
936
|
-
warnings: [],
|
|
937
|
-
filter: () => selection,
|
|
938
|
-
};
|
|
939
|
-
const results = {
|
|
940
|
-
experiments: [{ id: "compare/bub", snapshots: [snapshot], latest: snapshot, evalIds: ["algebra/x", "algebra/y"] }],
|
|
941
|
-
skipped: [],
|
|
942
|
-
latest: () => selection,
|
|
943
|
-
} as Results;
|
|
944
|
-
return { selection, results };
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/**
|
|
948
|
-
* 五份快照、两个双配置目录组 + 一个根目录单例组,全部通过、无成本——
|
|
949
|
-
* 一个多实验 Selection 夹具:ExperimentList 出五项,MetricScatter 五个点都无成本
|
|
950
|
-
* (0 可画点,如实走空态)。`filter` 落实真实语义(不像 fakeContext 那样恒等返回自己)。
|
|
951
|
-
*/
|
|
952
|
-
function fakeMultiGroupContext(): { selection: Selection; results: Results } {
|
|
953
|
-
const mkSnapshot = (experimentId: string, agent: string, dirSuffix: string): Snapshot => {
|
|
954
|
-
const dir = `/results/${dirSuffix}`;
|
|
955
|
-
const base = {
|
|
956
|
-
experimentId,
|
|
957
|
-
startedAt: "2026-07-01T10:00:00Z",
|
|
958
|
-
completedAt: "2026-07-01T10:05:00Z",
|
|
959
|
-
agent,
|
|
960
|
-
schemaVersion: 1,
|
|
961
|
-
dir,
|
|
962
|
-
};
|
|
963
|
-
const attempt = {
|
|
964
|
-
evalId: "algebra/x",
|
|
965
|
-
experimentId,
|
|
966
|
-
result: {
|
|
967
|
-
id: "algebra/x",
|
|
968
|
-
agent,
|
|
969
|
-
verdict: "passed" as const,
|
|
970
|
-
attempt: 0,
|
|
971
|
-
startedAt: "2026-07-01T10:00:00Z",
|
|
972
|
-
durationMs: 1000,
|
|
973
|
-
assertions: [],
|
|
974
|
-
},
|
|
975
|
-
ref: { snapshot: dirSuffix, attempt: "algebra/x/a0" },
|
|
976
|
-
snapshot: base,
|
|
977
|
-
events: async () => null,
|
|
978
|
-
trace: async () => null,
|
|
979
|
-
o11y: async () => null,
|
|
980
|
-
diff: async () => null,
|
|
981
|
-
sources: async () => null,
|
|
982
|
-
};
|
|
983
|
-
return {
|
|
984
|
-
...base,
|
|
985
|
-
evals: [{ id: attempt.evalId, attempts: [attempt] }],
|
|
986
|
-
attempts: [attempt],
|
|
987
|
-
} as unknown as Snapshot;
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
const snapshots = [
|
|
991
|
-
mkSnapshot("compare/bub", "bub", "compare_bub/snap-1"),
|
|
992
|
-
mkSnapshot("compare/codex", "codex", "compare_codex/snap-1"),
|
|
993
|
-
mkSnapshot("other/codex", "codex", "other_codex/snap-1"),
|
|
994
|
-
mkSnapshot("other/bub", "bub", "other_bub/snap-1"),
|
|
995
|
-
mkSnapshot("solo", "bub", "solo/snap-1"),
|
|
996
|
-
];
|
|
997
|
-
|
|
998
|
-
const makeSelection = (snaps: Snapshot[]): Selection => ({
|
|
999
|
-
snapshots: snaps,
|
|
1000
|
-
warnings: [],
|
|
1001
|
-
filter: (predicate) => makeSelection(snaps.filter(predicate)),
|
|
1002
|
-
});
|
|
1003
|
-
const selection = makeSelection(snapshots);
|
|
1004
|
-
const results = {
|
|
1005
|
-
experiments: snapshots.map((s) => ({
|
|
1006
|
-
id: s.experimentId,
|
|
1007
|
-
snapshots: [s],
|
|
1008
|
-
latest: s,
|
|
1009
|
-
evalIds: ["algebra/x"],
|
|
1010
|
-
})),
|
|
1011
|
-
skipped: [],
|
|
1012
|
-
latest: () => selection,
|
|
1013
|
-
} as Results;
|
|
1014
|
-
return { selection, results };
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
describe("defineReport + 渲染入口", () => {
|
|
1018
|
-
// ExperimentList 没有 selection-form:build() 里直接 await .data(selection) 拿到普通数组
|
|
1019
|
-
// 再传 items;RunOverview / Scoreboard 同样是预计算的 data 形态。三者都在 build() 里
|
|
1020
|
-
// await,一棵树验证同一个 renderReportToText/renderReportToStaticHtml 管线消费得动它们。
|
|
1021
|
-
const report = defineReport(async ({ selection }) => {
|
|
1022
|
-
const experiments = await ExperimentList.data(selection);
|
|
1023
|
-
return (
|
|
1024
|
-
<Col>
|
|
1025
|
-
<RunOverview data={await RunOverview.data(selection)} />
|
|
1026
|
-
<ExperimentList items={experiments} />
|
|
1027
|
-
<Section title="考试成绩单">
|
|
1028
|
-
<Scoreboard data={await Scoreboard.data(selection, { rows: "agent", subjects: "evalGroup" })} />
|
|
1029
|
-
</Section>
|
|
1030
|
-
</Col>
|
|
646
|
+
// ───────────────────────── Tabs ─────────────────────────
|
|
647
|
+
|
|
648
|
+
describe("Tabs", () => {
|
|
649
|
+
it("两面都输出全部 tab 完整内容:web 每 tab 一个 <details> 且仅首个 open;text 按声明序分节不省略", async () => {
|
|
650
|
+
const scope = scopeOf([]);
|
|
651
|
+
const definition = defineReport(
|
|
652
|
+
<Tabs>
|
|
653
|
+
<Tab title="First">
|
|
654
|
+
<Text>alpha body</Text>
|
|
655
|
+
</Tab>
|
|
656
|
+
<Tab title="Second">
|
|
657
|
+
<Text>beta body</Text>
|
|
658
|
+
</Tab>
|
|
659
|
+
</Tabs>,
|
|
1031
660
|
);
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
expect(
|
|
1036
|
-
expect(
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
expect(
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
expect(out).toContain("50/100");
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
it("renderReportToStaticHtml:同一棵树走 web 面,ExperimentList 自动接证据室深链", async () => {
|
|
1057
|
-
const html = await renderReportToStaticHtml(report, fakeContext());
|
|
1058
|
-
expect(html).toContain("nre-experiment-list");
|
|
1059
|
-
expect(html).toContain("nre-scoreboard");
|
|
1060
|
-
expect(html).toContain("考试成绩单");
|
|
1061
|
-
// 宿主注入的 attemptHref:ExperimentList 的 locator 徽标是普通 <a>,默认 view 路由(单段 locator)
|
|
1062
|
-
expect(html).toMatch(/href="#\/attempt\/@[0-9a-z]+"/);
|
|
1063
|
-
expect(html).not.toContain("<script");
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
it("RunOverview 已渲染 Selection warning 时,宿主不会在 text/web 顶部重复一遍", async () => {
|
|
1067
|
-
const context = fakeContext();
|
|
1068
|
-
const message = "snapshot is unfinished; completed attempts may be incomplete";
|
|
1069
|
-
context.selection.warnings.push({
|
|
1070
|
-
kind: "unfinished-snapshot",
|
|
1071
|
-
experimentId: "compare/bub",
|
|
1072
|
-
startedAt: "2026-07-01T10:00:00Z",
|
|
1073
|
-
dir: "/results/compare_bub/snap-1",
|
|
1074
|
-
message,
|
|
1075
|
-
});
|
|
1076
|
-
const textOut = await renderReportToText(report, context, { width: 100 });
|
|
1077
|
-
const htmlOut = await renderReportToStaticHtml(report, context);
|
|
1078
|
-
expect(textOut.split(message)).toHaveLength(2);
|
|
1079
|
-
expect(htmlOut.split(message)).toHaveLength(2);
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
it("树里混进 <div>:两个宿主 resolve 后同一遍校验拦住", async () => {
|
|
1083
|
-
const bad = defineReport(() => (
|
|
1084
|
-
<Col>
|
|
1085
|
-
<div>nope</div>
|
|
1086
|
-
</Col>
|
|
1087
|
-
));
|
|
1088
|
-
await expect(renderReportToText(bad, fakeContext())).rejects.toThrow(/Raw HTML <div>/);
|
|
1089
|
-
await expect(renderReportToStaticHtml(bad, fakeContext())).rejects.toThrow(/Raw HTML <div>/);
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
it("selection 形态组件裸嵌进 React(不经宿主 resolve):web 面直说未解析", () => {
|
|
1093
|
-
const { selection } = fakeContext();
|
|
1094
|
-
// selection 形态在类型上合法,但裸调用路径(不过 resolveReportTree)只接收 data 形态,
|
|
1095
|
-
// web 面拿不到 data —— 运行时直说,而不是画一张空组件。MetricScatter 是本文件里仍带
|
|
1096
|
-
// selection-form 的官方组件(ExperimentList/EvalList/AttemptList 没有 selection-form,
|
|
1097
|
-
// 这条契约不适用于它们)。
|
|
661
|
+
const hostCtx = { scope, results: resultsOf([]) };
|
|
662
|
+
const html = await renderReportToStaticHtml(definition, hostCtx);
|
|
663
|
+
expect(html.match(/<details/g)).toHaveLength(2);
|
|
664
|
+
expect(html.match(/<details[^>]* open/g)).toHaveLength(1);
|
|
665
|
+
expect(html).toContain("alpha body");
|
|
666
|
+
expect(html).toContain("beta body");
|
|
667
|
+
const text = await renderReportToText(definition, hostCtx);
|
|
668
|
+
expect(text.indexOf("First")).toBeLessThan(text.indexOf("Second"));
|
|
669
|
+
expect(text).toContain("alpha body");
|
|
670
|
+
expect(text).toContain("beta body"); // 不丢第二个 tab
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("空 Tabs、普通组件混作直接子节点、游离 Tab 都在树校验期给出完整用户反馈", () => {
|
|
674
|
+
expect(() => validateReportTree(<Tabs>{null}</Tabs>)).toThrow(/at least one <Tab>/);
|
|
675
|
+
expect(() =>
|
|
676
|
+
validateReportTree(
|
|
677
|
+
<Tabs>
|
|
678
|
+
<Text>stray</Text>
|
|
679
|
+
</Tabs>,
|
|
680
|
+
),
|
|
681
|
+
).toThrow(/only accepts <Tab>/);
|
|
1098
682
|
expect(() =>
|
|
1099
|
-
|
|
1100
|
-
|
|
683
|
+
validateReportTree(
|
|
684
|
+
<Col>
|
|
685
|
+
<Tab title="loose">
|
|
686
|
+
<Text>x</Text>
|
|
687
|
+
</Tab>
|
|
688
|
+
</Col>,
|
|
689
|
+
),
|
|
690
|
+
).toThrow(/direct child of <Tabs>/);
|
|
1101
691
|
});
|
|
1102
692
|
});
|
|
1103
693
|
|
|
1104
|
-
// ─────────────────────────
|
|
1105
|
-
// 只在 pnpm run typecheck 生效(src 纳入 tsconfig);函数从不执行,仅让 tsc 校验每条
|
|
1106
|
-
// ts-expect-error 注释对应一处编译错误。直接标注 Props 类型(而非 JSX)——JSX 的 union
|
|
1107
|
-
// 属性检查对「缺必填字段」较宽松,直接的类型赋值才严格钉住互斥不变量:同时传 data 与
|
|
1108
|
-
// selection、或两者都不传、或 selection 形态缺必填计算选项,全部编译失败。正向 JSX 用法由
|
|
1109
|
-
// ExperimentComparison / defineReport 报告在同一文件里编译验证。
|
|
1110
|
-
function metricScatterPropsTypeChecks(selection: Selection, data: ScatterData): void {
|
|
1111
|
-
const ok1: MetricScatterProps = { data }; // 合法:data 形态
|
|
1112
|
-
const ok2: MetricScatterProps = { selection, points: "experiment", series: "agent", x: costUSD, y: taskPassRate }; // 合法:selection 形态
|
|
1113
|
-
// @ts-expect-error 同时传 data 与 selection:非法
|
|
1114
|
-
const bad1: MetricScatterProps = { data, selection, points: "experiment", x: costUSD, y: taskPassRate };
|
|
1115
|
-
// @ts-expect-error data 与 selection 都不传:非法
|
|
1116
|
-
const bad2: MetricScatterProps = { pointHref: () => "/x" };
|
|
1117
|
-
// @ts-expect-error selection 形态缺必填的 x / y:非法
|
|
1118
|
-
const bad3: MetricScatterProps = { selection, points: "experiment" };
|
|
1119
|
-
void ok1;
|
|
1120
|
-
void ok2;
|
|
1121
|
-
void bad1;
|
|
1122
|
-
void bad2;
|
|
1123
|
-
void bad3;
|
|
1124
|
-
}
|
|
694
|
+
// ───────────────────────── defineReport 装载规范化 ─────────────────────────
|
|
1125
695
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
expect(
|
|
1141
|
-
expect(out).not.toContain("compare/bub");
|
|
1142
|
-
expect(out).toMatch(/1 passed[\s\S]*?\/ 1\s+failed/);
|
|
1143
|
-
expect(out).toContain("50%");
|
|
1144
|
-
expect(out).toMatch(/✗ failed\s+algebra\/y[\s\S]*└─ @[0-9a-z]+/);
|
|
1145
|
-
// 单组直接进入详情,不出现多组查看命令
|
|
1146
|
-
expect(out).not.toContain("Current verdicts");
|
|
1147
|
-
expect(out).not.toContain("niceeval show --experiment");
|
|
696
|
+
describe("defineReport 装载规范化", () => {
|
|
697
|
+
it("三种写法装载出等价的规范化结果:树 ≡ {content} ≡ pages [{id: report}]", () => {
|
|
698
|
+
const tree = <ScopeSummary />;
|
|
699
|
+
const fromTree = defineReport(tree);
|
|
700
|
+
const fromContent = defineReport({ content: tree });
|
|
701
|
+
const fromPages = defineReport({
|
|
702
|
+
pages: [{ id: "report", title: { en: "Report", "zh-CN": "报告" }, content: tree }],
|
|
703
|
+
});
|
|
704
|
+
for (const definition of [fromTree, fromContent, fromPages]) {
|
|
705
|
+
expect(definition.kind).toBe("report");
|
|
706
|
+
expect(definition.pages).toHaveLength(1);
|
|
707
|
+
expect(definition.pages[0]!.id).toBe("report");
|
|
708
|
+
expect(definition.pages[0]!.content).toBe(tree);
|
|
709
|
+
}
|
|
710
|
+
expect(fromTree.pages[0]!.title).toEqual(fromContent.pages[0]!.title);
|
|
1148
711
|
});
|
|
1149
712
|
|
|
1150
|
-
it("
|
|
1151
|
-
|
|
1152
|
-
expect(
|
|
1153
|
-
expect(html).toMatch(/<details[^>]*data-nre-experiment-group-panel="0"[^>]*\sopen=""/);
|
|
1154
|
-
expect(html).toContain("nre-metric-scatter");
|
|
1155
|
-
expect(html).toContain("nre-scatter-empty"); // 0 可画点的空态
|
|
1156
|
-
expect(html).toContain('<details class="nre-experiment-entry">');
|
|
1157
|
-
// 行标签去掉组前缀只显示 id 末段;完整 id 仍留在 data-sort-value 作排序/身份键
|
|
1158
|
-
expect(html).toMatch(/nre-experiment-id[^>]*>\s*bub\s*<\/b>/);
|
|
1159
|
-
expect(html).toContain('data-sort-value="compare/bub"');
|
|
1160
|
-
expect(html).toContain("nre-experiment-head");
|
|
1161
|
-
expect(html).toContain('data-nre-experiment-filter=""');
|
|
1162
|
-
expect(html).toContain("nre-experiment-evals");
|
|
1163
|
-
expect(html).not.toContain("<script");
|
|
713
|
+
it("content 与 pages 同时声明或都省略,装载报错且文案含 <ExperimentComparison /> 下一步", () => {
|
|
714
|
+
expect(() => defineReport({ content: <ScopeSummary />, pages: [] } as never)).toThrow(/both "content" and "pages"/);
|
|
715
|
+
expect(() => defineReport({ title: "X" } as never)).toThrow(/<ExperimentComparison \/>/);
|
|
1164
716
|
});
|
|
1165
717
|
|
|
1166
|
-
it("
|
|
1167
|
-
const
|
|
1168
|
-
expect(
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
expect(
|
|
1173
|
-
const enText = await renderReportToText(ExperimentComparison, fakeContext(), { locale: "en" });
|
|
1174
|
-
expect(enText).toContain("No data to plot");
|
|
718
|
+
it("defineReport 产物不是 ReportNode:页里放产物装载报错;树校验同样拒绝", () => {
|
|
719
|
+
const inner = defineReport(<ScopeSummary />);
|
|
720
|
+
expect(() => defineReport({ pages: [{ id: "a", title: "A", content: inner as never }] })).toThrow(
|
|
721
|
+
/shell cannot nest/,
|
|
722
|
+
);
|
|
723
|
+
expect(() => defineReport(inner as never)).toThrow(/shell cannot nest/);
|
|
724
|
+
expect(() => validateReportTree([inner] as never)).toThrow(/not a report node/);
|
|
1175
725
|
});
|
|
1176
726
|
|
|
1177
|
-
it("
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
expect(
|
|
1187
|
-
expect(
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
727
|
+
it("重复或非法 page id 装载报错并点名冲突;LocalizedText 全空对象报错", () => {
|
|
728
|
+
expect(() =>
|
|
729
|
+
defineReport({
|
|
730
|
+
pages: [
|
|
731
|
+
{ id: "exam", title: "A", content: null },
|
|
732
|
+
{ id: "exam", title: "B", content: null },
|
|
733
|
+
],
|
|
734
|
+
}),
|
|
735
|
+
).toThrow(/"exam" is declared twice/);
|
|
736
|
+
expect(() => defineReport({ pages: [{ id: "Bad/Id", title: "A", content: null }] })).toThrow(/invalid/);
|
|
737
|
+
expect(() => defineReport({ title: {}, content: null })).toThrow(/no non-empty value/);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("{src} 资产拒绝 .. 路径段、绝对路径与 ~;inline/src 互斥", () => {
|
|
741
|
+
expect(() => defineReport({ content: null, scripts: [{ src: "../x.js" }] })).toThrow(/not allowed/);
|
|
742
|
+
expect(() => defineReport({ content: null, scripts: [{ src: "/abs.js" }] })).toThrow(/not allowed/);
|
|
743
|
+
expect(() => defineReport({ content: null, styles: [{ src: "~/x.css" }] })).toThrow(/not allowed/);
|
|
744
|
+
expect(() => defineReport({ content: null, scripts: [{ src: "./a.js", inline: "x" } as never] })).toThrow(
|
|
745
|
+
/exactly one/,
|
|
746
|
+
);
|
|
747
|
+
expect(() => defineReport({ content: null, scripts: [{ src: "./assets/a.js" }] })).not.toThrow();
|
|
1191
748
|
});
|
|
1192
749
|
|
|
1193
|
-
it("
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
750
|
+
it("--page 语义:pickReportPage 缺省第一页,未命中抛 ReportPageNotFoundError 列出可用页", () => {
|
|
751
|
+
const definition = defineReport({
|
|
752
|
+
pages: [
|
|
753
|
+
{ id: "overview", title: "Overview", content: null },
|
|
754
|
+
{ id: "exam", title: "Exam", content: null },
|
|
755
|
+
],
|
|
756
|
+
});
|
|
757
|
+
expect(pickReportPage(definition).id).toBe("overview");
|
|
758
|
+
expect(pickReportPage(definition, "exam").id).toBe("exam");
|
|
759
|
+
try {
|
|
760
|
+
pickReportPage(definition, "typo");
|
|
761
|
+
expect.unreachable();
|
|
762
|
+
} catch (e) {
|
|
763
|
+
expect(e).toBeInstanceOf(ReportPageNotFoundError);
|
|
764
|
+
expect((e as ReportPageNotFoundError).available).toEqual(["overview", "exam"]);
|
|
765
|
+
}
|
|
766
|
+
// 树形态文件的唯一页 id 是缩写展开出的 report
|
|
767
|
+
const single = defineReport(<ScopeSummary />);
|
|
768
|
+
expect(pickReportPage(single, "report").id).toBe("report");
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it("标题回退链:def.title → 唯一且相同的快照 name → 内置文案「Eval 运行结果 / Eval Results」;en 相同 zh 不同也落内置文案", () => {
|
|
772
|
+
const named = snap({ experimentId: "t/a", name: "Memory Evals", results: [res("q", "passed")] });
|
|
773
|
+
const definition = defineReport(<ScopeSummary />);
|
|
774
|
+
expect(resolveReportTitle(defineReport({ title: "Custom", content: null }), scopeOf([named]))).toBe("Custom");
|
|
775
|
+
expect(resolveReportTitle(definition, scopeOf([named]))).toBe("Memory Evals");
|
|
776
|
+
expect(resolveReportTitle(definition, scopeOf([snap({ experimentId: "t/b", results: [] })]))).toEqual(
|
|
777
|
+
FALLBACK_REPORT_TITLE,
|
|
1201
778
|
);
|
|
1202
|
-
|
|
779
|
+
const zhA = snap({ experimentId: "t/c", name: { en: "Same", "zh-CN": "一" }, results: [] });
|
|
780
|
+
const zhB = snap({ experimentId: "t/d", name: { en: "Same", "zh-CN": "二" }, results: [] });
|
|
781
|
+
expect(resolveReportTitle(definition, scopeOf([zhA, zhB]))).toEqual(FALLBACK_REPORT_TITLE);
|
|
782
|
+
expect(FALLBACK_REPORT_TITLE).toEqual({ en: "Eval Results", "zh-CN": "Eval 运行结果" });
|
|
1203
783
|
});
|
|
1204
784
|
|
|
1205
|
-
it(".
|
|
1206
|
-
const
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
points: "experiment",
|
|
1220
|
-
series: "agent",
|
|
1221
|
-
x: costUSD,
|
|
1222
|
-
y: endToEndPassRate,
|
|
785
|
+
it("ReportLink.icon 是 { svg: string }:defineReport 接受合法形状;无类型 JS 传其它形状定义时报完整用户反馈", () => {
|
|
786
|
+
const svg = '<svg viewBox="0 0 16 16"><path d="M0 0h16v16z"/></svg>';
|
|
787
|
+
const withIcon = defineReport({
|
|
788
|
+
content: null,
|
|
789
|
+
links: [{ label: "GitHub", href: "https://example.com", icon: { svg } }],
|
|
790
|
+
});
|
|
791
|
+
expect(withIcon.links[0]!.icon).toEqual({ svg });
|
|
792
|
+
|
|
793
|
+
const reactNode = { $$typeof: Symbol.for("react.transitional.element"), type: "svg", props: {} };
|
|
794
|
+
for (const icon of [reactNode, "<svg/>", { svg: 42 }, { svg: "" }]) {
|
|
795
|
+
expect(() =>
|
|
796
|
+
defineReport({
|
|
797
|
+
content: null,
|
|
798
|
+
links: [{ label: "GitHub", href: "https://example.com", icon: icon as never }],
|
|
1223
799
|
}),
|
|
1224
|
-
);
|
|
1225
|
-
expect(group.experiments).toEqual(await ExperimentList.data(groupSelection));
|
|
1226
|
-
expect(group.scatter.rows.every((row) => group.experiments.some((item) => item.experimentId === row.key))).toBe(
|
|
1227
|
-
true,
|
|
1228
|
-
);
|
|
800
|
+
).toThrow(/"icon" must be \{ svg: string \}/);
|
|
1229
801
|
}
|
|
1230
|
-
|
|
1231
|
-
expect(data.groups.find((group) => group.key === "compare")!.experiments.map((item) => item.experimentId)).toEqual([
|
|
1232
|
-
"compare/bub",
|
|
1233
|
-
"compare/codex",
|
|
1234
|
-
]);
|
|
1235
|
-
expect(data.groups.find((group) => group.key === "other")!.experiments.map((item) => item.experimentId)).toEqual([
|
|
1236
|
-
"other/bub",
|
|
1237
|
-
"other/codex",
|
|
1238
|
-
]);
|
|
1239
|
-
expect(data.groups.find((group) => group.key === "solo")!.experiments.map((item) => item.experimentId)).toEqual([
|
|
1240
|
-
"solo",
|
|
1241
|
-
]);
|
|
1242
802
|
});
|
|
803
|
+
});
|
|
1243
804
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const
|
|
1253
|
-
const
|
|
1254
|
-
|
|
805
|
+
// ───────────────────────── 内建报告 ─────────────────────────
|
|
806
|
+
|
|
807
|
+
describe("内建报告", () => {
|
|
808
|
+
it("niceeval/report/built-in 默认导出是普通 defineReport 产物,与用户写的 defineReport(<ExperimentComparison />) 等价", async () => {
|
|
809
|
+
expect(builtInReport.kind).toBe("report");
|
|
810
|
+
expect(builtInReport.pages).toHaveLength(1);
|
|
811
|
+
|
|
812
|
+
const s1 = snap({ experimentId: "compare/a", agent: "bub", results: [res("q", "passed")] });
|
|
813
|
+
const s2 = snap({ experimentId: "compare/b", agent: "codex", results: [res("q", "failed")] });
|
|
814
|
+
const scope = scopeOf([s1, s2]);
|
|
815
|
+
const ctx = { scope, results: resultsOf([s1, s2]) };
|
|
816
|
+
const builtin = await renderReportToText(builtInReport, ctx, { width: 120 });
|
|
817
|
+
const user = await renderReportToText(defineReport(<ExperimentComparison />), ctx, { width: 120 });
|
|
818
|
+
expect(builtin).toBe(user);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("text 面多组时只输出组索引与单组查看命令;单组时输出散点与实验列表", async () => {
|
|
822
|
+
const g1 = snap({ experimentId: "compare/a", results: [res("q", "passed")] });
|
|
823
|
+
const g2 = snap({ experimentId: "dev/b", results: [res("q", "failed")] });
|
|
824
|
+
const multi = await renderReportToText(
|
|
825
|
+
builtInReport,
|
|
826
|
+
{ scope: scopeOf([g1, g2]), results: resultsOf([g1, g2]) },
|
|
827
|
+
{ width: 120 },
|
|
828
|
+
);
|
|
829
|
+
expect(multi).toContain("niceeval show --experiment compare");
|
|
830
|
+
expect(multi).toContain("niceeval show --experiment dev");
|
|
831
|
+
expect(multi).not.toContain("Eval / Attempt"); // 不倾倒组内 experiment 明细
|
|
832
|
+
|
|
833
|
+
const priced = snap({
|
|
834
|
+
experimentId: "compare/priced",
|
|
835
|
+
results: [res("q", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } })],
|
|
836
|
+
});
|
|
837
|
+
const single = await renderReportToText(
|
|
838
|
+
builtInReport,
|
|
839
|
+
{ scope: scopeOf([priced]), results: resultsOf([priced]) },
|
|
840
|
+
{ width: 140 },
|
|
841
|
+
);
|
|
842
|
+
expect(single).toContain("Eval / Attempt"); // 单组直接进入组内详情
|
|
843
|
+
// 成本轴(better: lower)反向渲染,「更好」恒指向右上;两轴都声明 better → 提示在场
|
|
844
|
+
expect(single).toContain("better → upper right");
|
|
1255
845
|
});
|
|
1256
846
|
});
|