niceeval 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +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/lib/artifact-url.ts +14 -3
- 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/artifact-serving.test.ts +21 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +6 -6
- 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,597 +0,0 @@
|
|
|
1
|
-
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
2
|
-
// Phase 4 验收:证明内置报告 ExperimentComparison 与一份普通用户 --report 文件走的是
|
|
3
|
-
// 完全同一套机制 —— 不是长得像,而是同一条 build → resolveReportTree → validate → render 管线。
|
|
4
|
-
//
|
|
5
|
-
// 证据链(plan/built-in-reports-user-parity.md「Phase 4」):
|
|
6
|
-
// 1. test/fixtures/report/experiment-comparison-public-copy.tsx 只从公开 barrel import,
|
|
7
|
-
// 与 built-in 逐节点同构(见文件头)。
|
|
8
|
-
// 2. 同一 ReportContext 下分别 build + resolveReportTree,深比较 resolved 树:组件类型 / 顺序 /
|
|
9
|
-
// resolved data / props keys 全部结构化相等(不靠 definition 引用相等,两份是独立文件)。
|
|
10
|
-
// 3. 两份分别过 renderReportToText / renderReportToStaticHtml,比较事实(散点键 / 表格行 /
|
|
11
|
-
// attempt 深链),不做 HTML 字面 snapshot。
|
|
12
|
-
// 4. 覆盖 plan 的 9 个必测场景。
|
|
13
|
-
// 5. 架构不变量:renderer 模块的 import 依赖图不含 built-ins。
|
|
14
|
-
// (show / view 的宿主选择与装载边界等价由 src/show/show.test.ts、src/view/view-report.test.ts 覆盖。)
|
|
15
|
-
|
|
16
|
-
import { readFileSync } from "node:fs";
|
|
17
|
-
import { renderToStaticMarkup } from "react-dom/server";
|
|
18
|
-
import { describe, expect, it } from "vitest";
|
|
19
|
-
|
|
20
|
-
import type { EvalResult, Verdict } from "../types.ts";
|
|
21
|
-
import type { AttemptHandle, Results, Selection, Snapshot } from "../results/index.ts";
|
|
22
|
-
import type { ExperimentListItem, ScatterData } from "./types.ts";
|
|
23
|
-
import type { ExperimentComparisonData } from "./built-ins/index.ts";
|
|
24
|
-
import { encodeAttemptLocator } from "../results/locator.ts";
|
|
25
|
-
import { Col, ExperimentList, defineReport } from "./index.ts";
|
|
26
|
-
import { ExperimentComparison } from "./built-ins/index.ts";
|
|
27
|
-
import publicCopy from "../../test/fixtures/report/experiment-comparison-public-copy.tsx";
|
|
28
|
-
import { renderReportToText } from "./report.ts";
|
|
29
|
-
import { renderReportToStaticHtml } from "./web.ts";
|
|
30
|
-
import {
|
|
31
|
-
createTextContext,
|
|
32
|
-
renderNodeToText,
|
|
33
|
-
resolveReportTree,
|
|
34
|
-
runWithWebContext,
|
|
35
|
-
validateReportTree,
|
|
36
|
-
type ReportNode,
|
|
37
|
-
} from "./tree.ts";
|
|
38
|
-
|
|
39
|
-
// ───────────────────────── fake Selection / Snapshot(按 results 读取契约造)─────────────────────────
|
|
40
|
-
|
|
41
|
-
// 夹具刻意做成纯确定性:同一 spec 每次都产出逐字节相同的 Snapshot —— 每个场景的 ctx() 被
|
|
42
|
-
// built-in / public-copy 分别调用一次,只有两份快照的身份元组(experimentId / snapshot
|
|
43
|
-
// startedAt / evalId / attempt)完全一致,locatorOf() 兜底算出的 AttemptLocator 才会相同,
|
|
44
|
-
// 跨 definition 的 attempt 深链集合与 resolved data 才可能相等。所以不用任何全局自增计数器。
|
|
45
|
-
function res(id: string, verdict: Verdict, extra: Partial<EvalResult> = {}): EvalResult {
|
|
46
|
-
return {
|
|
47
|
-
id,
|
|
48
|
-
agent: "agent-x",
|
|
49
|
-
verdict,
|
|
50
|
-
attempt: 0,
|
|
51
|
-
startedAt: "2026-07-01T10:00:00Z",
|
|
52
|
-
durationMs: 1000,
|
|
53
|
-
assertions: [],
|
|
54
|
-
...extra,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface SnapSpec {
|
|
59
|
-
experimentId: string;
|
|
60
|
-
agent: string;
|
|
61
|
-
model?: string;
|
|
62
|
-
results: EvalResult[];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function snap(spec: SnapSpec): Snapshot {
|
|
66
|
-
// 每个 experiment 在一次 Selection 里只出现一次,固定 snap-1 目录即可确定且不冲突。
|
|
67
|
-
const dirSuffix = `${spec.experimentId.replace(/\//g, "_")}/snap-1`;
|
|
68
|
-
const base = {
|
|
69
|
-
experimentId: spec.experimentId,
|
|
70
|
-
startedAt: "2026-07-01T10:00:00Z",
|
|
71
|
-
completedAt: "2026-07-01T10:00:30Z",
|
|
72
|
-
agent: spec.agent,
|
|
73
|
-
model: spec.model,
|
|
74
|
-
schemaVersion: 1,
|
|
75
|
-
dir: `/results/${dirSuffix}`,
|
|
76
|
-
};
|
|
77
|
-
const attempts: AttemptHandle[] = spec.results.map((r) => ({
|
|
78
|
-
evalId: r.id,
|
|
79
|
-
experimentId: spec.experimentId,
|
|
80
|
-
result: { ...r, agent: spec.agent },
|
|
81
|
-
ref: { snapshot: dirSuffix, attempt: `${r.id}/a${r.attempt}` },
|
|
82
|
-
snapshot: base as unknown as Snapshot,
|
|
83
|
-
events: async () => null,
|
|
84
|
-
trace: async () => null,
|
|
85
|
-
o11y: async () => null,
|
|
86
|
-
agentSetup: async () => null,
|
|
87
|
-
diff: async () => null,
|
|
88
|
-
sources: async () => null,
|
|
89
|
-
}));
|
|
90
|
-
const evals = new Map<string, AttemptHandle[]>();
|
|
91
|
-
for (const a of attempts) {
|
|
92
|
-
const list = evals.get(a.evalId);
|
|
93
|
-
if (list) list.push(a);
|
|
94
|
-
else evals.set(a.evalId, [a]);
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
...base,
|
|
98
|
-
evals: [...evals.entries()].map(([id, list]) => ({ id, attempts: list })),
|
|
99
|
-
attempts,
|
|
100
|
-
} as unknown as Snapshot;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function selection(snapshots: Snapshot[]): Selection {
|
|
104
|
-
const self: Selection = {
|
|
105
|
-
snapshots,
|
|
106
|
-
warnings: [],
|
|
107
|
-
filter: (predicate) => selection(snapshots.filter(predicate)),
|
|
108
|
-
};
|
|
109
|
-
return self;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function ctxOf(sel: Selection): { selection: Selection; results: Results } {
|
|
113
|
-
return { selection: sel, results: {} as Results };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── 场景夹具 ──
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 场景 1 + 2 + 5 + 6:三个 experiment、两个 agent。
|
|
120
|
-
* - compare/bub-low(bub):algebra/x 两轮(fail→pass,多轮 attempt)、algebra/y 通过,都有成本 → 可画点。
|
|
121
|
-
* - compare/codex-mid(codex):errored + skipped + passed 混合,有估算/实测成本 → 可画点。
|
|
122
|
-
* - solo/no-cost(bub):通过 + 失败,无任何成本 → scatter 缺 x(不可画),表格仍出行。
|
|
123
|
-
*/
|
|
124
|
-
function richContext() {
|
|
125
|
-
return ctxOf(
|
|
126
|
-
selection([
|
|
127
|
-
snap({
|
|
128
|
-
experimentId: "compare/bub-low",
|
|
129
|
-
agent: "bub",
|
|
130
|
-
model: "gpt-bub",
|
|
131
|
-
results: [
|
|
132
|
-
res("algebra/x", "failed", { attempt: 0, usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.1 } }),
|
|
133
|
-
res("algebra/x", "passed", { attempt: 1, usage: { inputTokens: 20, outputTokens: 10, costUSD: 0.2 } }),
|
|
134
|
-
res("algebra/y", "passed", { attempt: 0, usage: { inputTokens: 5, outputTokens: 5, costUSD: 0.05 } }),
|
|
135
|
-
],
|
|
136
|
-
}),
|
|
137
|
-
snap({
|
|
138
|
-
experimentId: "compare/codex-mid",
|
|
139
|
-
agent: "codex",
|
|
140
|
-
model: "gpt-codex",
|
|
141
|
-
results: [
|
|
142
|
-
res("algebra/x", "errored", {
|
|
143
|
-
error: { code: "unexpected-error", message: "adapter crashed", phase: "eval.run" },
|
|
144
|
-
estimatedCostUSD: 0.3,
|
|
145
|
-
}),
|
|
146
|
-
res("algebra/y", "skipped"),
|
|
147
|
-
res("algebra/z", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.15 } }),
|
|
148
|
-
],
|
|
149
|
-
}),
|
|
150
|
-
snap({
|
|
151
|
-
experimentId: "solo/no-cost",
|
|
152
|
-
agent: "bub",
|
|
153
|
-
results: [res("algebra/x", "passed"), res("algebra/y", "failed")],
|
|
154
|
-
}),
|
|
155
|
-
]),
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** 场景 3:恰好一个可画点(A 有成本 → 可画;B 无成本 → 不可画)。 */
|
|
160
|
-
function oneDrawableContext() {
|
|
161
|
-
return ctxOf(
|
|
162
|
-
selection([
|
|
163
|
-
snap({
|
|
164
|
-
experimentId: "only/priced",
|
|
165
|
-
agent: "bub",
|
|
166
|
-
results: [res("algebra/x", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } })],
|
|
167
|
-
}),
|
|
168
|
-
snap({
|
|
169
|
-
experimentId: "only/free",
|
|
170
|
-
agent: "codex",
|
|
171
|
-
results: [res("algebra/x", "passed")],
|
|
172
|
-
}),
|
|
173
|
-
]),
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** 场景 4:空 Selection —— 无任何快照,散点 0 可画点走空态,表格无行。 */
|
|
178
|
-
function emptyContext() {
|
|
179
|
-
return ctxOf(selection([]));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** 场景 4 变体:全部 skipped —— 指标全不可测(x/y 全 null),同样走散点空态。 */
|
|
183
|
-
function allSkippedContext() {
|
|
184
|
-
return ctxOf(
|
|
185
|
-
selection([
|
|
186
|
-
snap({ experimentId: "exp/a", agent: "bub", results: [res("algebra/x", "skipped"), res("algebra/y", "skipped")] }),
|
|
187
|
-
]),
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const SCENARIOS: { name: string; ctx: () => ReturnType<typeof ctxOf> }[] = [
|
|
192
|
-
{ name: "多 experiment/agent、成本与通过率都有值(≥2 可画点)", ctx: richContext },
|
|
193
|
-
{ name: "恰好一个可画点", ctx: oneDrawableContext },
|
|
194
|
-
{ name: "空 Selection", ctx: emptyContext },
|
|
195
|
-
{ name: "全部 skipped(指标全不可测)", ctx: allSkippedContext },
|
|
196
|
-
];
|
|
197
|
-
|
|
198
|
-
// ───────────────────────── 树遍历工具 ─────────────────────────
|
|
199
|
-
|
|
200
|
-
const FRAGMENT = Symbol.for("react.fragment");
|
|
201
|
-
|
|
202
|
-
function isEl(n: unknown): n is { type: unknown; props: Record<string, unknown> } {
|
|
203
|
-
return typeof n === "object" && n !== null && !Array.isArray(n) && "type" in n && "props" in n;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function labelOf(type: unknown): string {
|
|
207
|
-
if (typeof type === "string") return `<${type}>`;
|
|
208
|
-
if (type === FRAGMENT) return "<>";
|
|
209
|
-
if (typeof type === "function") {
|
|
210
|
-
const fn = type as { displayName?: string; name?: string };
|
|
211
|
-
return fn.displayName ?? fn.name ?? "anonymous";
|
|
212
|
-
}
|
|
213
|
-
return String(type);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** 把 resolved 树抹成可结构化比较的事实:组件标签 + props(去掉 children、函数值)+ children。 */
|
|
217
|
-
function factify(node: ReportNode): unknown {
|
|
218
|
-
if (Array.isArray(node)) return node.map(factify);
|
|
219
|
-
if (isEl(node)) {
|
|
220
|
-
const { children, ...rest } = node.props;
|
|
221
|
-
const props = JSON.parse(JSON.stringify(rest, (_k, v) => (typeof v === "function" ? "[fn]" : v)));
|
|
222
|
-
return {
|
|
223
|
-
component: labelOf(node.type),
|
|
224
|
-
props,
|
|
225
|
-
children: children === undefined ? null : factify(children as ReportNode),
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
return node ?? null;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** 前序遍历收集组件节点(标签 + 原始 props),用于按顺序 / 按类型取 resolved data。 */
|
|
232
|
-
function collect(node: ReportNode, acc: { label: string; props: Record<string, unknown> }[] = []) {
|
|
233
|
-
if (Array.isArray(node)) {
|
|
234
|
-
for (const c of node) collect(c, acc);
|
|
235
|
-
return acc;
|
|
236
|
-
}
|
|
237
|
-
if (isEl(node)) {
|
|
238
|
-
acc.push({ label: labelOf(node.type), props: node.props });
|
|
239
|
-
collect(node.props.children as ReportNode, acc);
|
|
240
|
-
}
|
|
241
|
-
return acc;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function resolvedTreeOf(def: { build: (c: never) => unknown }, ctx: ReturnType<typeof ctxOf>): Promise<ReportNode> {
|
|
245
|
-
const node = (await (def as { build: (c: unknown) => ReportNode | Promise<ReportNode> }).build(ctx)) as ReportNode;
|
|
246
|
-
return resolveReportTree(node);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function scatterOf(tree: ReportNode): ScatterData | undefined {
|
|
250
|
-
return collect(tree).find((c) => c.label === "MetricScatter")?.props.data as ScatterData | undefined;
|
|
251
|
-
}
|
|
252
|
-
/** ExperimentList 没有 selection-form,树里的 props.items 就是 `await ExperimentList.data(selection)` 的产物。 */
|
|
253
|
-
function experimentListOf(tree: ReportNode): ExperimentListItem[] | undefined {
|
|
254
|
-
return collect(tree).find((c) => c.label === "ExperimentList")?.props.items as ExperimentListItem[] | undefined;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function comparisonDataOf(tree: ReportNode): ExperimentComparisonData | undefined {
|
|
258
|
-
return collect(tree).find((c) => c.label === "ExperimentComparison")?.props.data as
|
|
259
|
-
| ExperimentComparisonData
|
|
260
|
-
| undefined;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** HTML 里所有 attempt 深链,排序后便于集合比较。 */
|
|
264
|
-
function hrefsOf(html: string): string[] {
|
|
265
|
-
return [...html.matchAll(/href="(#\/attempt\/[^"]+)"/g)].map((m) => m[1]).sort();
|
|
266
|
-
}
|
|
267
|
-
/** 散点 web 面每个点的 data-key。 */
|
|
268
|
-
function scatterKeysOf(html: string): string[] {
|
|
269
|
-
return [...html.matchAll(/data-key="([^"]+)"/g)].map((m) => m[1]).sort();
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* 这份夹具的 AttemptHandle 都不带 `.locator` 字段(snap() 手工构造,故意留白),
|
|
274
|
-
* report 侧的 locatorOf() 按身份元组兜底算一份——这里复算同一个身份元组,验证深链确实
|
|
275
|
-
* 指向 Selection 里真实存在的那个 attempt,而不是随便一个字符串。
|
|
276
|
-
*/
|
|
277
|
-
function expectedLocatorFor(attempt: AttemptHandle): string {
|
|
278
|
-
return encodeAttemptLocator({
|
|
279
|
-
experimentId: attempt.experimentId,
|
|
280
|
-
snapshotStartedAt: attempt.snapshot.startedAt,
|
|
281
|
-
evalId: attempt.evalId,
|
|
282
|
-
attempt: attempt.result.attempt,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// ═════════════════════════ 1. built-in 与 public-copy 逐节点同构 ═════════════════════════
|
|
287
|
-
|
|
288
|
-
describe("resolved 树结构化等价(built-in ≡ 包外 public-copy)", () => {
|
|
289
|
-
it("两份是独立 ReportDefinition,不是同一引用", () => {
|
|
290
|
-
expect(ExperimentComparison).not.toBe(publicCopy);
|
|
291
|
-
expect(typeof ExperimentComparison.build).toBe("function");
|
|
292
|
-
expect(typeof publicCopy.build).toBe("function");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
for (const { name, ctx } of SCENARIOS) {
|
|
296
|
-
it(`同一 ReportContext 下 resolved 树逐节点相等 — ${name}`, async () => {
|
|
297
|
-
const c = ctx();
|
|
298
|
-
const builtIn = await resolvedTreeOf(ExperimentComparison, c);
|
|
299
|
-
const user = await resolvedTreeOf(publicCopy, ctx()); // 各用一份等价 ctx,证明不依赖共享状态
|
|
300
|
-
|
|
301
|
-
// build 面只把预计算分组数据交给公开组合件,没有 renderer 私有节点。
|
|
302
|
-
const labels = collect(builtIn).map((n) => n.label);
|
|
303
|
-
expect(labels).toEqual(["ExperimentComparison"]);
|
|
304
|
-
expect(collect(user).map((n) => n.label)).toEqual(labels);
|
|
305
|
-
|
|
306
|
-
// 逐节点结构化相等:组件类型、顺序、resolved data、props keys(pointHref/filter/…)全一致。
|
|
307
|
-
expect(factify(user)).toEqual(factify(builtIn));
|
|
308
|
-
|
|
309
|
-
// resolved data 本身也逐字段相等(不只是树同构)。
|
|
310
|
-
expect(comparisonDataOf(user)).toEqual(comparisonDataOf(builtIn));
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// ═════════════════════════ 2. 9 个必测场景的 resolved 事实 ═════════════════════════
|
|
316
|
-
|
|
317
|
-
describe("必测场景:resolved data 事实", () => {
|
|
318
|
-
it("场景 1+2:多 experiment/agent,成本与通过率都有值;缺成本的点如实 x=null,实验列表仍出项", async () => {
|
|
319
|
-
const tree = await resolvedTreeOf(ExperimentComparison, richContext());
|
|
320
|
-
const data = comparisonDataOf(tree)!;
|
|
321
|
-
expect(data.groups.map((group) => group.key)).toEqual(["compare", "solo"]);
|
|
322
|
-
const compare = data.groups[0]!;
|
|
323
|
-
const soloGroup = data.groups[1]!;
|
|
324
|
-
const scatter = compare.scatter;
|
|
325
|
-
const experiments = [...compare.experiments, ...soloGroup.experiments];
|
|
326
|
-
|
|
327
|
-
// 散点:points=experiment、series=agent、x=cost、y=end-to-end-pass-rate
|
|
328
|
-
expect(scatter.points).toBe("experiment");
|
|
329
|
-
expect(scatter.series).toBe("agent");
|
|
330
|
-
expect(scatter.x.key).toBe("cost");
|
|
331
|
-
expect(scatter.y.key).toBe("end-to-end-pass-rate");
|
|
332
|
-
expect(scatter.rows.map((r) => r.key).sort()).toEqual(["compare/bub-low", "compare/codex-mid"]);
|
|
333
|
-
|
|
334
|
-
const bub = scatter.rows.find((r) => r.key === "compare/bub-low")!;
|
|
335
|
-
expect(bub.series).toBe("bub");
|
|
336
|
-
// 两级聚合:algebra/x=(0.1,0.2)→0.15、algebra/y=0.05 → across mean 0.1;pass (0,1)→0.5、1 → 0.75
|
|
337
|
-
expect(bub.x.value).toBeCloseTo(0.1, 10);
|
|
338
|
-
expect(bub.y.value).toBeCloseTo(0.75, 10);
|
|
339
|
-
const codex = scatter.rows.find((r) => r.key === "compare/codex-mid")!;
|
|
340
|
-
expect(codex.series).toBe("codex");
|
|
341
|
-
expect(codex.x.value).toBeCloseTo(0.225, 10); // errored 0.3 与 passed 0.15,skipped 不计
|
|
342
|
-
// endToEndPassRate:errored → 0,skipped → null,algebra/z passed → 1,跨题均值 0.5
|
|
343
|
-
expect(codex.y.value).toBeCloseTo(0.5, 10);
|
|
344
|
-
|
|
345
|
-
// 场景 2:solo/no-cost 缺成本 → x 为 null(点在,不可画),y 仍有值
|
|
346
|
-
const solo = soloGroup.scatter.rows.find((r) => r.key === "solo/no-cost")!;
|
|
347
|
-
expect(solo.x.value).toBeNull();
|
|
348
|
-
expect(solo.y.value).toBeCloseTo(0.5, 10);
|
|
349
|
-
// ≥2 可画点
|
|
350
|
-
expect(scatter.rows.filter((r) => r.x.value !== null && r.y.value !== null)).toHaveLength(2);
|
|
351
|
-
|
|
352
|
-
// 实验列表仍为每个 experiment 出一项,包括缺成本的 solo
|
|
353
|
-
expect(experiments.map((e) => e.experimentId).sort()).toEqual(["compare/bub-low", "compare/codex-mid", "solo/no-cost"]);
|
|
354
|
-
expect(experiments.find((e) => e.experimentId === "solo/no-cost")!.cost.value).toBeNull();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("场景 4:空 Selection —— 没有可比组", async () => {
|
|
358
|
-
const tree = await resolvedTreeOf(ExperimentComparison, emptyContext());
|
|
359
|
-
expect(comparisonDataOf(tree)!.groups).toEqual([]);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it("场景 4 变体:全 skipped —— 每个点 x/y 都不可测,0 可画点", async () => {
|
|
363
|
-
const tree = await resolvedTreeOf(ExperimentComparison, allSkippedContext());
|
|
364
|
-
const scatter = comparisonDataOf(tree)!.groups[0]!.scatter;
|
|
365
|
-
expect(scatter.rows.every((r) => r.x.value === null && r.y.value === null)).toBe(true);
|
|
366
|
-
expect(scatter.rows.filter((r) => r.x.value !== null && r.y.value !== null)).toHaveLength(0);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("场景 5:failed/errored/skipped 混合 —— ExperimentList 展开区结果摘要正确(eval 级折叠计票)", async () => {
|
|
370
|
-
const tree = await resolvedTreeOf(ExperimentComparison, richContext());
|
|
371
|
-
const codex = comparisonDataOf(tree)!.groups
|
|
372
|
-
.find((group) => group.key === "compare")!
|
|
373
|
-
.experiments.find((e) => e.experimentId === "compare/codex-mid")!;
|
|
374
|
-
// algebra/x errored、algebra/y skipped、algebra/z passed → eval 级折叠 {passed:1, errored:1, skipped:1}
|
|
375
|
-
expect(codex.verdicts).toEqual({ passed: 1, failed: 0, errored: 1, skipped: 1 });
|
|
376
|
-
const errored = codex.evalRows.find((e) => e.evalId === "algebra/x")!;
|
|
377
|
-
expect(errored.verdict).toBe("errored");
|
|
378
|
-
expect(errored).not.toHaveProperty("reason");
|
|
379
|
-
expect(errored.attempts[0]!.error?.message).toBe("adapter crashed");
|
|
380
|
-
expect(codex.evalRows.find((e) => e.evalId === "algebra/y")!.verdict).toBe("skipped");
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
// ═════════════════════════ 3. text / web 渲染的事实相同 ═════════════════════════
|
|
386
|
-
|
|
387
|
-
describe("built-in 与 public-copy 渲染出的事实相同", () => {
|
|
388
|
-
for (const { name, ctx } of SCENARIOS) {
|
|
389
|
-
it(`text / web 事实一致 — ${name}`, async () => {
|
|
390
|
-
const builtinText = await renderReportToText(ExperimentComparison, ctx(), { width: 100 });
|
|
391
|
-
const userText = await renderReportToText(publicCopy, ctx(), { width: 100 });
|
|
392
|
-
// 同一机制、同一 options:text 输出逐字相等(最强的事实级证据)。
|
|
393
|
-
expect(userText).toBe(builtinText);
|
|
394
|
-
|
|
395
|
-
const builtinHtml = await renderReportToStaticHtml(ExperimentComparison, ctx());
|
|
396
|
-
const userHtml = await renderReportToStaticHtml(publicCopy, ctx());
|
|
397
|
-
// 事实比较(不做整段 HTML snapshot):散点点键、attempt 深链集合相同。
|
|
398
|
-
expect(scatterKeysOf(userHtml)).toEqual(scatterKeysOf(builtinHtml));
|
|
399
|
-
expect(hrefsOf(userHtml)).toEqual(hrefsOf(builtinHtml));
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
it("场景 8:view 的 attemptHref 深链格式与用户 --report 完全一致", async () => {
|
|
404
|
-
const ctx = richContext();
|
|
405
|
-
// renderReportToStaticHtml 默认 attemptHref = view 的 `#/attempt/@<locator>`(不透明单段路由,
|
|
406
|
-
// 与 view/data.ts renderReportSlot 不传 attemptHref 时同一条默认路径)。
|
|
407
|
-
const builtinHtml = await renderReportToStaticHtml(ExperimentComparison, ctx);
|
|
408
|
-
const userHtml = await renderReportToStaticHtml(publicCopy, richContext());
|
|
409
|
-
const builtinHrefs = hrefsOf(builtinHtml);
|
|
410
|
-
const userHrefs = hrefsOf(userHtml);
|
|
411
|
-
expect(userHrefs).toEqual(builtinHrefs);
|
|
412
|
-
expect(builtinHrefs.length).toBeGreaterThan(0);
|
|
413
|
-
for (const href of builtinHrefs) {
|
|
414
|
-
expect(href).toMatch(/^#\/attempt\/@[0-9a-z]+$/);
|
|
415
|
-
}
|
|
416
|
-
// 每个深链都指向 Selection 里真实存在的 attempt(身份元组确定性派生同一个 locator)。
|
|
417
|
-
const realDeepLinks = new Set(
|
|
418
|
-
ctx.selection.snapshots.flatMap((s) => s.attempts.map((a) => `#/attempt/${expectedLocatorFor(a)}`)),
|
|
419
|
-
);
|
|
420
|
-
for (const href of builtinHrefs) expect(realDeepLinks.has(href)).toBe(true);
|
|
421
|
-
// errored eval 一定被深链(codex-mid 的 algebra/x)
|
|
422
|
-
const erroredAttempt = ctx.selection.snapshots
|
|
423
|
-
.flatMap((s) => s.attempts)
|
|
424
|
-
.find((a) => a.experimentId === "compare/codex-mid" && a.evalId === "algebra/x")!;
|
|
425
|
-
expect(builtinHrefs).toContain(`#/attempt/${expectedLocatorFor(erroredAttempt)}`);
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// ═════════════════════════ 4. 散点空 / 单点 / 多点行为(text + web 同一事实)═════════════════════════
|
|
430
|
-
|
|
431
|
-
describe("分组空态 / 单点 / 多点行为", () => {
|
|
432
|
-
it("空 Selection:text / web 都显示没有实验组,不伪造空 scatter", async () => {
|
|
433
|
-
const text = await renderReportToText(ExperimentComparison, emptyContext(), { width: 100 });
|
|
434
|
-
expect(text).toContain("No experiment groups");
|
|
435
|
-
const html = await renderReportToStaticHtml(ExperimentComparison, emptyContext());
|
|
436
|
-
expect(html).toContain("nre-experiment-groups-empty");
|
|
437
|
-
expect(html).not.toContain("nre-metric-scatter");
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// ═════════════════════════ 5. 场景 7:数据只 resolve 一次,locale 只改 chrome ═════════════════════════
|
|
443
|
-
|
|
444
|
-
describe("场景 7:en / zh-CN 数据 resolve 一次、chrome 分别本地化", () => {
|
|
445
|
-
it("resolve 一次的同一棵树,en / zh-CN 各渲染一遍:data 事实相同,只有 chrome 文案不同", async () => {
|
|
446
|
-
// 关键:build + resolveReportTree 只做一次,拿到唯一的 resolved 树;两个 locale 复用它。
|
|
447
|
-
const node = (await ExperimentComparison.build(richContext())) as ReportNode;
|
|
448
|
-
const resolved = await resolveReportTree(node);
|
|
449
|
-
validateReportTree(resolved);
|
|
450
|
-
|
|
451
|
-
// 同一棵 resolved 树 → data 只算了一次(下方 spy 版另证调用次数)。
|
|
452
|
-
const comparison = comparisonDataOf(resolved)!;
|
|
453
|
-
const scatterRows = comparison.groups.flatMap((group) => group.scatter.rows);
|
|
454
|
-
const experiments = comparison.groups.flatMap((group) => group.experiments);
|
|
455
|
-
|
|
456
|
-
const enText = renderNodeToText(resolved, createTextContext({ width: 100, locale: "en" }));
|
|
457
|
-
const zhText = renderNodeToText(resolved, createTextContext({ width: 100, locale: "zh-CN" }));
|
|
458
|
-
const enHtml = runWithWebContext(
|
|
459
|
-
{ attemptHref: (locator) => `#/attempt/${locator}`, locale: "en" },
|
|
460
|
-
() => renderToStaticMarkup(resolved as never),
|
|
461
|
-
);
|
|
462
|
-
const zhHtml = runWithWebContext(
|
|
463
|
-
{ attemptHref: (locator) => `#/attempt/${locator}`, locale: "zh-CN" },
|
|
464
|
-
() => renderToStaticMarkup(resolved as never),
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
// chrome 分语言
|
|
468
|
-
expect(enText).toContain("End-to-end pass rate");
|
|
469
|
-
expect(zhText).toContain("端到端成功率");
|
|
470
|
-
expect(enHtml).toContain("End-to-end pass rate");
|
|
471
|
-
expect(zhHtml).toContain("端到端成功率");
|
|
472
|
-
expect(enText).not.toEqual(zhText);
|
|
473
|
-
|
|
474
|
-
// 但两语言下的事实(散点键 / x / y、attempt 深链)完全相同 —— 数据没被重算成不同结果。
|
|
475
|
-
expect(scatterKeysOf(zhHtml)).toEqual(scatterKeysOf(enHtml));
|
|
476
|
-
expect(hrefsOf(zhHtml)).toEqual(hrefsOf(enHtml));
|
|
477
|
-
// 从同一棵 resolved 树取的 data,en/zh 渲染不改变它
|
|
478
|
-
expect(scatterRows.map((r) => r.key)).toEqual(["compare/bub-low", "compare/codex-mid", "solo/no-cost"]);
|
|
479
|
-
const [bub, codex, solo] = scatterRows;
|
|
480
|
-
expect(bub.x.value).toBeCloseTo(0.1, 10);
|
|
481
|
-
expect(bub.y.value).toBeCloseTo(0.75, 10);
|
|
482
|
-
expect(codex.x.value).toBeCloseTo(0.225, 10);
|
|
483
|
-
expect(codex.y.value).toBeCloseTo(0.5, 10); // endToEndPassRate:errored=0,skipped=null,passed=1
|
|
484
|
-
expect(solo.x.value).toBeNull();
|
|
485
|
-
expect(solo.y.value).toBeCloseTo(0.5, 10);
|
|
486
|
-
// ExperimentList 按 endToEndPassRate 从高到低:bub-low(0.75)> codex-mid(0.5)=solo(0.5),同分按 id。
|
|
487
|
-
expect(experiments.map((e) => e.experimentId)).toEqual(["compare/bub-low", "compare/codex-mid", "solo/no-cost"]);
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
it("公共边界佐证:build + resolve 后,两 locale 渲染不再读取 Selection(不重算)", async () => {
|
|
491
|
-
// 观察面是公共的 Selection 契约:所有计算都必须经由 selection.snapshots 取数,
|
|
492
|
-
// filter 产物共享同一计数器,渲染阶段任何形式的重算都会让读数增长。
|
|
493
|
-
const counter = { reads: 0 };
|
|
494
|
-
function countingSelection(snapshots: Snapshot[]): Selection {
|
|
495
|
-
const self = {
|
|
496
|
-
warnings: [] as Selection["warnings"],
|
|
497
|
-
get snapshots() {
|
|
498
|
-
counter.reads += 1;
|
|
499
|
-
return snapshots;
|
|
500
|
-
},
|
|
501
|
-
filter: (predicate: (s: Snapshot) => boolean) => countingSelection(snapshots.filter(predicate)),
|
|
502
|
-
};
|
|
503
|
-
return self as unknown as Selection;
|
|
504
|
-
}
|
|
505
|
-
const counting = countingSelection(richContext().selection.snapshots);
|
|
506
|
-
|
|
507
|
-
const node = (await ExperimentComparison.build({ selection: counting, results: {} as Results })) as ReportNode;
|
|
508
|
-
const resolved = await resolveReportTree(node);
|
|
509
|
-
// 计算阶段确实经由这个公共边界取过数 —— 计数器没有空转。
|
|
510
|
-
expect(counter.reads).toBeGreaterThan(0);
|
|
511
|
-
const readsAfterResolve = counter.reads;
|
|
512
|
-
|
|
513
|
-
// 渲染面纯同步、零 IO:两 locale、两宿主渲染同一棵 resolved 树,不再触发任何计算。
|
|
514
|
-
renderNodeToText(resolved, createTextContext({ locale: "en" }));
|
|
515
|
-
renderNodeToText(resolved, createTextContext({ locale: "zh-CN" }));
|
|
516
|
-
runWithWebContext({ attemptHref: (locator) => `#/attempt/${locator}`, locale: "en" }, () =>
|
|
517
|
-
renderToStaticMarkup(resolved as never),
|
|
518
|
-
);
|
|
519
|
-
runWithWebContext({ attemptHref: (locator) => `#/attempt/${locator}`, locale: "zh-CN" }, () =>
|
|
520
|
-
renderToStaticMarkup(resolved as never),
|
|
521
|
-
);
|
|
522
|
-
expect(counter.reads).toBe(readsAfterResolve);
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// ═════════════════════════ 6. 场景 9:只放 ExperimentList 的最小报告不触发散点计算 ═════════════════════════
|
|
527
|
-
|
|
528
|
-
describe("场景 9:最小用户报告只放 <ExperimentList> —— 不计算散点", () => {
|
|
529
|
-
const minimal = defineReport(async ({ selection }) => {
|
|
530
|
-
const experiments = await ExperimentList.data(selection);
|
|
531
|
-
return (
|
|
532
|
-
<Col>
|
|
533
|
-
<ExperimentList items={experiments} />
|
|
534
|
-
</Col>
|
|
535
|
-
);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("结构性缺席:resolved 树里没有 MetricScatter 节点", async () => {
|
|
539
|
-
const tree = await resolvedTreeOf(minimal, richContext());
|
|
540
|
-
expect(collect(tree).map((n) => n.label)).toEqual(["Col", "ExperimentList"]);
|
|
541
|
-
expect(scatterOf(tree)).toBeUndefined();
|
|
542
|
-
expect(experimentListOf(tree)).toBeDefined();
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
it("用户换指标/维度无需框架新增分支:改 points/series 只改 resolved data,不改机制", async () => {
|
|
547
|
-
// 一份「用户自定义」报告:同一 MetricScatter 组件,换成 model 维度 + 只画一个指标轴组合。
|
|
548
|
-
const custom = defineReport(async ({ selection }) => {
|
|
549
|
-
const { MetricScatter, costUSD, tokens } = await import("./index.ts");
|
|
550
|
-
return (
|
|
551
|
-
<Col>
|
|
552
|
-
<MetricScatter selection={selection} points="agent" series="model" x={tokens} y={costUSD} />
|
|
553
|
-
</Col>
|
|
554
|
-
);
|
|
555
|
-
});
|
|
556
|
-
const tree = await resolvedTreeOf(custom, richContext());
|
|
557
|
-
const scatter = scatterOf(tree)!;
|
|
558
|
-
expect(scatter.points).toBe("agent");
|
|
559
|
-
expect(scatter.series).toBe("model");
|
|
560
|
-
expect(scatter.x.key).toBe("tokens");
|
|
561
|
-
expect(scatter.y.key).toBe("cost");
|
|
562
|
-
// 走的仍是同一条 resolve 管线,不需要任何 built-in 专属处理。
|
|
563
|
-
expect(collect(tree).map((n) => n.label)).toEqual(["Col", "MetricScatter"]);
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
// ═════════════════════════ 7. 架构不变量:renderer 不依赖 built-in(import 依赖图)═════════════════════════
|
|
568
|
-
|
|
569
|
-
describe("架构不变量:renderer 不认识 built-in", () => {
|
|
570
|
-
const RENDERER_FILES = ["./report.ts", "./web.ts", "./tree.ts"];
|
|
571
|
-
|
|
572
|
-
/** 只解析 import / export…from / 动态 import() 语句的模块 specifier,不 grep 实现文本。 */
|
|
573
|
-
function importSpecifiersOf(rel: string): string[] {
|
|
574
|
-
const src = readFileSync(new URL(rel, import.meta.url), "utf8");
|
|
575
|
-
const specifiers: string[] = [];
|
|
576
|
-
const patterns = [
|
|
577
|
-
/(?:^|\n)\s*import\s+[^"'()]*?from\s*["']([^"']+)["']/g, // import x from "…"
|
|
578
|
-
/(?:^|\n)\s*import\s*["']([^"']+)["']/g, // import "…"(副作用导入)
|
|
579
|
-
/(?:^|\n)\s*export\s+[^"'()]*?from\s*["']([^"']+)["']/g, // export … from "…"
|
|
580
|
-
/import\(\s*["']([^"']+)["']\s*\)/g, // 动态 import("…")
|
|
581
|
-
];
|
|
582
|
-
for (const re of patterns) {
|
|
583
|
-
for (const m of src.matchAll(re)) specifiers.push(m[1]);
|
|
584
|
-
}
|
|
585
|
-
return specifiers;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
for (const rel of RENDERER_FILES) {
|
|
589
|
-
it(`${rel} 的 import 依赖图不含 built-ins 模块`, () => {
|
|
590
|
-
const specifiers = importSpecifiersOf(rel);
|
|
591
|
-
expect(specifiers.length).toBeGreaterThan(0); // 解析确实取到了依赖,守护没有空转
|
|
592
|
-
for (const spec of specifiers) {
|
|
593
|
-
expect(spec).not.toMatch(/built-ins/);
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
});
|