niceeval 0.6.0 → 0.6.2
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/dist/agents/types.d.ts +72 -6
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +55 -1
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +5 -3
- package/dist/report/aggregate.js +32 -5
- package/dist/report/built-ins/experiment-comparison.d.ts +39 -0
- package/dist/report/built-ins/experiment-comparison.js +119 -0
- package/dist/report/built-ins/index.d.ts +2 -1
- package/dist/report/built-ins/index.js +2 -2
- package/dist/report/components.d.ts +10 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +68 -66
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -14
- package/dist/report/format.js +28 -30
- package/dist/report/index.d.ts +5 -4
- package/dist/report/index.js +6 -5
- package/dist/report/locale.d.ts +23 -3
- package/dist/report/locale.js +47 -6
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +66 -15
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +4 -4
- package/dist/report/react/AttemptList.js +8 -10
- package/dist/report/react/EvalList.d.ts +1 -1
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +8 -0
- package/dist/report/react/ExperimentComparison.js +11 -0
- package/dist/report/react/ExperimentList.d.ts +4 -2
- package/dist/report/react/ExperimentList.js +57 -7
- package/dist/report/react/MetricScatter.js +6 -16
- package/dist/report/react/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +3 -3
- package/dist/report/react/fixtures.js +30 -18
- package/dist/report/react/format.d.ts +1 -1
- package/dist/report/react/format.js +1 -1
- package/dist/report/react/index.d.ts +1 -1
- package/dist/report/report.d.ts +5 -1
- package/dist/report/report.js +6 -2
- package/dist/report/text/faces.d.ts +1 -1
- package/dist/report/text/faces.js +100 -61
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +40 -34
- package/dist/results/types.d.ts +11 -0
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +1 -0
- package/dist/shared/aggregate.js +3 -3
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- package/docs-site/zh/concepts/adapter.mdx +24 -6
- package/docs-site/zh/concepts/assert.mdx +11 -10
- package/docs-site/zh/concepts/evals.mdx +7 -6
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +7 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +35 -31
- package/docs-site/zh/guides/authoring.mdx +33 -0
- package/docs-site/zh/guides/ci-integration.mdx +23 -12
- package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
- package/docs-site/zh/guides/custom-reports.mdx +29 -34
- package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
- package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
- package/docs-site/zh/guides/debugging.mdx +210 -0
- package/docs-site/zh/guides/experiments.mdx +10 -3
- package/docs-site/zh/guides/fixtures.mdx +3 -1
- package/docs-site/zh/guides/official-adapters.mdx +27 -3
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +49 -37
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +42 -8
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +57 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +258 -10
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +85 -41
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +19 -2
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +27 -0
- package/docs-site/zh/reference/capabilities.mdx +2 -2
- package/docs-site/zh/reference/cli.mdx +35 -9
- package/docs-site/zh/reference/define-agent.mdx +60 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/events.mdx +2 -2
- package/docs-site/zh/reference/expect.mdx +36 -6
- package/package.json +5 -1
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +11 -0
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/types.ts +73 -6
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +411 -108
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +161 -29
- package/src/context/session.test.ts +1 -0
- package/src/context/session.ts +114 -6
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +65 -4
- package/src/i18n/zh-CN.ts +66 -4
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +34 -5
- package/src/report/built-in-user-parity.test.tsx +127 -173
- package/src/report/built-ins/experiment-comparison.tsx +179 -0
- package/src/report/built-ins/index.ts +7 -2
- package/src/report/components.tsx +11 -3
- package/src/report/compute.ts +80 -74
- package/src/report/dual-render.test.tsx +222 -91
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +36 -27
- package/src/report/index.ts +23 -6
- package/src/report/locale.ts +49 -6
- package/src/report/metrics.ts +68 -15
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +9 -36
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +173 -55
- package/src/report/react/MetricScatter.tsx +13 -25
- package/src/report/react/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +72 -1
- package/src/report/react/fixtures.ts +34 -21
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +0 -1
- package/src/report/react/render.test.tsx +30 -69
- package/src/report/react/styles.css +112 -14
- package/src/report/report.test.ts +308 -105
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +111 -67
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +42 -34
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +9 -7
- package/src/results/attempt-evidence.ts +15 -8
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +145 -55
- package/src/results/host-equivalence.test.ts +11 -9
- package/src/results/index.ts +2 -0
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +7 -1
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +85 -51
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +7 -0
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +242 -36
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +3 -2
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +2 -2
- package/src/show/index.ts +29 -16
- package/src/show/render.ts +626 -308
- package/src/show/show.test.ts +251 -36
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/components/AttemptModal.tsx +153 -2
- package/src/view/app/components/CodeView.tsx +32 -11
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +6 -0
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/artifact-serving.test.ts +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +2 -1
- package/src/view/data.ts +17 -7
- package/src/view/index.ts +12 -1
- package/src/view/server.ts +2 -0
- package/src/view/shared/types.ts +1 -1
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +11 -10
- package/dist/o11y/execution-tree.d.ts +0 -103
- package/dist/o11y/otlp/select.d.ts +0 -22
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
- package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
- package/dist/results/annotated-source.d.ts +0 -61
- package/dist/results/attempt-evidence.d.ts +0 -69
- package/dist/results/attempt-source.d.ts +0 -15
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
package/src/o11y/types.ts
CHANGED
|
@@ -53,11 +53,22 @@ export interface InputRequest {
|
|
|
53
53
|
readonly options?: readonly { id: string; label?: string }[];
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 落盘截断的结构化标记(见 docs/feature/results/architecture.md「大值截断」):
|
|
58
|
+
* `path` 是被截断的位置(事件里是字段名,如 "output";span 里是 attribute key,如 "output.value"),
|
|
59
|
+
* `originalBytes` 是截断前的 UTF-8 字节数。view 显示「输出过大,已截断」靠它,不正则匹配 marker。
|
|
60
|
+
*/
|
|
61
|
+
export interface Truncation {
|
|
62
|
+
path: string;
|
|
63
|
+
originalBytes: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
/**
|
|
57
67
|
* 标准事件流的词汇(对标 docs/feature/adapters/architecture/events.md)。adapter 唯一的硬活就是把
|
|
58
68
|
* 各 agent 五花八门的原始 transcript 映射成 StreamEvent[];映射完,整套断言免费。
|
|
69
|
+
* `truncated` 只由 results writer 在落盘时刻写入(运行时全量,落盘截断);adapter 不产出它。
|
|
59
70
|
*/
|
|
60
|
-
export type StreamEvent =
|
|
71
|
+
export type StreamEvent = { truncated?: Truncation[] } & (
|
|
61
72
|
/** 一条文本消息(assistant 回复或 user 输入);`loc` 是可选的源码位置,用于把消息叠回 eval 源码。 */
|
|
62
73
|
| { type: "message"; role: "assistant" | "user"; text: string; loc?: SourceLoc }
|
|
63
74
|
/** 发起一次工具/动作调用;`tool` 是归一化后的规范工具名,原始名保留在 DerivedFacts.ToolCall.originalName。 */
|
|
@@ -85,7 +96,8 @@ export type StreamEvent =
|
|
|
85
96
|
/** 上下文被压缩/摘要(如超长会话截断历史);`reason` 是可选的压缩原因说明。 */
|
|
86
97
|
| { type: "compaction"; reason?: string }
|
|
87
98
|
/** 运行中出现的错误。 */
|
|
88
|
-
| { type: "error"; message: string }
|
|
99
|
+
| { type: "error"; message: string }
|
|
100
|
+
);
|
|
89
101
|
|
|
90
102
|
/** core 从事件流折叠出的结构化事实(deriveRunFacts)。 */
|
|
91
103
|
export interface ToolCall {
|
|
@@ -145,6 +157,8 @@ export interface TraceSpan {
|
|
|
145
157
|
kind?: SpanKind;
|
|
146
158
|
/** OTLP span 属性(gen_ai.* / tool 名 / token 等),按 key 摊平。raw 属性始终保留供下钻。 */
|
|
147
159
|
attributes?: Record<string, JsonValue>;
|
|
160
|
+
/** 落盘截断标记(只由 results writer 在序列化时写入,运行时全量;见 Truncation)。 */
|
|
161
|
+
truncated?: Truncation[];
|
|
148
162
|
}
|
|
149
163
|
|
|
150
164
|
/** 给人 / 给 EVAL.ts 看的 o11y 摘要(注入沙箱 __niceeval__/results.json)。 */
|
package/src/report/aggregate.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type { AttemptHandle, Selection, SelectionWarning, Snapshot } from "../re
|
|
|
9
9
|
import { encodeAttemptLocator, type AttemptLocator } from "../results/locator.ts";
|
|
10
10
|
import type {
|
|
11
11
|
Aggregator,
|
|
12
|
+
AxisInput,
|
|
13
|
+
ConfigRef,
|
|
12
14
|
DimensionInput,
|
|
13
15
|
Metric,
|
|
14
16
|
MetricCell,
|
|
@@ -115,29 +117,56 @@ function isFlagRef(dimension: DimensionInput): dimension is FlagRef {
|
|
|
115
117
|
return typeof dimension === "object" && "kind" in dimension && dimension.kind === "flag";
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
function isConfigRef(dimension: DimensionInput): dimension is ConfigRef {
|
|
121
|
+
return typeof dimension === "object" && "kind" in dimension && dimension.kind === "config";
|
|
122
|
+
}
|
|
123
|
+
|
|
118
124
|
/** experiment 声明的 flags(经 runner 原样透传进持久化字段 ExperimentRunInfo.flags)。 */
|
|
119
125
|
function flagsOf(attempt: AttemptHandle): Record<string, unknown> | undefined {
|
|
120
126
|
return attempt.result.experiment?.flags;
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
/**
|
|
130
|
+
* config() 的取值:读快照的 `ExperimentRunInfo` 投影(快照优先;第三方落盘只把投影拼在
|
|
131
|
+
* 条目上时回退 result.experiment),外加桥接到快照顶层权威字段的 `model` / `agent` 两个键
|
|
132
|
+
* (与 "model" / "agent" 内置维度同一读法,不另造第二套口径)。未投影 → undefined。
|
|
133
|
+
*/
|
|
134
|
+
function configValueOf(ref: ConfigRef, item: Item): unknown {
|
|
135
|
+
if (ref.name === "model") return item.attempt.result.model ?? item.snapshot.model;
|
|
136
|
+
if (ref.name === "agent") return item.attempt.result.agent || item.snapshot.agent;
|
|
137
|
+
const info = item.snapshot.experiment ?? item.attempt.result.experiment;
|
|
138
|
+
// ExperimentRunInfo 是穷尽可序列化投影;按字段名取值,新增投影字段无需改这里
|
|
139
|
+
return (info as Record<string, unknown> | undefined)?.[ref.name];
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
/** flag 声明值 → 组标签:label 函数优先,其余 String();未声明 → FLAG_UNSET。 */
|
|
124
143
|
export function flagGroupKey(ref: FlagRef, item: Item): string {
|
|
125
|
-
|
|
144
|
+
return refGroupKey(ref, flagsOf(item.attempt)?.[ref.name]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** config 投影值 → 组标签,规则与 flagGroupKey 一致;未投影 → FLAG_UNSET。 */
|
|
148
|
+
export function configGroupKey(ref: ConfigRef, item: Item): string {
|
|
149
|
+
return refGroupKey(ref, configValueOf(ref, item));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function refGroupKey(ref: FlagRef | ConfigRef, value: unknown): string {
|
|
126
153
|
if (value === undefined) return FLAG_UNSET;
|
|
127
|
-
//
|
|
154
|
+
// 声明侧的合法标量就是这三种;非标量投影(sandbox / selectedEvalIds…)如实 JSON 化,不猜
|
|
128
155
|
if (typeof ref.label === "function") return ref.label(value as string | number | boolean);
|
|
156
|
+
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
129
157
|
return String(value);
|
|
130
158
|
}
|
|
131
159
|
|
|
132
|
-
/** flag
|
|
133
|
-
export function
|
|
134
|
-
const value = flagsOf(item.attempt)?.[ref.name];
|
|
160
|
+
/** flag / config 作轴:要求数值;未声明、未投影或非数值 → null(点不画,注脚报数)。 */
|
|
161
|
+
export function axisValue(ref: AxisInput, item: Item): number | null {
|
|
162
|
+
const value = ref.kind === "flag" ? flagsOf(item.attempt)?.[ref.name] : configValueOf(ref, item);
|
|
135
163
|
return typeof value === "number" ? value : null;
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
export function dimensionKey(dimension: DimensionInput, item: Item): string {
|
|
139
167
|
if (typeof dimension !== "string") {
|
|
140
168
|
if (isFlagRef(dimension)) return flagGroupKey(dimension, item);
|
|
169
|
+
if (isConfigRef(dimension)) return configGroupKey(dimension, item);
|
|
141
170
|
return dimension.of(item.attempt);
|
|
142
171
|
}
|
|
143
172
|
const result = item.attempt.result;
|
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
//
|
|
1
|
+
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
2
|
+
// Phase 4 验收:证明内置报告 ExperimentComparison 与一份普通用户 --report 文件走的是
|
|
2
3
|
// 完全同一套机制 —— 不是长得像,而是同一条 build → resolveReportTree → validate → render 管线。
|
|
3
4
|
//
|
|
4
5
|
// 证据链(plan/built-in-reports-user-parity.md「Phase 4」):
|
|
5
|
-
// 1. test/fixtures/report/
|
|
6
|
+
// 1. test/fixtures/report/experiment-comparison-public-copy.tsx 只从公开 barrel import,
|
|
6
7
|
// 与 built-in 逐节点同构(见文件头)。
|
|
7
8
|
// 2. 同一 ReportContext 下分别 build + resolveReportTree,深比较 resolved 树:组件类型 / 顺序 /
|
|
8
9
|
// resolved data / props keys 全部结构化相等(不靠 definition 引用相等,两份是独立文件)。
|
|
9
10
|
// 3. 两份分别过 renderReportToText / renderReportToStaticHtml,比较事实(散点键 / 表格行 /
|
|
10
11
|
// attempt 深链),不做 HTML 字面 snapshot。
|
|
11
12
|
// 4. 覆盖 plan 的 9 个必测场景。
|
|
12
|
-
// 5. 架构不变量:renderer
|
|
13
|
-
//
|
|
14
|
-
// Selection / resolve / validate / render。
|
|
13
|
+
// 5. 架构不变量:renderer 模块的 import 依赖图不含 built-ins。
|
|
14
|
+
// (show / view 的宿主选择与装载边界等价由 src/show/show.test.ts、src/view/view-report.test.ts 覆盖。)
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
18
|
-
import { describe, expect, it
|
|
18
|
+
import { describe, expect, it } from "vitest";
|
|
19
19
|
|
|
20
20
|
import type { EvalResult, Verdict } from "../types.ts";
|
|
21
21
|
import type { AttemptHandle, Results, Selection, Snapshot } from "../results/index.ts";
|
|
22
22
|
import type { ExperimentListItem, ScatterData } from "./types.ts";
|
|
23
|
+
import type { ExperimentComparisonData } from "./built-ins/index.ts";
|
|
23
24
|
import { encodeAttemptLocator } from "../results/locator.ts";
|
|
24
25
|
import { Col, ExperimentList, defineReport } from "./index.ts";
|
|
25
|
-
import {
|
|
26
|
-
import publicCopy from "../../test/fixtures/report/
|
|
26
|
+
import { ExperimentComparison } from "./built-ins/index.ts";
|
|
27
|
+
import publicCopy from "../../test/fixtures/report/experiment-comparison-public-copy.tsx";
|
|
27
28
|
import { renderReportToText } from "./report.ts";
|
|
28
29
|
import { renderReportToStaticHtml } from "./web.ts";
|
|
29
30
|
import {
|
|
@@ -34,7 +35,6 @@ import {
|
|
|
34
35
|
validateReportTree,
|
|
35
36
|
type ReportNode,
|
|
36
37
|
} from "./tree.ts";
|
|
37
|
-
import * as compute from "./compute.ts";
|
|
38
38
|
|
|
39
39
|
// ───────────────────────── fake Selection / Snapshot(按 results 读取契约造)─────────────────────────
|
|
40
40
|
|
|
@@ -139,7 +139,10 @@ function richContext() {
|
|
|
139
139
|
agent: "codex",
|
|
140
140
|
model: "gpt-codex",
|
|
141
141
|
results: [
|
|
142
|
-
res("algebra/x", "errored", {
|
|
142
|
+
res("algebra/x", "errored", {
|
|
143
|
+
error: { code: "unexpected-error", message: "adapter crashed", phase: "eval.run" },
|
|
144
|
+
estimatedCostUSD: 0.3,
|
|
145
|
+
}),
|
|
143
146
|
res("algebra/y", "skipped"),
|
|
144
147
|
res("algebra/z", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.15 } }),
|
|
145
148
|
],
|
|
@@ -251,6 +254,12 @@ function experimentListOf(tree: ReportNode): ExperimentListItem[] | undefined {
|
|
|
251
254
|
return collect(tree).find((c) => c.label === "ExperimentList")?.props.items as ExperimentListItem[] | undefined;
|
|
252
255
|
}
|
|
253
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
|
+
|
|
254
263
|
/** HTML 里所有 attempt 深链,排序后便于集合比较。 */
|
|
255
264
|
function hrefsOf(html: string): string[] {
|
|
256
265
|
return [...html.matchAll(/href="(#\/attempt\/[^"]+)"/g)].map((m) => m[1]).sort();
|
|
@@ -278,28 +287,27 @@ function expectedLocatorFor(attempt: AttemptHandle): string {
|
|
|
278
287
|
|
|
279
288
|
describe("resolved 树结构化等价(built-in ≡ 包外 public-copy)", () => {
|
|
280
289
|
it("两份是独立 ReportDefinition,不是同一引用", () => {
|
|
281
|
-
expect(
|
|
282
|
-
expect(typeof
|
|
290
|
+
expect(ExperimentComparison).not.toBe(publicCopy);
|
|
291
|
+
expect(typeof ExperimentComparison.build).toBe("function");
|
|
283
292
|
expect(typeof publicCopy.build).toBe("function");
|
|
284
293
|
});
|
|
285
294
|
|
|
286
295
|
for (const { name, ctx } of SCENARIOS) {
|
|
287
296
|
it(`同一 ReportContext 下 resolved 树逐节点相等 — ${name}`, async () => {
|
|
288
297
|
const c = ctx();
|
|
289
|
-
const builtIn = await resolvedTreeOf(
|
|
298
|
+
const builtIn = await resolvedTreeOf(ExperimentComparison, c);
|
|
290
299
|
const user = await resolvedTreeOf(publicCopy, ctx()); // 各用一份等价 ctx,证明不依赖共享状态
|
|
291
300
|
|
|
292
|
-
//
|
|
301
|
+
// build 面只把预计算分组数据交给公开组合件,没有 renderer 私有节点。
|
|
293
302
|
const labels = collect(builtIn).map((n) => n.label);
|
|
294
|
-
expect(labels).toEqual(["
|
|
303
|
+
expect(labels).toEqual(["ExperimentComparison"]);
|
|
295
304
|
expect(collect(user).map((n) => n.label)).toEqual(labels);
|
|
296
305
|
|
|
297
306
|
// 逐节点结构化相等:组件类型、顺序、resolved data、props keys(pointHref/filter/…)全一致。
|
|
298
307
|
expect(factify(user)).toEqual(factify(builtIn));
|
|
299
308
|
|
|
300
309
|
// resolved data 本身也逐字段相等(不只是树同构)。
|
|
301
|
-
expect(
|
|
302
|
-
expect(experimentListOf(user)).toEqual(experimentListOf(builtIn));
|
|
310
|
+
expect(comparisonDataOf(user)).toEqual(comparisonDataOf(builtIn));
|
|
303
311
|
});
|
|
304
312
|
}
|
|
305
313
|
});
|
|
@@ -308,16 +316,20 @@ describe("resolved 树结构化等价(built-in ≡ 包外 public-copy)", () => {
|
|
|
308
316
|
|
|
309
317
|
describe("必测场景:resolved data 事实", () => {
|
|
310
318
|
it("场景 1+2:多 experiment/agent,成本与通过率都有值;缺成本的点如实 x=null,实验列表仍出项", async () => {
|
|
311
|
-
const tree = await resolvedTreeOf(
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
316
328
|
expect(scatter.points).toBe("experiment");
|
|
317
329
|
expect(scatter.series).toBe("agent");
|
|
318
330
|
expect(scatter.x.key).toBe("cost");
|
|
319
|
-
expect(scatter.y.key).toBe("pass-rate");
|
|
320
|
-
expect(scatter.rows.map((r) => r.key).sort()).toEqual(["compare/bub-low", "compare/codex-mid"
|
|
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"]);
|
|
321
333
|
|
|
322
334
|
const bub = scatter.rows.find((r) => r.key === "compare/bub-low")!;
|
|
323
335
|
expect(bub.series).toBe("bub");
|
|
@@ -327,10 +339,11 @@ describe("必测场景:resolved data 事实", () => {
|
|
|
327
339
|
const codex = scatter.rows.find((r) => r.key === "compare/codex-mid")!;
|
|
328
340
|
expect(codex.series).toBe("codex");
|
|
329
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
|
|
330
343
|
expect(codex.y.value).toBeCloseTo(0.5, 10);
|
|
331
344
|
|
|
332
345
|
// 场景 2:solo/no-cost 缺成本 → x 为 null(点在,不可画),y 仍有值
|
|
333
|
-
const solo = scatter.rows.find((r) => r.key === "solo/no-cost")!;
|
|
346
|
+
const solo = soloGroup.scatter.rows.find((r) => r.key === "solo/no-cost")!;
|
|
334
347
|
expect(solo.x.value).toBeNull();
|
|
335
348
|
expect(solo.y.value).toBeCloseTo(0.5, 10);
|
|
336
349
|
// ≥2 可画点
|
|
@@ -341,52 +354,32 @@ describe("必测场景:resolved data 事实", () => {
|
|
|
341
354
|
expect(experiments.find((e) => e.experimentId === "solo/no-cost")!.cost.value).toBeNull();
|
|
342
355
|
});
|
|
343
356
|
|
|
344
|
-
it("场景
|
|
345
|
-
const tree = await resolvedTreeOf(
|
|
346
|
-
|
|
347
|
-
const drawable = scatter.rows.filter((r) => r.x.value !== null && r.y.value !== null);
|
|
348
|
-
expect(drawable).toHaveLength(1);
|
|
349
|
-
expect(drawable[0].key).toBe("only/priced");
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("场景 4:空 Selection —— 散点无 rows、实验列表为空数组", async () => {
|
|
353
|
-
const tree = await resolvedTreeOf(CostPassRateComparison, emptyContext());
|
|
354
|
-
expect(scatterOf(tree)!.rows).toEqual([]);
|
|
355
|
-
expect(experimentListOf(tree)).toEqual([]);
|
|
357
|
+
it("场景 4:空 Selection —— 没有可比组", async () => {
|
|
358
|
+
const tree = await resolvedTreeOf(ExperimentComparison, emptyContext());
|
|
359
|
+
expect(comparisonDataOf(tree)!.groups).toEqual([]);
|
|
356
360
|
});
|
|
357
361
|
|
|
358
362
|
it("场景 4 变体:全 skipped —— 每个点 x/y 都不可测,0 可画点", async () => {
|
|
359
|
-
const tree = await resolvedTreeOf(
|
|
360
|
-
const scatter =
|
|
363
|
+
const tree = await resolvedTreeOf(ExperimentComparison, allSkippedContext());
|
|
364
|
+
const scatter = comparisonDataOf(tree)!.groups[0]!.scatter;
|
|
361
365
|
expect(scatter.rows.every((r) => r.x.value === null && r.y.value === null)).toBe(true);
|
|
362
366
|
expect(scatter.rows.filter((r) => r.x.value !== null && r.y.value !== null)).toHaveLength(0);
|
|
363
367
|
});
|
|
364
368
|
|
|
365
369
|
it("场景 5:failed/errored/skipped 混合 —— ExperimentList 展开区结果摘要正确(eval 级折叠计票)", async () => {
|
|
366
|
-
const tree = await resolvedTreeOf(
|
|
367
|
-
const codex =
|
|
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")!;
|
|
368
374
|
// algebra/x errored、algebra/y skipped、algebra/z passed → eval 级折叠 {passed:1, errored:1, skipped:1}
|
|
369
375
|
expect(codex.verdicts).toEqual({ passed: 1, failed: 0, errored: 1, skipped: 1 });
|
|
370
376
|
const errored = codex.evalRows.find((e) => e.evalId === "algebra/x")!;
|
|
371
377
|
expect(errored.verdict).toBe("errored");
|
|
372
|
-
expect(errored
|
|
378
|
+
expect(errored).not.toHaveProperty("reason");
|
|
379
|
+
expect(errored.attempts[0]!.error?.message).toBe("adapter crashed");
|
|
373
380
|
expect(codex.evalRows.find((e) => e.evalId === "algebra/y")!.verdict).toBe("skipped");
|
|
374
381
|
});
|
|
375
382
|
|
|
376
|
-
it("场景 6:同 experiment 多 eval、多 attempts —— 展开层与 attempt locator 正确", async () => {
|
|
377
|
-
const tree = await resolvedTreeOf(CostPassRateComparison, richContext());
|
|
378
|
-
const bub = experimentListOf(tree)!.find((e) => e.experimentId === "compare/bub-low")!;
|
|
379
|
-
expect(bub.evalRows.map((e) => e.evalId).sort()).toEqual(["algebra/x", "algebra/y"]);
|
|
380
|
-
const multi = bub.evalRows.find((e) => e.evalId === "algebra/x")!;
|
|
381
|
-
// 多轮 attempt 折成 passed(任一轮通过),两次 attempt 都在展开区的 attempts 里
|
|
382
|
-
expect(multi.verdict).toBe("passed");
|
|
383
|
-
expect(multi.attempts.map((a) => a.attempt)).toEqual([0, 1]);
|
|
384
|
-
expect(multi.attempts.map((a) => a.verdict)).toEqual(["failed", "passed"]);
|
|
385
|
-
// attempt locator 唯一、格式正确 —— 回到证据的引用不丢
|
|
386
|
-
const locators = multi.attempts.map((a) => a.locator);
|
|
387
|
-
expect(new Set(locators).size).toBe(2);
|
|
388
|
-
for (const locator of locators) expect(locator).toMatch(/^@1[0-9a-z]{7}$/);
|
|
389
|
-
});
|
|
390
383
|
});
|
|
391
384
|
|
|
392
385
|
// ═════════════════════════ 3. text / web 渲染的事实相同 ═════════════════════════
|
|
@@ -394,12 +387,12 @@ describe("必测场景:resolved data 事实", () => {
|
|
|
394
387
|
describe("built-in 与 public-copy 渲染出的事实相同", () => {
|
|
395
388
|
for (const { name, ctx } of SCENARIOS) {
|
|
396
389
|
it(`text / web 事实一致 — ${name}`, async () => {
|
|
397
|
-
const builtinText = await renderReportToText(
|
|
390
|
+
const builtinText = await renderReportToText(ExperimentComparison, ctx(), { width: 100 });
|
|
398
391
|
const userText = await renderReportToText(publicCopy, ctx(), { width: 100 });
|
|
399
392
|
// 同一机制、同一 options:text 输出逐字相等(最强的事实级证据)。
|
|
400
393
|
expect(userText).toBe(builtinText);
|
|
401
394
|
|
|
402
|
-
const builtinHtml = await renderReportToStaticHtml(
|
|
395
|
+
const builtinHtml = await renderReportToStaticHtml(ExperimentComparison, ctx());
|
|
403
396
|
const userHtml = await renderReportToStaticHtml(publicCopy, ctx());
|
|
404
397
|
// 事实比较(不做整段 HTML snapshot):散点点键、attempt 深链集合相同。
|
|
405
398
|
expect(scatterKeysOf(userHtml)).toEqual(scatterKeysOf(builtinHtml));
|
|
@@ -411,7 +404,7 @@ describe("built-in 与 public-copy 渲染出的事实相同", () => {
|
|
|
411
404
|
const ctx = richContext();
|
|
412
405
|
// renderReportToStaticHtml 默认 attemptHref = view 的 `#/attempt/@<locator>`(不透明单段路由,
|
|
413
406
|
// 与 view/data.ts renderReportSlot 不传 attemptHref 时同一条默认路径)。
|
|
414
|
-
const builtinHtml = await renderReportToStaticHtml(
|
|
407
|
+
const builtinHtml = await renderReportToStaticHtml(ExperimentComparison, ctx);
|
|
415
408
|
const userHtml = await renderReportToStaticHtml(publicCopy, richContext());
|
|
416
409
|
const builtinHrefs = hrefsOf(builtinHtml);
|
|
417
410
|
const userHrefs = hrefsOf(userHtml);
|
|
@@ -435,36 +428,15 @@ describe("built-in 与 public-copy 渲染出的事实相同", () => {
|
|
|
435
428
|
|
|
436
429
|
// ═════════════════════════ 4. 散点空 / 单点 / 多点行为(text + web 同一事实)═════════════════════════
|
|
437
430
|
|
|
438
|
-
describe("
|
|
439
|
-
it("
|
|
440
|
-
const text = await renderReportToText(
|
|
441
|
-
expect(text).toContain("No
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
expect(html).toContain("nre-scatter
|
|
445
|
-
expect(html).toContain("nre-missing");
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it("恰好 1 可画点:text/web 都说「至少 2 个实验」而不是画孤点", async () => {
|
|
449
|
-
const text = await renderReportToText(CostPassRateComparison, oneDrawableContext(), { width: 100 });
|
|
450
|
-
expect(text).toContain("At least 2 experiments needed to compare");
|
|
451
|
-
expect(text).not.toContain("better → upper right");
|
|
452
|
-
const html = await renderReportToStaticHtml(CostPassRateComparison, oneDrawableContext());
|
|
453
|
-
expect(html).toContain("nre-scatter-empty");
|
|
454
|
-
// 单点不是「缺数据」空态:散点空态本身不带 nre-missing 变体类(与 0 可画点的
|
|
455
|
-
// "nre-scatter-empty nre-missing" 区分开)。ExperimentList 展开区自己的缺成本格子
|
|
456
|
-
// 也合法渲染 nre-missing,不属于这条散点专属的断言范围。
|
|
457
|
-
expect(html).not.toContain("nre-scatter-empty nre-missing");
|
|
458
|
-
expect(html).not.toContain("<svg");
|
|
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");
|
|
459
438
|
});
|
|
460
439
|
|
|
461
|
-
it("≥2 可画点:正常绘图(text 有 better → upper right,web 有 svg)", async () => {
|
|
462
|
-
const text = await renderReportToText(CostPassRateComparison, richContext(), { width: 100 });
|
|
463
|
-
expect(text).toContain("better → upper right");
|
|
464
|
-
const html = await renderReportToStaticHtml(CostPassRateComparison, richContext());
|
|
465
|
-
expect(html).toContain("<svg");
|
|
466
|
-
expect(html).not.toContain("nre-scatter-empty");
|
|
467
|
-
});
|
|
468
440
|
});
|
|
469
441
|
|
|
470
442
|
// ═════════════════════════ 5. 场景 7:数据只 resolve 一次,locale 只改 chrome ═════════════════════════
|
|
@@ -472,13 +444,14 @@ describe("散点空态 / 单点 / 多点行为", () => {
|
|
|
472
444
|
describe("场景 7:en / zh-CN 数据 resolve 一次、chrome 分别本地化", () => {
|
|
473
445
|
it("resolve 一次的同一棵树,en / zh-CN 各渲染一遍:data 事实相同,只有 chrome 文案不同", async () => {
|
|
474
446
|
// 关键:build + resolveReportTree 只做一次,拿到唯一的 resolved 树;两个 locale 复用它。
|
|
475
|
-
const node = (await
|
|
447
|
+
const node = (await ExperimentComparison.build(richContext())) as ReportNode;
|
|
476
448
|
const resolved = await resolveReportTree(node);
|
|
477
449
|
validateReportTree(resolved);
|
|
478
450
|
|
|
479
451
|
// 同一棵 resolved 树 → data 只算了一次(下方 spy 版另证调用次数)。
|
|
480
|
-
const
|
|
481
|
-
const
|
|
452
|
+
const comparison = comparisonDataOf(resolved)!;
|
|
453
|
+
const scatterRows = comparison.groups.flatMap((group) => group.scatter.rows);
|
|
454
|
+
const experiments = comparison.groups.flatMap((group) => group.experiments);
|
|
482
455
|
|
|
483
456
|
const enText = renderNodeToText(resolved, createTextContext({ width: 100, locale: "en" }));
|
|
484
457
|
const zhText = renderNodeToText(resolved, createTextContext({ width: 100, locale: "zh-CN" }));
|
|
@@ -492,55 +465,61 @@ describe("场景 7:en / zh-CN 数据 resolve 一次、chrome 分别本地化", (
|
|
|
492
465
|
);
|
|
493
466
|
|
|
494
467
|
// chrome 分语言
|
|
495
|
-
expect(enText).toContain("
|
|
496
|
-
expect(zhText).toContain("
|
|
497
|
-
expect(enHtml).toContain("
|
|
498
|
-
expect(zhHtml).toContain("
|
|
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("端到端成功率");
|
|
499
472
|
expect(enText).not.toEqual(zhText);
|
|
500
473
|
|
|
501
474
|
// 但两语言下的事实(散点键 / x / y、attempt 深链)完全相同 —— 数据没被重算成不同结果。
|
|
502
475
|
expect(scatterKeysOf(zhHtml)).toEqual(scatterKeysOf(enHtml));
|
|
503
476
|
expect(hrefsOf(zhHtml)).toEqual(hrefsOf(enHtml));
|
|
504
477
|
// 从同一棵 resolved 树取的 data,en/zh 渲染不改变它
|
|
505
|
-
expect(
|
|
506
|
-
const [bub, codex, solo] =
|
|
478
|
+
expect(scatterRows.map((r) => r.key)).toEqual(["compare/bub-low", "compare/codex-mid", "solo/no-cost"]);
|
|
479
|
+
const [bub, codex, solo] = scatterRows;
|
|
507
480
|
expect(bub.x.value).toBeCloseTo(0.1, 10);
|
|
508
481
|
expect(bub.y.value).toBeCloseTo(0.75, 10);
|
|
509
482
|
expect(codex.x.value).toBeCloseTo(0.225, 10);
|
|
510
|
-
expect(codex.y.value).toBeCloseTo(0.5, 10);
|
|
483
|
+
expect(codex.y.value).toBeCloseTo(0.5, 10); // endToEndPassRate:errored=0,skipped=null,passed=1
|
|
511
484
|
expect(solo.x.value).toBeNull();
|
|
512
485
|
expect(solo.y.value).toBeCloseTo(0.5, 10);
|
|
486
|
+
// ExperimentList 按 endToEndPassRate 从高到低:bub-low(0.75)> codex-mid(0.5)=solo(0.5),同分按 id。
|
|
513
487
|
expect(experiments.map((e) => e.experimentId)).toEqual(["compare/bub-low", "compare/codex-mid", "solo/no-cost"]);
|
|
514
488
|
});
|
|
515
489
|
|
|
516
|
-
it("
|
|
517
|
-
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
expect(experimentListSpy).toHaveBeenCalledTimes(1);
|
|
531
|
-
const resolved = await resolveReportTree(node);
|
|
532
|
-
expect(scatterSpy).toHaveBeenCalledTimes(1);
|
|
533
|
-
expect(experimentListSpy).toHaveBeenCalledTimes(1);
|
|
534
|
-
|
|
535
|
-
// 渲染面纯同步、零 IO:两 locale 渲染同一棵 resolved 树,不再触发任何计算。
|
|
536
|
-
renderNodeToText(resolved, createTextContext({ locale: "en" }));
|
|
537
|
-
renderNodeToText(resolved, createTextContext({ locale: "zh-CN" }));
|
|
538
|
-
expect(scatterSpy).toHaveBeenCalledTimes(1);
|
|
539
|
-
expect(experimentListSpy).toHaveBeenCalledTimes(1);
|
|
540
|
-
} finally {
|
|
541
|
-
scatterSpy.mockRestore();
|
|
542
|
-
experimentListSpy.mockRestore();
|
|
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;
|
|
543
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);
|
|
544
523
|
});
|
|
545
524
|
});
|
|
546
525
|
|
|
@@ -563,19 +542,6 @@ describe("场景 9:最小用户报告只放 <ExperimentList> —— 不计算散
|
|
|
563
542
|
expect(experimentListOf(tree)).toBeDefined();
|
|
564
543
|
});
|
|
565
544
|
|
|
566
|
-
it("spy 佐证:scatterData 从未被调用,experimentListData 调一次", async () => {
|
|
567
|
-
const scatterSpy = vi.spyOn(compute, "scatterData");
|
|
568
|
-
// 见上一个 describe 块的注释:直接调用方读的是 ExperimentList.data 属性,打桩要打在这上面。
|
|
569
|
-
const experimentListSpy = vi.spyOn(ExperimentList, "data");
|
|
570
|
-
try {
|
|
571
|
-
await resolvedTreeOf(minimal, richContext());
|
|
572
|
-
expect(scatterSpy).not.toHaveBeenCalled();
|
|
573
|
-
expect(experimentListSpy).toHaveBeenCalledTimes(1);
|
|
574
|
-
} finally {
|
|
575
|
-
scatterSpy.mockRestore();
|
|
576
|
-
experimentListSpy.mockRestore();
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
545
|
|
|
580
546
|
it("用户换指标/维度无需框架新增分支:改 points/series 只改 resolved data,不改机制", async () => {
|
|
581
547
|
// 一份「用户自定义」报告:同一 MetricScatter 组件,换成 model 维度 + 只画一个指标轴组合。
|
|
@@ -598,46 +564,34 @@ describe("场景 9:最小用户报告只放 <ExperimentList> —— 不计算散
|
|
|
598
564
|
});
|
|
599
565
|
});
|
|
600
566
|
|
|
601
|
-
// ═════════════════════════ 7. 架构不变量:renderer
|
|
567
|
+
// ═════════════════════════ 7. 架构不变量:renderer 不依赖 built-in(import 依赖图)═════════════════════════
|
|
602
568
|
|
|
603
569
|
describe("架构不变量:renderer 不认识 built-in", () => {
|
|
604
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
|
+
|
|
605
588
|
for (const rel of RENDERER_FILES) {
|
|
606
|
-
it(`${rel}
|
|
607
|
-
const
|
|
608
|
-
expect(
|
|
609
|
-
|
|
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
|
+
}
|
|
610
595
|
});
|
|
611
596
|
}
|
|
612
597
|
});
|
|
613
|
-
|
|
614
|
-
// ═════════════════════════ 8. 宿主选择:show 索引与可替换报告槽分开 ═════════════════════════
|
|
615
|
-
|
|
616
|
-
describe("宿主选择:裸 show 是诊断索引,--report 与 view 走报告 definition", () => {
|
|
617
|
-
it("show / view 源码:裸 show 选择专用索引,--report 才装载 definition", () => {
|
|
618
|
-
const showSrc = readFileSync(new URL("../show/index.ts", import.meta.url), "utf8");
|
|
619
|
-
expect(showSrc).toMatch(/flags\.report === undefined[\s\S]*showIndexText\(selection, io\.width\)/);
|
|
620
|
-
expect(showSrc).toMatch(/const definition = await loadReportFile\(cwd, flags\.report\)/);
|
|
621
|
-
expect(showSrc).not.toMatch(/import \{ CostPassRateComparison \}/);
|
|
622
|
-
// Selection 在报告槽之前、与 report 无关地由 selectCurrentResults 合成。
|
|
623
|
-
expect(showSrc).toMatch(/selectCurrentResults\(results, \{ experiment: flags\.experiment, patterns \}\)/);
|
|
624
|
-
|
|
625
|
-
const viewSrc = readFileSync(new URL("../view/data.ts", import.meta.url), "utf8");
|
|
626
|
-
// view 的报告槽:report 有值走 loadReportFile,否则 CostPassRateComparison;Selection 同样先于报告槽合成。
|
|
627
|
-
expect(viewSrc).toMatch(/report\s*\?[\s\S]*loadReportFile[\s\S]*:\s*\(await import\([^)]*built-ins[^)]*\)\)\.CostPassRateComparison/);
|
|
628
|
-
expect(viewSrc).toMatch(/selectCurrentResults\(results, \{ experiment: opts\.experiment, patterns \}\)/);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it("CostPassRateComparison 与 public-copy 作为显式报告时事实完全相同", async () => {
|
|
632
|
-
for (const { ctx } of SCENARIOS) {
|
|
633
|
-
const bareText = await renderReportToText(CostPassRateComparison, ctx(), { width: 100 });
|
|
634
|
-
const reportText = await renderReportToText(publicCopy, ctx(), { width: 100 });
|
|
635
|
-
expect(reportText).toBe(bareText);
|
|
636
|
-
|
|
637
|
-
const bareHtml = await renderReportToStaticHtml(CostPassRateComparison, ctx());
|
|
638
|
-
const reportHtml = await renderReportToStaticHtml(publicCopy, ctx());
|
|
639
|
-
expect(scatterKeysOf(reportHtml)).toEqual(scatterKeysOf(bareHtml));
|
|
640
|
-
expect(hrefsOf(reportHtml)).toEqual(hrefsOf(bareHtml));
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
});
|