niceeval 0.6.1 → 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 +67 -5
- 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 -1
- package/dist/report/built-ins/experiment-comparison.js +116 -10
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +1 -1
- package/dist/report/components.d.ts +8 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +54 -34
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -8
- package/dist/report/format.js +27 -12
- package/dist/report/index.d.ts +4 -3
- package/dist/report/index.js +5 -4
- package/dist/report/locale.d.ts +11 -2
- package/dist/report/locale.js +23 -5
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +65 -14
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +2 -2
- package/dist/report/react/AttemptList.js +5 -6
- 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 +2 -1
- package/dist/report/react/ExperimentList.js +8 -10
- package/dist/report/react/MetricScatter.js +5 -11
- 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 +21 -14
- 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 +42 -41
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +39 -21
- 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 +22 -4
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
- 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/official-adapters.mdx +26 -2
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +42 -30
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +17 -9
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +79 -36
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +17 -1
- 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 +33 -7
- package/docs-site/zh/reference/define-agent.mdx +57 -4
- 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/expect.mdx +26 -1
- 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 +68 -5
- 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 +64 -2
- package/src/i18n/zh-CN.ts +65 -3
- 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 +110 -153
- package/src/report/built-ins/experiment-comparison.tsx +173 -13
- package/src/report/built-ins/index.ts +6 -1
- package/src/report/components.tsx +9 -3
- package/src/report/compute.ts +70 -40
- package/src/report/dual-render.test.tsx +194 -67
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +35 -11
- package/src/report/index.ts +22 -4
- package/src/report/locale.ts +25 -5
- package/src/report/metrics.ts +67 -14
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +6 -31
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +15 -9
- package/src/report/react/MetricScatter.tsx +12 -14
- 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 +33 -1
- package/src/report/react/fixtures.ts +24 -17
- package/src/report/react/render.test.tsx +9 -64
- package/src/report/react/styles.css +73 -2
- package/src/report/report.test.ts +306 -98
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +47 -43
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +41 -21
- 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 +8 -6
- 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 +21 -1
- package/src/show/render.ts +619 -104
- package/src/show/show.test.ts +235 -19
- 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 +1 -0
- package/src/view/data.ts +11 -1
- package/src/view/index.ts +11 -0
- package/src/view/server.ts +2 -0
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +6 -5
- 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,3 +1,4 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/reports/cases.md
|
|
1
2
|
// Phase 4 验收:证明内置报告 ExperimentComparison 与一份普通用户 --report 文件走的是
|
|
2
3
|
// 完全同一套机制 —— 不是长得像,而是同一条 build → resolveReportTree → validate → render 管线。
|
|
3
4
|
//
|
|
@@ -9,17 +10,17 @@
|
|
|
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
26
|
import { ExperimentComparison } from "./built-ins/index.ts";
|
|
@@ -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();
|
|
@@ -289,17 +298,16 @@ describe("resolved 树结构化等价(built-in ≡ 包外 public-copy)", () => {
|
|
|
289
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
|
});
|
|
@@ -309,15 +317,19 @@ describe("resolved 树结构化等价(built-in ≡ 包外 public-copy)", () => {
|
|
|
309
317
|
describe("必测场景:resolved data 事实", () => {
|
|
310
318
|
it("场景 1+2:多 experiment/agent,成本与通过率都有值;缺成本的点如实 x=null,实验列表仍出项", async () => {
|
|
311
319
|
const tree = await resolvedTreeOf(ExperimentComparison, richContext());
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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(ExperimentComparison, oneDrawableContext());
|
|
346
|
-
const scatter = scatterOf(tree)!;
|
|
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 () => {
|
|
357
|
+
it("场景 4:空 Selection —— 没有可比组", async () => {
|
|
353
358
|
const tree = await resolvedTreeOf(ExperimentComparison, emptyContext());
|
|
354
|
-
expect(
|
|
355
|
-
expect(experimentListOf(tree)).toEqual([]);
|
|
359
|
+
expect(comparisonDataOf(tree)!.groups).toEqual([]);
|
|
356
360
|
});
|
|
357
361
|
|
|
358
362
|
it("场景 4 变体:全 skipped —— 每个点 x/y 都不可测,0 可画点", async () => {
|
|
359
363
|
const tree = await resolvedTreeOf(ExperimentComparison, allSkippedContext());
|
|
360
|
-
const scatter =
|
|
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
370
|
const tree = await resolvedTreeOf(ExperimentComparison, richContext());
|
|
367
|
-
const codex =
|
|
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(ExperimentComparison, 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 渲染的事实相同 ═════════════════════════
|
|
@@ -435,33 +428,15 @@ describe("built-in 与 public-copy 渲染出的事实相同", () => {
|
|
|
435
428
|
|
|
436
429
|
// ═════════════════════════ 4. 散点空 / 单点 / 多点行为(text + web 同一事实)═════════════════════════
|
|
437
430
|
|
|
438
|
-
describe("
|
|
439
|
-
it("
|
|
431
|
+
describe("分组空态 / 单点 / 多点行为", () => {
|
|
432
|
+
it("空 Selection:text / web 都显示没有实验组,不伪造空 scatter", async () => {
|
|
440
433
|
const text = await renderReportToText(ExperimentComparison, emptyContext(), { width: 100 });
|
|
441
|
-
expect(text).toContain("No
|
|
442
|
-
expect(text).not.toContain("better → upper right");
|
|
434
|
+
expect(text).toContain("No experiment groups");
|
|
443
435
|
const html = await renderReportToStaticHtml(ExperimentComparison, emptyContext());
|
|
444
|
-
expect(html).toContain("nre-
|
|
445
|
-
expect(html).toContain("nre-
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it("恰好 1 可画点:text/web 都正常画单点", async () => {
|
|
449
|
-
const text = await renderReportToText(ExperimentComparison, oneDrawableContext(), { width: 100 });
|
|
450
|
-
expect(text).toContain("better → upper right");
|
|
451
|
-
const html = await renderReportToStaticHtml(ExperimentComparison, oneDrawableContext());
|
|
452
|
-
expect(html).toContain("nre-scatter-point");
|
|
453
|
-
expect(html).not.toContain("nre-scatter-empty");
|
|
454
|
-
expect(html).not.toContain("nre-scatter-empty nre-missing");
|
|
455
|
-
expect(html).toContain("<svg");
|
|
436
|
+
expect(html).toContain("nre-experiment-groups-empty");
|
|
437
|
+
expect(html).not.toContain("nre-metric-scatter");
|
|
456
438
|
});
|
|
457
439
|
|
|
458
|
-
it("≥2 可画点:正常绘图(text 有 better → upper right,web 有 svg)", async () => {
|
|
459
|
-
const text = await renderReportToText(ExperimentComparison, richContext(), { width: 100 });
|
|
460
|
-
expect(text).toContain("better → upper right");
|
|
461
|
-
const html = await renderReportToStaticHtml(ExperimentComparison, richContext());
|
|
462
|
-
expect(html).toContain("<svg");
|
|
463
|
-
expect(html).not.toContain("nre-scatter-empty");
|
|
464
|
-
});
|
|
465
440
|
});
|
|
466
441
|
|
|
467
442
|
// ═════════════════════════ 5. 场景 7:数据只 resolve 一次,locale 只改 chrome ═════════════════════════
|
|
@@ -474,8 +449,9 @@ describe("场景 7:en / zh-CN 数据 resolve 一次、chrome 分别本地化", (
|
|
|
474
449
|
validateReportTree(resolved);
|
|
475
450
|
|
|
476
451
|
// 同一棵 resolved 树 → data 只算了一次(下方 spy 版另证调用次数)。
|
|
477
|
-
const
|
|
478
|
-
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);
|
|
479
455
|
|
|
480
456
|
const enText = renderNodeToText(resolved, createTextContext({ width: 100, locale: "en" }));
|
|
481
457
|
const zhText = renderNodeToText(resolved, createTextContext({ width: 100, locale: "zh-CN" }));
|
|
@@ -489,55 +465,61 @@ describe("场景 7:en / zh-CN 数据 resolve 一次、chrome 分别本地化", (
|
|
|
489
465
|
);
|
|
490
466
|
|
|
491
467
|
// chrome 分语言
|
|
492
|
-
expect(enText).toContain("
|
|
493
|
-
expect(zhText).toContain("
|
|
494
|
-
expect(enHtml).toContain("
|
|
495
|
-
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("端到端成功率");
|
|
496
472
|
expect(enText).not.toEqual(zhText);
|
|
497
473
|
|
|
498
474
|
// 但两语言下的事实(散点键 / x / y、attempt 深链)完全相同 —— 数据没被重算成不同结果。
|
|
499
475
|
expect(scatterKeysOf(zhHtml)).toEqual(scatterKeysOf(enHtml));
|
|
500
476
|
expect(hrefsOf(zhHtml)).toEqual(hrefsOf(enHtml));
|
|
501
477
|
// 从同一棵 resolved 树取的 data,en/zh 渲染不改变它
|
|
502
|
-
expect(
|
|
503
|
-
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;
|
|
504
480
|
expect(bub.x.value).toBeCloseTo(0.1, 10);
|
|
505
481
|
expect(bub.y.value).toBeCloseTo(0.75, 10);
|
|
506
482
|
expect(codex.x.value).toBeCloseTo(0.225, 10);
|
|
507
|
-
expect(codex.y.value).toBeCloseTo(0.5, 10);
|
|
483
|
+
expect(codex.y.value).toBeCloseTo(0.5, 10); // endToEndPassRate:errored=0,skipped=null,passed=1
|
|
508
484
|
expect(solo.x.value).toBeNull();
|
|
509
485
|
expect(solo.y.value).toBeCloseTo(0.5, 10);
|
|
486
|
+
// ExperimentList 按 endToEndPassRate 从高到低:bub-low(0.75)> codex-mid(0.5)=solo(0.5),同分按 id。
|
|
510
487
|
expect(experiments.map((e) => e.experimentId)).toEqual(["compare/bub-low", "compare/codex-mid", "solo/no-cost"]);
|
|
511
488
|
});
|
|
512
489
|
|
|
513
|
-
it("
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
expect(experimentListSpy).toHaveBeenCalledTimes(1);
|
|
528
|
-
const resolved = await resolveReportTree(node);
|
|
529
|
-
expect(scatterSpy).toHaveBeenCalledTimes(1);
|
|
530
|
-
expect(experimentListSpy).toHaveBeenCalledTimes(1);
|
|
531
|
-
|
|
532
|
-
// 渲染面纯同步、零 IO:两 locale 渲染同一棵 resolved 树,不再触发任何计算。
|
|
533
|
-
renderNodeToText(resolved, createTextContext({ locale: "en" }));
|
|
534
|
-
renderNodeToText(resolved, createTextContext({ locale: "zh-CN" }));
|
|
535
|
-
expect(scatterSpy).toHaveBeenCalledTimes(1);
|
|
536
|
-
expect(experimentListSpy).toHaveBeenCalledTimes(1);
|
|
537
|
-
} finally {
|
|
538
|
-
scatterSpy.mockRestore();
|
|
539
|
-
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;
|
|
540
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);
|
|
541
523
|
});
|
|
542
524
|
});
|
|
543
525
|
|
|
@@ -560,19 +542,6 @@ describe("场景 9:最小用户报告只放 <ExperimentList> —— 不计算散
|
|
|
560
542
|
expect(experimentListOf(tree)).toBeDefined();
|
|
561
543
|
});
|
|
562
544
|
|
|
563
|
-
it("spy 佐证:scatterData 从未被调用,experimentListData 调一次", async () => {
|
|
564
|
-
const scatterSpy = vi.spyOn(compute, "scatterData");
|
|
565
|
-
// 见上一个 describe 块的注释:直接调用方读的是 ExperimentList.data 属性,打桩要打在这上面。
|
|
566
|
-
const experimentListSpy = vi.spyOn(ExperimentList, "data");
|
|
567
|
-
try {
|
|
568
|
-
await resolvedTreeOf(minimal, richContext());
|
|
569
|
-
expect(scatterSpy).not.toHaveBeenCalled();
|
|
570
|
-
expect(experimentListSpy).toHaveBeenCalledTimes(1);
|
|
571
|
-
} finally {
|
|
572
|
-
scatterSpy.mockRestore();
|
|
573
|
-
experimentListSpy.mockRestore();
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
545
|
|
|
577
546
|
it("用户换指标/维度无需框架新增分支:改 points/series 只改 resolved data,不改机制", async () => {
|
|
578
547
|
// 一份「用户自定义」报告:同一 MetricScatter 组件,换成 model 维度 + 只画一个指标轴组合。
|
|
@@ -595,46 +564,34 @@ describe("场景 9:最小用户报告只放 <ExperimentList> —— 不计算散
|
|
|
595
564
|
});
|
|
596
565
|
});
|
|
597
566
|
|
|
598
|
-
// ═════════════════════════ 7. 架构不变量:renderer
|
|
567
|
+
// ═════════════════════════ 7. 架构不变量:renderer 不依赖 built-in(import 依赖图)═════════════════════════
|
|
599
568
|
|
|
600
569
|
describe("架构不变量:renderer 不认识 built-in", () => {
|
|
601
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
|
+
|
|
602
588
|
for (const rel of RENDERER_FILES) {
|
|
603
|
-
it(`${rel}
|
|
604
|
-
const
|
|
605
|
-
expect(
|
|
606
|
-
|
|
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
|
+
}
|
|
607
595
|
});
|
|
608
596
|
}
|
|
609
597
|
});
|
|
610
|
-
|
|
611
|
-
// ═════════════════════════ 8. 宿主选择:show / view 共用默认 definition ═════════════════════════
|
|
612
|
-
|
|
613
|
-
describe("宿主选择:裸 show / view 共用 ExperimentComparison,--report 替换 definition", () => {
|
|
614
|
-
it("show / view 源码:Selection 先合成,随后选择内置或用户 definition", () => {
|
|
615
|
-
const showSrc = readFileSync(new URL("../show/index.ts", import.meta.url), "utf8");
|
|
616
|
-
expect(showSrc).toMatch(/import \{ ExperimentComparison \} from "\.\.\/\.\.\/dist\/report\/built-ins\/index\.js"/);
|
|
617
|
-
expect(showSrc).toMatch(/flags\.report === undefined \? ExperimentComparison : await loadReportFile\(cwd, flags\.report\)/);
|
|
618
|
-
expect(showSrc).not.toContain("showIndexText(selection");
|
|
619
|
-
// Selection 在报告槽之前、与 report 无关地由 selectCurrentResults 合成。
|
|
620
|
-
expect(showSrc).toMatch(/selectCurrentResults\(results, \{ experiment: flags\.experiment, patterns \}\)/);
|
|
621
|
-
|
|
622
|
-
const viewSrc = readFileSync(new URL("../view/data.ts", import.meta.url), "utf8");
|
|
623
|
-
// view 的报告槽:report 有值走 loadReportFile,否则 ExperimentComparison;Selection 同样先于报告槽合成。
|
|
624
|
-
expect(viewSrc).toMatch(/report\s*\?[\s\S]*loadReportFile[\s\S]*:\s*\(await import\([^)]*built-ins[^)]*\)\)\.ExperimentComparison/);
|
|
625
|
-
expect(viewSrc).toMatch(/selectCurrentResults\(results, \{ experiment: opts\.experiment, patterns \}\)/);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
it("ExperimentComparison 与 public-copy 作为显式报告时事实完全相同", async () => {
|
|
629
|
-
for (const { ctx } of SCENARIOS) {
|
|
630
|
-
const bareText = await renderReportToText(ExperimentComparison, ctx(), { width: 100 });
|
|
631
|
-
const reportText = await renderReportToText(publicCopy, ctx(), { width: 100 });
|
|
632
|
-
expect(reportText).toBe(bareText);
|
|
633
|
-
|
|
634
|
-
const bareHtml = await renderReportToStaticHtml(ExperimentComparison, ctx());
|
|
635
|
-
const reportHtml = await renderReportToStaticHtml(publicCopy, ctx());
|
|
636
|
-
expect(scatterKeysOf(reportHtml)).toEqual(scatterKeysOf(bareHtml));
|
|
637
|
-
expect(hrefsOf(reportHtml)).toEqual(hrefsOf(bareHtml));
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
});
|