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/context/session.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { Agent, AgentContext, AgentSession, InputFile, InputRequest, InputResponse, Sandbox, StreamEvent, Telemetry, TraceSpan, Turn, Usage } from "../types.ts";
|
|
5
5
|
import type { AgentOtelChannel } from "../o11y/otlp/turn-otel.ts";
|
|
6
|
+
import { downgradeCoverage, resolveAgentCoverage, worstCoverage, type ResolvedCoverage } from "../scoring/coverage.ts";
|
|
6
7
|
import { captureLoc } from "../source-loc.ts";
|
|
7
8
|
import { t } from "../i18n/index.ts";
|
|
8
9
|
|
|
@@ -81,6 +82,10 @@ export class RunSession implements AgentSession {
|
|
|
81
82
|
readonly events: StreamEvent[] = [];
|
|
82
83
|
readonly pendingInputRequests: InputRequest[] = [];
|
|
83
84
|
readonly usage: Usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, requests: 0 };
|
|
85
|
+
/** 本会话累计的证据覆盖(初值 = Agent 级默认,逐轮按 Turn.coverage 降级折叠)。 */
|
|
86
|
+
coverage!: ResolvedCoverage;
|
|
87
|
+
/** 本会话内的轮次计数(turn 时间树 / 展示标签 s<session>/t<turn> 用)。 */
|
|
88
|
+
turnCount = 0;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
export interface SessionDeps {
|
|
@@ -91,6 +96,29 @@ export interface SessionDeps {
|
|
|
91
96
|
flags: Record<string, unknown>;
|
|
92
97
|
signal: AbortSignal;
|
|
93
98
|
log(msg: string): void;
|
|
99
|
+
/** runner 绑定的作用域反馈(adapter ctx.progress/diagnostic);省略时 progress 退回 log。 */
|
|
100
|
+
feedback?: import("../types.ts").ScopedFeedback;
|
|
101
|
+
/** adapter send 在飞时通知 runner(errored 归因到嵌套的 `agent.run` 阶段用)。 */
|
|
102
|
+
onSendActive?: (active: boolean) => void;
|
|
103
|
+
/**
|
|
104
|
+
* 变更分类账的 send 窗口钩子(仅沙箱型 agent):`beforeSend` 在 adapter send 前落 eval 归因
|
|
105
|
+
* commit,`afterSend` 在返回后落 agent 归因 commit;label 是 `s<session>/t<turn>` 窗口标签。
|
|
106
|
+
* 提供钩子时 send 自动串行(同一 workdir 上重叠的 send 是写入竞争,窗口不重叠)。
|
|
107
|
+
*/
|
|
108
|
+
ledgerHooks?: {
|
|
109
|
+
beforeSend(label: string): Promise<void>;
|
|
110
|
+
afterSend(label: string): Promise<void>;
|
|
111
|
+
};
|
|
112
|
+
/** 每轮 send 结束后回报墙钟包络(runner 挂成 eval.run 下的 turn 时间树节点)。 */
|
|
113
|
+
onTurn?: (info: {
|
|
114
|
+
sessionIndex: number;
|
|
115
|
+
turnIndex: number;
|
|
116
|
+
startedAt: number;
|
|
117
|
+
durationMs: number;
|
|
118
|
+
failed?: boolean;
|
|
119
|
+
traceId?: string;
|
|
120
|
+
traceAttribution?: "traceparent" | "window" | "none";
|
|
121
|
+
}) => void;
|
|
94
122
|
/** 路径推导出的实验 id(经 send ctx 透给 adapter,见 AgentContext.experimentId)。 */
|
|
95
123
|
experimentId?: string;
|
|
96
124
|
/** tracing agent 的 OTLP 端点(经 send ctx 透给 adapter,用于注入导出 env)。 */
|
|
@@ -104,6 +132,10 @@ export class SessionManager {
|
|
|
104
132
|
readonly allEvents: StreamEvent[] = [];
|
|
105
133
|
readonly usage: Usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, requests: 0 };
|
|
106
134
|
lastStatus: "completed" | "failed" | "waiting" = "completed";
|
|
135
|
+
/** Agent 级默认覆盖(全通道解析,未声明 = unknown)。 */
|
|
136
|
+
readonly agentCoverage: ResolvedCoverage;
|
|
137
|
+
/** attempt 级累计覆盖:各轮解析后覆盖的最差值,随每次 send 折叠。 */
|
|
138
|
+
coverage: ResolvedCoverage;
|
|
107
139
|
|
|
108
140
|
/** 归属到本 attempt 的 span(逐轮攒;attempt 末尾连同 sweep 的迟到 span 一起挂 trace)。 */
|
|
109
141
|
readonly otelSpans: TraceSpan[] = [];
|
|
@@ -116,18 +148,28 @@ export class SessionManager {
|
|
|
116
148
|
private readonly sessions: RunSession[] = [];
|
|
117
149
|
private turnCount = 0;
|
|
118
150
|
private sessionCount = 0;
|
|
151
|
+
/** 沙箱型 send 的串行链(见 SessionDeps.ledgerHooks):窗口不重叠。 */
|
|
152
|
+
private sendChain: Promise<unknown> = Promise.resolve();
|
|
119
153
|
|
|
120
154
|
constructor(private readonly deps: SessionDeps) {
|
|
155
|
+
this.agentCoverage = resolveAgentCoverage(deps.agent.coverage);
|
|
156
|
+
this.coverage = this.agentCoverage;
|
|
121
157
|
this.primary = this.newSession();
|
|
122
158
|
}
|
|
123
159
|
|
|
124
160
|
newSession(): RunSession {
|
|
125
161
|
const s = new RunSession();
|
|
126
162
|
s.index = ++this.sessionCount;
|
|
163
|
+
s.coverage = this.agentCoverage;
|
|
127
164
|
this.sessions.push(s);
|
|
128
165
|
return s;
|
|
129
166
|
}
|
|
130
167
|
|
|
168
|
+
/** 一轮的解析后覆盖:Agent 默认按 Turn.coverage 降级(只降不升)。 */
|
|
169
|
+
resolveTurnCoverage(turn: Turn): ResolvedCoverage {
|
|
170
|
+
return downgradeCoverage(this.agentCoverage, turn.coverage);
|
|
171
|
+
}
|
|
172
|
+
|
|
131
173
|
async send(
|
|
132
174
|
session: RunSession,
|
|
133
175
|
text: string,
|
|
@@ -136,6 +178,23 @@ export class SessionManager {
|
|
|
136
178
|
): Promise<Turn> {
|
|
137
179
|
// 抓住作者调 t.send / t.sendFile 那一行(view 把回复叠回这一行)。
|
|
138
180
|
const loc = captureLoc();
|
|
181
|
+
if (this.deps.ledgerHooks) {
|
|
182
|
+
// 沙箱型 send 经串行链执行:同一 workdir 上重叠的 send 本身就是写入竞争,
|
|
183
|
+
// 合并窗口只会掩盖归因不确定性(见 docs/feature/sandbox/architecture.md)。
|
|
184
|
+
const run = this.sendChain.then(() => this.sendSerialized(session, text, loc, files, responses));
|
|
185
|
+
this.sendChain = run.catch(() => {});
|
|
186
|
+
return run;
|
|
187
|
+
}
|
|
188
|
+
return this.sendSerialized(session, text, loc, files, responses);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async sendSerialized(
|
|
192
|
+
session: RunSession,
|
|
193
|
+
text: string,
|
|
194
|
+
loc: ReturnType<typeof captureLoc>,
|
|
195
|
+
files?: readonly InputFile[],
|
|
196
|
+
responses?: readonly InputResponse[],
|
|
197
|
+
): Promise<Turn> {
|
|
139
198
|
const ctx: AgentContext = {
|
|
140
199
|
signal: this.deps.signal,
|
|
141
200
|
model: this.deps.model,
|
|
@@ -145,6 +204,12 @@ export class SessionManager {
|
|
|
145
204
|
sandbox: this.deps.sandbox,
|
|
146
205
|
session,
|
|
147
206
|
telemetry: this.deps.telemetry,
|
|
207
|
+
progress: (u) =>
|
|
208
|
+
this.deps.feedback
|
|
209
|
+
? this.deps.feedback.progress(u)
|
|
210
|
+
: this.deps.log(u.current !== undefined && u.total !== undefined ? `${u.message} (${u.current}/${u.total})` : u.message),
|
|
211
|
+
diagnostic: (d) => this.deps.feedback?.diagnostic(d),
|
|
212
|
+
// log 是 progress({ message }) 的别名(见 AgentContext.log)。
|
|
148
213
|
log: this.deps.log,
|
|
149
214
|
};
|
|
150
215
|
|
|
@@ -162,9 +227,48 @@ export class SessionManager {
|
|
|
162
227
|
this.allEvents.push(userEvent);
|
|
163
228
|
session.events.push(userEvent);
|
|
164
229
|
session.pendingInputRequests.length = 0;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
230
|
+
const turnIndex = ++session.turnCount;
|
|
231
|
+
const windowLabel = `s${session.index}/t${turnIndex}`;
|
|
232
|
+
// send 进入前:workdir 有未记录变化(fixture / setup / runCommand 副作用)先落 eval 归因。
|
|
233
|
+
await this.deps.ledgerHooks?.beforeSend(windowLabel);
|
|
234
|
+
let turn: Turn;
|
|
235
|
+
let sentTraceId: string | undefined;
|
|
236
|
+
let sentAttribution: "traceparent" | "window" | "none" | undefined;
|
|
237
|
+
this.deps.onSendActive?.(true);
|
|
238
|
+
try {
|
|
239
|
+
if (this.deps.otel) {
|
|
240
|
+
const r = await this.sendWithOtel(this.deps.otel, { text, files, responses }, ctx);
|
|
241
|
+
turn = r.turn;
|
|
242
|
+
sentTraceId = r.traceId;
|
|
243
|
+
sentAttribution = r.attribution;
|
|
244
|
+
} else {
|
|
245
|
+
turn = await this.deps.agent.send({ text, files, responses }, ctx);
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
this.deps.onTurn?.({
|
|
249
|
+
sessionIndex: session.index,
|
|
250
|
+
turnIndex,
|
|
251
|
+
startedAt: t0,
|
|
252
|
+
durationMs: Date.now() - t0,
|
|
253
|
+
failed: true,
|
|
254
|
+
traceAttribution: sentAttribution,
|
|
255
|
+
});
|
|
256
|
+
throw e;
|
|
257
|
+
} finally {
|
|
258
|
+
this.deps.onSendActive?.(false);
|
|
259
|
+
// send 返回后:这个 send 窗口内的全部 workspace 变化落 agent 归因(HITL waiting 同样收窗:
|
|
260
|
+
// adapter 义务保证返回时 agent 侧进程已退出或进入不再写 workspace 的静止态)。
|
|
261
|
+
await this.deps.ledgerHooks?.afterSend(windowLabel).catch(() => {});
|
|
262
|
+
}
|
|
263
|
+
this.deps.onTurn?.({
|
|
264
|
+
sessionIndex: session.index,
|
|
265
|
+
turnIndex,
|
|
266
|
+
startedAt: t0,
|
|
267
|
+
durationMs: Date.now() - t0,
|
|
268
|
+
failed: turn.status === "failed" ? true : undefined,
|
|
269
|
+
traceId: sentTraceId,
|
|
270
|
+
traceAttribution: sentAttribution,
|
|
271
|
+
});
|
|
168
272
|
|
|
169
273
|
this.allEvents.push(...turn.events);
|
|
170
274
|
session.events.push(...turn.events);
|
|
@@ -177,6 +281,10 @@ export class SessionManager {
|
|
|
177
281
|
accumulateUsage(this.usage, turn.usage);
|
|
178
282
|
accumulateUsage(session.usage, turn.usage);
|
|
179
283
|
}
|
|
284
|
+
// 证据覆盖:attempt / session 级聚合取各轮最差值(见 scoring/coverage.ts)。
|
|
285
|
+
const turnCoverage = this.resolveTurnCoverage(turn);
|
|
286
|
+
this.coverage = worstCoverage([this.coverage, turnCoverage]);
|
|
287
|
+
session.coverage = worstCoverage([session.coverage, turnCoverage]);
|
|
180
288
|
session.lastStatus = turn.status;
|
|
181
289
|
this.lastStatus = turn.status;
|
|
182
290
|
const reply = lastAssistantText(turn.events);
|
|
@@ -200,7 +308,7 @@ export class SessionManager {
|
|
|
200
308
|
otel: AgentOtelChannel,
|
|
201
309
|
input: { text: string; files?: readonly InputFile[]; responses?: readonly InputResponse[] },
|
|
202
310
|
ctx: AgentContext,
|
|
203
|
-
): Promise<Turn> {
|
|
311
|
+
): Promise<{ turn: Turn; traceId: string; attribution: "traceparent" | "window" | "none" }> {
|
|
204
312
|
const r = await otel.runTurn((headers) => {
|
|
205
313
|
const turnCtx: AgentContext = ctx.telemetry
|
|
206
314
|
? { ...ctx, telemetry: { ...ctx.telemetry, headers } }
|
|
@@ -218,7 +326,7 @@ export class SessionManager {
|
|
|
218
326
|
this.warnedNoSpans = true;
|
|
219
327
|
this.deps.log(t("otel.noSpans"));
|
|
220
328
|
}
|
|
221
|
-
return r.result;
|
|
329
|
+
return { turn: r.result, traceId: r.traceId, attribution: r.spans.length === 0 ? "none" : r.attribution };
|
|
222
330
|
}
|
|
223
331
|
}
|
|
224
332
|
|
|
@@ -236,7 +344,7 @@ function failureReason(events: readonly StreamEvent[]): string | undefined {
|
|
|
236
344
|
return undefined;
|
|
237
345
|
}
|
|
238
346
|
|
|
239
|
-
/**
|
|
347
|
+
/** 单行截断:折叠空白 + 120 字符上限。 */
|
|
240
348
|
function truncateOneLine(s: string, width: number): string {
|
|
241
349
|
const clean = s.replace(/\s+/g, " ").trim();
|
|
242
350
|
if (clean.length <= width) return clean;
|
package/src/context/types.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// `t` 的形状按 Agent 能力组装(见 docs/architecture.md「能力决定形状」)。
|
|
3
3
|
|
|
4
4
|
import type { InputRequest, StreamEvent, ToolCall, Usage } from "../o11y/types.ts";
|
|
5
|
+
import type { DiagnosticInput, ProgressUpdate } from "../shared/types.ts";
|
|
5
6
|
import type { AssertionHandle, ValueAssertion } from "../scoring/types.ts";
|
|
6
7
|
import type {
|
|
7
8
|
CommandOptions,
|
|
@@ -11,6 +12,9 @@ import type {
|
|
|
11
12
|
SourceFiles,
|
|
12
13
|
} from "../sandbox/types.ts";
|
|
13
14
|
|
|
15
|
+
/** `t.send()` / `session.send()` 的入参:字符串,或带附件的结构化消息。 */
|
|
16
|
+
export type SendInput = string | { text: string; files?: readonly import("../agents/types.ts").InputFile[] };
|
|
17
|
+
|
|
14
18
|
/** t.send() 返回的句柄:从事件流派生便利字段 + expectOk。 */
|
|
15
19
|
export interface TurnHandle {
|
|
16
20
|
/** 本轮的原始事件流(工具调用、消息增量等);下面的派生字段都算自它。 */
|
|
@@ -59,8 +63,8 @@ export interface TurnHandle {
|
|
|
59
63
|
calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
|
|
60
64
|
/** 断言本轮事件按给定类型顺序出现(允许中间夹杂其它事件)。 */
|
|
61
65
|
eventOrder(types: StreamEvent["type"][]): AssertionHandle;
|
|
62
|
-
/**
|
|
63
|
-
eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean
|
|
66
|
+
/** 用自定义谓词对本轮整段事件流断言;label 必填、进断言标题(谓词不透明,解释责任在 label)。 */
|
|
67
|
+
eventsSatisfy(label: string, predicate: (events: readonly StreamEvent[]) => boolean): AssertionHandle;
|
|
64
68
|
/** 断言本轮 token 用量不超过 max。 */
|
|
65
69
|
maxTokens(max: number): AssertionHandle;
|
|
66
70
|
/** 断言本轮花费(USD)不超过 usd。 */
|
|
@@ -93,7 +97,11 @@ export interface DiffView {
|
|
|
93
97
|
|
|
94
98
|
/** 工具匹配小语言。 */
|
|
95
99
|
export interface ToolMatch {
|
|
96
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* 只匹配入参包含这些键值的调用:**深度部分匹配**——嵌套对象逐键下钻,数组按值比较;
|
|
102
|
+
* 值可以是 RegExp(对字符串字段测试,不命中时再对整个 input 的序列化串兜底测一次)
|
|
103
|
+
* 或谓词函数。不要求深度相等,多余的入参键不影响命中。
|
|
104
|
+
*/
|
|
97
105
|
input?: Record<string, unknown>;
|
|
98
106
|
/** 精确匹配调用次数,省略则只要求「至少一次」。 */
|
|
99
107
|
count?: number;
|
|
@@ -179,8 +187,8 @@ export interface SandboxHandle {
|
|
|
179
187
|
* 只看最后一轮要用 `send()` 返回的 TurnHandle。
|
|
180
188
|
*/
|
|
181
189
|
export interface SessionHandle {
|
|
182
|
-
/**
|
|
183
|
-
send(
|
|
190
|
+
/** 在这个会话里发一条消息(字符串或结构化消息),返回该轮的 TurnHandle。 */
|
|
191
|
+
send(input: SendInput): Promise<TurnHandle>;
|
|
184
192
|
/** 在这个会话里发一条带文件的消息,语义同 TestContext.sendFile。 */
|
|
185
193
|
sendFile(path: string, text?: string): Promise<TurnHandle>;
|
|
186
194
|
/** 取这个会话里等待中的 HITL 输入请求;拿不到就抛,语义同 TestContext.requireInputRequest。 */
|
|
@@ -223,8 +231,8 @@ export interface SessionHandle {
|
|
|
223
231
|
calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
|
|
224
232
|
/** 断言这个会话累计事件按给定类型顺序出现。 */
|
|
225
233
|
eventOrder(types: StreamEvent["type"][]): AssertionHandle;
|
|
226
|
-
/**
|
|
227
|
-
eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean
|
|
234
|
+
/** 用自定义谓词对这个会话累计的整段事件流断言;label 必填、进断言标题。 */
|
|
235
|
+
eventsSatisfy(label: string, predicate: (events: readonly StreamEvent[]) => boolean): AssertionHandle;
|
|
228
236
|
/** 断言这个会话累计 token 用量不超过 max。 */
|
|
229
237
|
maxTokens(max: number): AssertionHandle;
|
|
230
238
|
/** 断言这个会话累计花费(USD)不超过 usd。 */
|
|
@@ -241,8 +249,8 @@ export interface SessionHandle {
|
|
|
241
249
|
*/
|
|
242
250
|
export interface TestContext {
|
|
243
251
|
// 会话
|
|
244
|
-
/**
|
|
245
|
-
send(
|
|
252
|
+
/** 向默认会话发一条消息(字符串或结构化消息),返回该轮的 TurnHandle。事件同时累加进默认会话的累计事件流,供下面的作用域断言使用。 */
|
|
253
|
+
send(input: SendInput): Promise<TurnHandle>;
|
|
246
254
|
/** 发一条带文件(图片等多模态输入)的消息。`path` 相对项目根;读出后 base64 随 TurnInput.files 交给 adapter。 */
|
|
247
255
|
sendFile(path: string, text?: string): Promise<TurnHandle>;
|
|
248
256
|
/** 取默认会话里等待中的 HITL 输入请求;不传 filter 要求恰好一条,拿不到就抛。 */
|
|
@@ -276,7 +284,17 @@ export interface TestContext {
|
|
|
276
284
|
readonly reasoningEffort?: string;
|
|
277
285
|
/** 本次 attempt 生效的实验 flags(experiment.flags 的只读视图;实验条件,非命令行开关)。 */
|
|
278
286
|
readonly flags: Readonly<Record<string, unknown>>;
|
|
279
|
-
/**
|
|
287
|
+
/**
|
|
288
|
+
* 作用域反馈:报告 eval 自己执行的长步骤(上传 fixture、跑构建……)。短命状态,scope 固定
|
|
289
|
+
* 为 `eval.run`;只报告不断言(见 docs/feature/eval/library/context.md「向运行反馈长步骤」)。
|
|
290
|
+
*/
|
|
291
|
+
progress(update: ProgressUpdate): void;
|
|
292
|
+
/**
|
|
293
|
+
* 作用域反馈:报告运行结束后仍需保留的问题(永久事件,落 attempt diagnostics)。
|
|
294
|
+
* 即使 level 为 "error" 也不自动改变 verdict——测试结论仍由断言决定。
|
|
295
|
+
*/
|
|
296
|
+
diagnostic(input: DiagnosticInput): void;
|
|
297
|
+
/** `progress({ message: msg })` 的别名(调试日志),不出现在最终结果里。 */
|
|
280
298
|
log(msg: string): void;
|
|
281
299
|
/** 立即中止本 eval 并标记为 skipped(verdict / EvalResult.skipReason),reason 不能为空。 */
|
|
282
300
|
skip(reason: string): never;
|
|
@@ -330,8 +348,8 @@ export interface TestContext {
|
|
|
330
348
|
calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
|
|
331
349
|
/** 断言默认会话累计事件按给定类型顺序出现(允许中间夹杂其它事件)。 */
|
|
332
350
|
eventOrder(types: StreamEvent["type"][]): AssertionHandle;
|
|
333
|
-
/**
|
|
334
|
-
eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean
|
|
351
|
+
/** 用自定义谓词对默认会话累计的整段事件流断言;label 必填、进断言标题。 */
|
|
352
|
+
eventsSatisfy(label: string, predicate: (events: readonly StreamEvent[]) => boolean): AssertionHandle;
|
|
335
353
|
|
|
336
354
|
// 工作区 / 沙箱
|
|
337
355
|
/** 受限沙箱视图:能执行命令 / 读写文件 / 看最终 diff,不能 stop 沙箱本身(见 SandboxHandle)。 */
|
package/src/define.test.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/sandbox/cases.md
|
|
1
2
|
// SandboxSpec 链式钩子(.setup()/.teardown())的构造期契约:不可变、多次追加按顺序累加。
|
|
2
3
|
// 执行顺序(setup 正序 / teardown 逆序 / LIFO cleanup)是 runner 的事,见
|
|
3
4
|
// test/e2e-sandbox-hooks.test.ts;这里只测 dockerSandbox()/vercelSandbox()/e2bSandbox()/
|
|
4
5
|
// defineSandbox() 这四个工厂产出的 spec 对象本身的构造行为。
|
|
5
6
|
import { describe, expect, it } from "vitest";
|
|
6
7
|
import { dockerSandbox, e2bSandbox, vercelSandbox, defineSandbox } from "./define.ts";
|
|
7
|
-
|
|
8
|
+
// 从公开 subpath 取类型,守住下游共享 helper 的正式导入面。
|
|
9
|
+
import type { SandboxHook, SandboxHookContext } from "./sandbox/index.ts";
|
|
8
10
|
|
|
9
|
-
const noopSetup:
|
|
10
|
-
const noopTeardown:
|
|
11
|
+
const noopSetup: SandboxHook = () => {};
|
|
12
|
+
const noopTeardown: SandboxHook = () => {};
|
|
13
|
+
// subpath 类型导出的守护由 typecheck 完成:这两个类型标注编译不过即失败,无需运行时断言。
|
|
14
|
+
const _acceptsPublicHookContext = (_ctx: SandboxHookContext): void => {};
|
|
15
|
+
void _acceptsPublicHookContext;
|
|
11
16
|
|
|
12
17
|
describe("sandbox factories: .setup()/.teardown() chain", () => {
|
|
13
18
|
it("dockerSandbox() starts with empty hook arrays", () => {
|
|
@@ -27,15 +32,15 @@ describe("sandbox factories: .setup()/.teardown() chain", () => {
|
|
|
27
32
|
});
|
|
28
33
|
|
|
29
34
|
it("multiple .setup() calls accumulate in append order", () => {
|
|
30
|
-
const a:
|
|
31
|
-
const b:
|
|
35
|
+
const a: SandboxHook = () => {};
|
|
36
|
+
const b: SandboxHook = () => {};
|
|
32
37
|
const spec = dockerSandbox().setup(a).setup(b);
|
|
33
38
|
expect(spec.setupHooks).toEqual([a, b]);
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
it("multiple .teardown() calls accumulate in append order (execution order is the runner's job)", () => {
|
|
37
|
-
const x:
|
|
38
|
-
const y:
|
|
42
|
+
const x: SandboxHook = () => {};
|
|
43
|
+
const y: SandboxHook = () => {};
|
|
39
44
|
const spec = dockerSandbox().teardown(x).teardown(y);
|
|
40
45
|
expect(spec.teardownHooks).toEqual([x, y]);
|
|
41
46
|
});
|
|
@@ -72,7 +77,7 @@ describe("sandbox factories: .setup()/.teardown() chain", () => {
|
|
|
72
77
|
|
|
73
78
|
it("each chain call produces an independent object (no shared mutable state)", () => {
|
|
74
79
|
const base = dockerSandbox();
|
|
75
|
-
const other:
|
|
80
|
+
const other: SandboxHook = () => {};
|
|
76
81
|
const branchA = base.setup(noopSetup);
|
|
77
82
|
const branchB = base.setup(other);
|
|
78
83
|
expect(branchA.setupHooks).toEqual([noopSetup]);
|
package/src/define.ts
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
Agent,
|
|
6
|
-
AgentSetup,
|
|
7
|
-
AgentTeardown,
|
|
8
6
|
Config,
|
|
9
7
|
CustomSandboxSpec,
|
|
10
8
|
DockerSandboxSpec,
|
|
@@ -13,6 +11,7 @@ import type {
|
|
|
13
11
|
ExperimentDef,
|
|
14
12
|
RemoteAgentDef,
|
|
15
13
|
SandboxAgentDef,
|
|
14
|
+
SandboxHook,
|
|
16
15
|
SandboxHooks,
|
|
17
16
|
VercelSandboxSpec,
|
|
18
17
|
} from "./types.ts";
|
|
@@ -24,6 +23,7 @@ export function defineSandboxAgent(def: SandboxAgentDef): Agent {
|
|
|
24
23
|
return {
|
|
25
24
|
name: def.name,
|
|
26
25
|
kind: "sandbox",
|
|
26
|
+
coverage: def.coverage,
|
|
27
27
|
setup: def.setup,
|
|
28
28
|
tracing: def.tracing,
|
|
29
29
|
spanMapper: def.spanMapper,
|
|
@@ -38,6 +38,7 @@ export function defineAgent(def: RemoteAgentDef): Agent {
|
|
|
38
38
|
return {
|
|
39
39
|
name: def.name,
|
|
40
40
|
kind: "remote",
|
|
41
|
+
coverage: def.coverage,
|
|
41
42
|
setup: def.setup,
|
|
42
43
|
tracing: def.tracing,
|
|
43
44
|
spanMapper: def.spanMapper,
|
|
@@ -63,9 +64,26 @@ export function defineExperiment(def: ExperimentDef): ExperimentDef {
|
|
|
63
64
|
throw new Error(t("define.experimentIdRejected"));
|
|
64
65
|
}
|
|
65
66
|
if (!def.agent) throw new Error(t("define.experimentAgentRequired"));
|
|
67
|
+
// flags 必须可 JSON 序列化(进结果快照的 ExperimentRunInfo.flags):解析时即校验,
|
|
68
|
+
// 非 JSON 值(函数 / undefined / 循环引用 / bigint)直接报错,不等到落盘才炸。
|
|
69
|
+
if (def.flags !== undefined) {
|
|
70
|
+
for (const [key, value] of Object.entries(def.flags)) {
|
|
71
|
+
if (!isJsonValue(value)) {
|
|
72
|
+
throw new Error(t("define.experimentFlagNotJson", { key }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
66
76
|
return def;
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
function isJsonValue(v: unknown): boolean {
|
|
80
|
+
if (v === null || typeof v === "string" || typeof v === "boolean") return true;
|
|
81
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
82
|
+
if (Array.isArray(v)) return v.every(isJsonValue);
|
|
83
|
+
if (typeof v === "object") return Object.values(v as Record<string, unknown>).every(isJsonValue);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
/** 项目级配置。 */
|
|
70
88
|
export function defineConfig(config: Config): Config {
|
|
71
89
|
return config;
|
|
@@ -81,8 +99,8 @@ export function defineConfig(config: Config): Config {
|
|
|
81
99
|
|
|
82
100
|
/** 链式追加中累积的钩子(setup 按追加顺序执行,teardown 执行时逆序,见 SandboxHooks)。 */
|
|
83
101
|
interface HookState {
|
|
84
|
-
readonly setupHooks: readonly
|
|
85
|
-
readonly teardownHooks: readonly
|
|
102
|
+
readonly setupHooks: readonly SandboxHook[];
|
|
103
|
+
readonly teardownHooks: readonly SandboxHook[];
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
/** 四个工厂共用:把当前钩子状态包成 `.setup()` / `.teardown()` 方法,调用即用新状态重新 build。 */
|
|
@@ -148,6 +166,8 @@ export function defineSandbox(def: {
|
|
|
148
166
|
name: string;
|
|
149
167
|
create: CustomSandboxSpec["create"];
|
|
150
168
|
recommendedConcurrency?: number;
|
|
169
|
+
/** 可发布参数的投影(进结果快照);未实现时只落 provider 名。 */
|
|
170
|
+
publicConfig?: CustomSandboxSpec["publicConfig"];
|
|
151
171
|
}): CustomSandboxSpec {
|
|
152
172
|
if (!def.name) throw new Error(t("define.sandboxNameRequired"));
|
|
153
173
|
if (typeof def.create !== "function") throw new Error(t("define.sandboxCreateRequired"));
|
|
@@ -155,6 +175,7 @@ export function defineSandbox(def: {
|
|
|
155
175
|
provider: def.name,
|
|
156
176
|
create: def.create,
|
|
157
177
|
recommendedConcurrency: def.recommendedConcurrency,
|
|
178
|
+
publicConfig: def.publicConfig,
|
|
158
179
|
setupHooks: state.setupHooks,
|
|
159
180
|
teardownHooks: state.teardownHooks,
|
|
160
181
|
...hookMethods(state, build),
|
package/src/expect/index.ts
CHANGED
|
@@ -17,24 +17,31 @@ export interface MatchOptions {
|
|
|
17
17
|
// ───────────────────────── 内部工厂 ─────────────────────────
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* 唯一的内部工厂。gate()/atLeast() 都基于它返回新的不可变实例,
|
|
21
|
-
* 共享同一个 score(只换 severity / threshold)。
|
|
20
|
+
* 唯一的内部工厂。gate()/atLeast()/optional() 都基于它返回新的不可变实例,
|
|
21
|
+
* 共享同一个 score(只换 severity / threshold / isOptional)。
|
|
22
|
+
* `expected` 是期望条件的有界文本描述(如 `contains "Brooklyn"`),失败时进
|
|
23
|
+
* AssertionResult.expected 供 show/view 展示。
|
|
22
24
|
*/
|
|
23
25
|
function createAssertion(
|
|
24
26
|
name: string,
|
|
25
27
|
severity: Severity,
|
|
26
28
|
score: (value: unknown) => number | Promise<number>,
|
|
27
29
|
threshold?: number,
|
|
30
|
+
opts?: { expected?: string; isOptional?: boolean },
|
|
28
31
|
): ValueAssertion {
|
|
29
32
|
const self: ValueAssertion = {
|
|
30
33
|
name,
|
|
31
34
|
severity,
|
|
32
35
|
threshold,
|
|
36
|
+
...(opts?.isOptional ? { isOptional: true } : {}),
|
|
37
|
+
...(opts?.expected !== undefined ? { expected: opts.expected } : {}),
|
|
33
38
|
score,
|
|
34
39
|
// 转成硬门槛(失败即整条 eval 不通过)。
|
|
35
|
-
gate: (t?: number) => createAssertion(name, "gate", score, t),
|
|
40
|
+
gate: (t?: number) => createAssertion(name, "gate", score, t, opts),
|
|
36
41
|
// 软阈值:默认不改变 verdict;--strict 下软阈值失败也会使 verdict=failed。
|
|
37
|
-
atLeast: (t: number) => createAssertion(name, "soft", score, t),
|
|
42
|
+
atLeast: (t: number) => createAssertion(name, "soft", score, t, opts),
|
|
43
|
+
// 允许证据缺席:评不了只记 unavailable,不影响判定(与 severity 正交)。
|
|
44
|
+
optional: () => createAssertion(name, severity, score, threshold, { ...opts, isOptional: true }),
|
|
38
45
|
};
|
|
39
46
|
return Object.freeze(self);
|
|
40
47
|
}
|
|
@@ -86,28 +93,44 @@ function toMatchTarget(value: unknown, opts?: MatchOptions): string {
|
|
|
86
93
|
export function includes(needle: string | RegExp, opts?: MatchOptions): ValueAssertion {
|
|
87
94
|
const label = needle instanceof RegExp ? needle.toString() : safeLabel(needle);
|
|
88
95
|
const suffix = opts?.stripComments ? ", stripComments" : "";
|
|
89
|
-
return createAssertion(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
return createAssertion(
|
|
97
|
+
`includes(${label}${suffix})`,
|
|
98
|
+
"gate",
|
|
99
|
+
(value) => {
|
|
100
|
+
const s = toMatchTarget(value, opts);
|
|
101
|
+
if (needle instanceof RegExp) return needle.test(s) ? 1 : 0;
|
|
102
|
+
return s.includes(needle) ? 1 : 0;
|
|
103
|
+
},
|
|
104
|
+
undefined,
|
|
105
|
+
{ expected: needle instanceof RegExp ? `matches ${label}` : `contains ${label}` },
|
|
106
|
+
);
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
/** includes 的取反:不含子串 / 不命中正则则 1,否则 0。默认硬门槛。opts.stripComments 时只看真实代码。 */
|
|
97
110
|
export function excludes(needle: string | RegExp, opts?: MatchOptions): ValueAssertion {
|
|
98
111
|
const label = needle instanceof RegExp ? needle.toString() : safeLabel(needle);
|
|
99
112
|
const suffix = opts?.stripComments ? ", stripComments" : "";
|
|
100
|
-
return createAssertion(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
return createAssertion(
|
|
114
|
+
`excludes(${label}${suffix})`,
|
|
115
|
+
"gate",
|
|
116
|
+
(value) => {
|
|
117
|
+
const s = toMatchTarget(value, opts);
|
|
118
|
+
if (needle instanceof RegExp) return needle.test(s) ? 0 : 1;
|
|
119
|
+
return s.includes(needle) ? 0 : 1;
|
|
120
|
+
},
|
|
121
|
+
undefined,
|
|
122
|
+
{ expected: needle instanceof RegExp ? `does not match ${label}` : `does not contain ${label}` },
|
|
123
|
+
);
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
/** 深相等则 1,否则 0。默认硬门槛。 */
|
|
108
127
|
export function equals(expected: unknown): ValueAssertion {
|
|
109
|
-
return createAssertion(
|
|
110
|
-
|
|
128
|
+
return createAssertion(
|
|
129
|
+
`equals(${safeLabel(expected)})`,
|
|
130
|
+
"gate",
|
|
131
|
+
(value) => (deepEqual(value, expected) ? 1 : 0),
|
|
132
|
+
undefined,
|
|
133
|
+
{ expected: safeLabel(expected) },
|
|
111
134
|
);
|
|
112
135
|
}
|
|
113
136
|
|
|
@@ -138,6 +161,7 @@ export function similarity(expected: string): ValueAssertion {
|
|
|
138
161
|
return 1 - levenshtein(a, b) / maxLen;
|
|
139
162
|
},
|
|
140
163
|
0.6,
|
|
164
|
+
{ expected: safeLabel(expected) },
|
|
141
165
|
);
|
|
142
166
|
}
|
|
143
167
|
|
|
@@ -150,27 +174,33 @@ export function satisfies(predicate: (v: unknown) => boolean, label?: string): V
|
|
|
150
174
|
/** value 非 null / 非 undefined 则 1,否则 0。省掉 `x !== undefined` + isTrue 的样板。默认硬门槛。 */
|
|
151
175
|
export function isDefined(label?: string): ValueAssertion {
|
|
152
176
|
const name = label ? `isDefined(${label})` : "isDefined()";
|
|
153
|
-
return createAssertion(name, "gate", (value) => (value != null ? 1 : 0));
|
|
177
|
+
return createAssertion(name, "gate", (value) => (value != null ? 1 : 0), undefined, { expected: "defined" });
|
|
154
178
|
}
|
|
155
179
|
|
|
156
180
|
/** value === true 则 1,否则 0。带 label 的布尔断言(fileExists 等检查用)。默认硬门槛。 */
|
|
157
181
|
export function isTrue(label?: string): ValueAssertion {
|
|
158
182
|
const name = label ? `isTrue(${label})` : "isTrue()";
|
|
159
|
-
return createAssertion(name, "gate", (value) => (value === true ? 1 : 0));
|
|
183
|
+
return createAssertion(name, "gate", (value) => (value === true ? 1 : 0), undefined, { expected: "true" });
|
|
160
184
|
}
|
|
161
185
|
|
|
162
186
|
/** CommandResult.exitCode === 0 则 1,否则 0。默认硬门槛。 */
|
|
163
187
|
export function commandSucceeded(): ValueAssertion {
|
|
164
|
-
return createAssertion(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
188
|
+
return createAssertion(
|
|
189
|
+
"commandSucceeded()",
|
|
190
|
+
"gate",
|
|
191
|
+
(value) => {
|
|
192
|
+
if (value === null || typeof value !== "object") return 0;
|
|
193
|
+
return (value as { exitCode?: unknown }).exitCode === 0 ? 1 : 0;
|
|
194
|
+
},
|
|
195
|
+
undefined,
|
|
196
|
+
{ expected: "exit 0" },
|
|
197
|
+
);
|
|
168
198
|
}
|
|
169
199
|
|
|
170
200
|
/** value === false 则 1,否则 0。带 label 的布尔断言。默认硬门槛。 */
|
|
171
201
|
export function isFalse(label?: string): ValueAssertion {
|
|
172
202
|
const name = label ? `isFalse(${label})` : "isFalse()";
|
|
173
|
-
return createAssertion(name, "gate", (value) => (value === false ? 1 : 0));
|
|
203
|
+
return createAssertion(name, "gate", (value) => (value === false ? 1 : 0), undefined, { expected: "false" });
|
|
174
204
|
}
|
|
175
205
|
|
|
176
206
|
/**
|