@tea-agent/loop-agent 0.24.7 → 0.24.9
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/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.24.9] - 2026-07-29
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 后端测试执行失败时的诊断信息更加完善,避免出现无输出导致无法定位的问题
|
|
10
|
+
- 修复 Windows 环境下前端证据校验因长命令终端转义导致的崩溃
|
|
11
|
+
- 节点检查器新增「节点输入」只读页签,支持安全审查冻结的节点定义
|
|
12
|
+
|
|
5
13
|
### 新增
|
|
6
14
|
|
|
7
|
-
- Observe / Inspect 节点检查器在「节点输出」左侧新增「节点输入」页签:只读投影冻结
|
|
15
|
+
- Observe / Inspect 节点检查器在「节点输出」左侧新增「节点输入」页签:只读投影冻结 run.json 顶层节点定义(任务正文、依赖、执行器摘要、边界)与可选 assembled prompt 指纹
|
|
16
|
+
|
|
17
|
+
### 改进
|
|
18
|
+
|
|
19
|
+
- 后端测试节点执行失败时可观测性增强:写入包含映射脚本、PYTHON_BIN、pytest 版本、exit、html 是否存在与 STEP 等信息的诊断文件,并在节点 stderr 汇总失败摘要
|
|
20
|
+
|
|
21
|
+
### 修复
|
|
22
|
+
|
|
23
|
+
- 修复前端证据校验在 Windows 下通过长 node -e 命令执行时的终端转义崩溃:改为由 runtime 内部直接校验,兼容 CMD、Git Bash、PowerShell 与 Unix shell
|
|
24
|
+
|
|
25
|
+
## [0.24.8] - 2026-07-29
|
|
26
|
+
|
|
27
|
+
### 重点更新
|
|
28
|
+
|
|
29
|
+
- 修复 FINAL-VERIFY 改写已存在的 allowed_paths 证据(gitignore)时 Git checkpoint 误失败,以及 record-error 后 Task Pool 卡在 Running 无法 mark-failed/retry 的问题
|
|
8
30
|
|
|
9
31
|
### 修复
|
|
10
32
|
|
|
11
|
-
-
|
|
33
|
+
- `finalizeGitTask` 对 allowed_paths 内的 pre-existing ignored evidence 允许 rewrite 并 force-add
|
|
34
|
+
- `runReadyTasks` 在 record-error 时 best-effort 投影 `runs.jsonl` + Failed 状态,便于官方 retry
|
|
12
35
|
|
|
13
36
|
## [0.24.7] - 2026-07-29
|
|
14
37
|
|
|
@@ -385,28 +385,101 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
385
385
|
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
386
386
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
387
387
|
const pytestTargets = mappedScripts.map(shellQuote).join(" ");
|
|
388
|
+
const mappedScriptDiagLines = mappedScripts
|
|
389
|
+
.map((script) => `echo " - ${script.replaceAll('"', '\\"')}" >> "\${DIAG_FILE}"`)
|
|
390
|
+
.join("; ");
|
|
391
|
+
// Diagnostic-first single shell: keep pytest exactly once, but always
|
|
392
|
+
// emit STEP markers, mapped targets, interpreter identity, exit code
|
|
393
|
+
// and html presence so empty nonzero-exit failures remain auditable.
|
|
388
394
|
const pytestCommand = [
|
|
389
395
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
396
|
+
'DIAG_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-execute-diagnostics.md"',
|
|
397
|
+
[
|
|
398
|
+
'echo "# Backend Test Execute Diagnostics" > "${DIAG_FILE}"',
|
|
399
|
+
'echo >> "${DIAG_FILE}"',
|
|
400
|
+
'echo "- startedAt: $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)" >> "${DIAG_FILE}"',
|
|
401
|
+
'echo "- pwd: $(pwd)" >> "${DIAG_FILE}"',
|
|
402
|
+
'echo "- HARNESS_DAG_RUN_DIR: ${HARNESS_DAG_RUN_DIR}" >> "${DIAG_FILE}"',
|
|
403
|
+
'echo "- shell: ${BASH:-bash} (${BASH_VERSION:-unknown})" >> "${DIAG_FILE}"',
|
|
404
|
+
`echo "- mappedScriptCount: ${mappedScripts.length}" >> "\${DIAG_FILE}"`,
|
|
405
|
+
'echo "- mappedScripts:" >> "${DIAG_FILE}"',
|
|
406
|
+
].join("; "),
|
|
407
|
+
mappedScriptDiagLines || 'echo " - <none>" >> "${DIAG_FILE}"',
|
|
408
|
+
'echo "STEP=resolve-python"',
|
|
390
409
|
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
391
|
-
|
|
410
|
+
[
|
|
411
|
+
'echo "- command_v_python: $(command -v python || true)" >> "${DIAG_FILE}"',
|
|
412
|
+
'echo "- command_v_python3: $(command -v python3 || true)" >> "${DIAG_FILE}"',
|
|
413
|
+
'echo "- PYTHON_BIN: ${PYTHON_BIN:-<empty>}" >> "${DIAG_FILE}"',
|
|
414
|
+
].join("; "),
|
|
415
|
+
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
416
|
+
'echo "STEP=python-identity"',
|
|
417
|
+
'if ! PYTHONUNBUFFERED=1 "${PYTHON_BIN}" -c "import sys; print(sys.executable); print(sys.version.splitlines()[0])" >> "${DIAG_FILE}" 2>&1; then echo "STEP=python-identity-failed" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
418
|
+
'echo "STEP=pytest-version"',
|
|
419
|
+
'if ! PYTHONUNBUFFERED=1 "${PYTHON_BIN}" -m pytest --version >> "${DIAG_FILE}" 2>&1; then echo "STEP=pytest-version-failed" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
420
|
+
'echo "STEP=pytest-run"',
|
|
421
|
+
`echo "- pytestCommand: python -m pytest ${mappedScripts.join(" ")} -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
|
|
392
422
|
// Run pytest exactly once and emit the native pytest-html
|
|
393
423
|
// self-contained report. The HTML/facts renderers read per-case
|
|
394
424
|
// captured stdout (HTTP_REQUEST/HTTP_RESPONSE) directly from the
|
|
395
425
|
// pytest-html data-jsonblob island, so a JUnit XML report is no
|
|
396
426
|
// longer generated.
|
|
397
|
-
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
|
|
427
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
|
|
398
428
|
"STATUS=$?",
|
|
399
429
|
'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
430
|
+
[
|
|
431
|
+
'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
|
|
432
|
+
'if [ -f "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ]; then echo "- pytestHtml: present ($(wc -c < "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" | tr -d " ") bytes)" >> "${DIAG_FILE}"; else echo "- pytestHtml: missing" >> "${DIAG_FILE}"; fi',
|
|
433
|
+
'echo "- finishedAt: $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)" >> "${DIAG_FILE}"',
|
|
434
|
+
].join("; "),
|
|
435
|
+
'echo "STEP=pytest-finished status=${STATUS}"',
|
|
400
436
|
// exit 0 (all pass) or 1 (assertion failures) with a valid
|
|
401
437
|
// pytest-html report are reportable; collection errors / crashes
|
|
402
438
|
// (exit >= 2) or a missing report surface as a pipeline error below.
|
|
403
439
|
'if { [ "${STATUS}" -eq 0 ] || [ "${STATUS}" -eq 1 ]; } && [ -s "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ]; then exit 0; fi',
|
|
440
|
+
'echo "STEP=pipeline-failed status=${STATUS} html=$([ -s "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ] && echo present || echo missing)" >&2',
|
|
404
441
|
'exit "${STATUS}"',
|
|
405
442
|
].join("; ");
|
|
406
443
|
const results = await executePipelineCommands(input, meta, [pytestCommand]);
|
|
407
444
|
if (!results.every((result) => result.ok)) {
|
|
408
445
|
const failure = results.find((result) => !result.ok);
|
|
409
|
-
|
|
446
|
+
const combinedStdout = results.map((result) => result.stdout).join("\n");
|
|
447
|
+
let diagnosticsText = "";
|
|
448
|
+
try {
|
|
449
|
+
diagnosticsText = await readFile(path.join(meta.runDir, "reports", "backend-test-execute-diagnostics.md"), "utf8");
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
diagnosticsText = "";
|
|
453
|
+
}
|
|
454
|
+
const stderrSummary = [
|
|
455
|
+
"backend-test markdown-execute-html shell failed",
|
|
456
|
+
`failureCategory=${failure.failureCategory}`,
|
|
457
|
+
`exitCode=${failure.exitCode ?? "null"}`,
|
|
458
|
+
`durationMs=${failure.durationMs}`,
|
|
459
|
+
`mappedScripts=${mappedScripts.join(",") || "<none>"}`,
|
|
460
|
+
`shellStdoutEmpty=${combinedStdout.trim().length === 0}`,
|
|
461
|
+
`shellStderrEmpty=${failure.stderr.trim().length === 0}`,
|
|
462
|
+
failure.stdoutArtifactPath
|
|
463
|
+
? `commandStdoutArtifact=${failure.stdoutArtifactPath}`
|
|
464
|
+
: undefined,
|
|
465
|
+
failure.stderrArtifactPath
|
|
466
|
+
? `commandStderrArtifact=${failure.stderrArtifactPath}`
|
|
467
|
+
: undefined,
|
|
468
|
+
"diagnostics=reports/backend-test-execute-diagnostics.md",
|
|
469
|
+
failure.stderr.trim() || "(shell stderr empty)",
|
|
470
|
+
diagnosticsText.trim()
|
|
471
|
+
? `--- diagnostics ---\n${diagnosticsText.trim()}`
|
|
472
|
+
: "(diagnostics file missing or empty)",
|
|
473
|
+
]
|
|
474
|
+
.filter((line) => Boolean(line))
|
|
475
|
+
.join("\n");
|
|
476
|
+
return {
|
|
477
|
+
ok: false,
|
|
478
|
+
stdout: combinedStdout,
|
|
479
|
+
stderr: stderrSummary,
|
|
480
|
+
failureCategory: failure.failureCategory,
|
|
481
|
+
durationMs: Date.now() - started,
|
|
482
|
+
};
|
|
410
483
|
}
|
|
411
484
|
const reportsDir = path.join(meta.runDir, "reports");
|
|
412
485
|
const pytestHtmlContent = await readFile(path.join(reportsDir, "backend-test.html"), "utf8");
|
|
@@ -101,11 +101,12 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
101
101
|
await assertTransactionPosition(transaction.repoRoot, current);
|
|
102
102
|
const currentIgnored = await readIgnoredBaseline(transaction.repoRoot);
|
|
103
103
|
const ignoredBaselineByPath = new Map(current.ignoredBaseline.map((entry) => [entry.path, entry.sha256]));
|
|
104
|
-
const changedBaselineIgnored = currentIgnored
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
const changedBaselineIgnored = currentIgnored
|
|
105
|
+
.filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256)
|
|
106
|
+
.map((entry) => entry.path);
|
|
107
|
+
const missingBaselineIgnored = current.ignoredBaseline
|
|
108
|
+
.filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path))
|
|
109
|
+
.map((entry) => entry.path);
|
|
109
110
|
const existing = current.checkpoints.find((entry) => entry.workerRunId === outcome.workerRunId);
|
|
110
111
|
if (outcome.status === "reused" || existing) {
|
|
111
112
|
if (!existing)
|
|
@@ -123,12 +124,28 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
123
124
|
const newIgnored = currentIgnored.map((entry) => entry.path).filter((entry) => !ignoredBaselineByPath.has(entry));
|
|
124
125
|
if (outcome.status === "succeeded") {
|
|
125
126
|
const allowedPaths = expandAllowedPathsForWorkflow(resolveWorkflow(outcome.taskSpec).workflow, outcome.taskSpec.constraints.allowed_paths);
|
|
127
|
+
const isAllowedEvidencePath = (entry) => !isSensitivePath(entry) &&
|
|
128
|
+
!isEphemeralToolCachePath(entry) &&
|
|
129
|
+
allowedPaths.some((glob) => matchesGlob(entry, glob));
|
|
130
|
+
// Pre-existing ignored evidence under allowed_paths may be rewritten by
|
|
131
|
+
// FINAL-VERIFY smoke / verify-final and must force-add into the checkpoint.
|
|
132
|
+
// Other baseline ignored mutations remain fail-closed (e.g. dist/, secrets).
|
|
133
|
+
const blockedBaselineChanged = changedBaselineIgnored.filter((entry) => !isAllowedEvidencePath(entry));
|
|
134
|
+
const blockedBaselineMissing = missingBaselineIgnored.filter((entry) => !isEphemeralToolCachePath(entry) && !isAllowedEvidencePath(entry));
|
|
135
|
+
if (blockedBaselineChanged.length > 0 || blockedBaselineMissing.length > 0) {
|
|
136
|
+
throw new Error(`pre-existing ignored file changed during Git transaction: ${[...blockedBaselineChanged, ...blockedBaselineMissing].join(", ")}`);
|
|
137
|
+
}
|
|
126
138
|
// Evidence under allowed_paths may be gitignored for local convenience (e.g.
|
|
127
139
|
// reports/welcome/** smoke JSON) but must still checkpoint for Task Pool Done.
|
|
128
140
|
// Sensitive / out-of-scope ignored files remain fail-closed; ephemeral tool
|
|
129
141
|
// caches neither block nor force-add.
|
|
130
142
|
const forceAddIgnored = [];
|
|
131
143
|
const blockedIgnored = [];
|
|
144
|
+
const rewrittenBaselineEvidence = changedBaselineIgnored.filter((entry) => isAllowedEvidencePath(entry));
|
|
145
|
+
for (const entry of rewrittenBaselineEvidence) {
|
|
146
|
+
if (!forceAddIgnored.includes(entry))
|
|
147
|
+
forceAddIgnored.push(entry);
|
|
148
|
+
}
|
|
132
149
|
for (const entry of newIgnored) {
|
|
133
150
|
if (isEphemeralToolCachePath(entry))
|
|
134
151
|
continue;
|
|
@@ -137,7 +154,8 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
137
154
|
continue;
|
|
138
155
|
}
|
|
139
156
|
if (allowedPaths.some((glob) => matchesGlob(entry, glob))) {
|
|
140
|
-
forceAddIgnored.
|
|
157
|
+
if (!forceAddIgnored.includes(entry))
|
|
158
|
+
forceAddIgnored.push(entry);
|
|
141
159
|
}
|
|
142
160
|
else {
|
|
143
161
|
blockedIgnored.push(entry);
|
|
@@ -166,7 +184,10 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
166
184
|
await assertTransactionPosition(transaction.repoRoot, { ...current, lastCheckpoint: commit });
|
|
167
185
|
await assertCheckpointMetadata(transaction.repoRoot, commit, outcome.taskSpec, outcome.workerRunId);
|
|
168
186
|
const afterCommitIgnored = await readIgnoredBaseline(transaction.repoRoot);
|
|
169
|
-
|
|
187
|
+
// force-add may promote allowed evidence from ignored → tracked; drop those
|
|
188
|
+
// paths from the post-commit baseline equality check.
|
|
189
|
+
const forceAdded = new Set(forceAddIgnored);
|
|
190
|
+
assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)));
|
|
170
191
|
}
|
|
171
192
|
catch (error) {
|
|
172
193
|
const branch = await git(transaction.repoRoot, ["branch", "--show-current"]).catch(() => "");
|
|
@@ -199,6 +220,10 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
199
220
|
changedFiles: checkpointFiles,
|
|
200
221
|
};
|
|
201
222
|
}
|
|
223
|
+
// Failed / keep-diff paths still fail closed on unexpected baseline ignored mutations.
|
|
224
|
+
if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
|
|
225
|
+
throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
|
|
226
|
+
}
|
|
202
227
|
const artifactDir = path.join(path.dirname(transaction.recordPath), "failures", outcome.workerRunId);
|
|
203
228
|
await captureFailureArtifacts(transaction.repoRoot, artifactDir, changes, newIgnored, outcome);
|
|
204
229
|
if (outcome.keepFailedDiff) {
|
|
@@ -304,11 +304,51 @@ export async function runReadyTasks(options) {
|
|
|
304
304
|
if (result.status !== "succeeded") {
|
|
305
305
|
await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
|
|
306
306
|
}
|
|
307
|
+
const recordErrorMessage = errorMessage(error);
|
|
308
|
+
// Always project a Pool run fact for record-error so operators can
|
|
309
|
+
// mark-failed / retry without hand-editing Running state.
|
|
310
|
+
try {
|
|
311
|
+
await recordTaskPoolRun({
|
|
312
|
+
repoRoot: options.repoRoot,
|
|
313
|
+
run: {
|
|
314
|
+
schemaVersion: 1,
|
|
315
|
+
batchRunId,
|
|
316
|
+
workerRunId: result.workerRunId,
|
|
317
|
+
taskId: result.businessId,
|
|
318
|
+
featureId: taskSpec.feature_id,
|
|
319
|
+
status: "run-error",
|
|
320
|
+
recordedAt: new Date().toISOString(),
|
|
321
|
+
error: recordErrorMessage,
|
|
322
|
+
runRecordPath: result.runRecordPath,
|
|
323
|
+
...(result.workflow ? { workflow: result.workflow } : {}),
|
|
324
|
+
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
325
|
+
...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
await writeTaskPoolState(options.repoRoot, {
|
|
329
|
+
schemaVersion: 2,
|
|
330
|
+
featureId: taskSpec.feature_id,
|
|
331
|
+
taskId,
|
|
332
|
+
status: "Failed",
|
|
333
|
+
updatedAt: new Date().toISOString(),
|
|
334
|
+
workerRunId: result.workerRunId,
|
|
335
|
+
lastRunRecordPath: result.runRecordPath,
|
|
336
|
+
failure: {
|
|
337
|
+
category: "EnvFailure",
|
|
338
|
+
recommendedFollowUpKind: "manual-review",
|
|
339
|
+
derivedFollowUpTaskId: `${taskId}-record-error-review`,
|
|
340
|
+
source: "report-decision",
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Best-effort; batch still reports record-error.
|
|
346
|
+
}
|
|
307
347
|
tasks.push({
|
|
308
348
|
taskId,
|
|
309
349
|
workerRunId: result.workerRunId,
|
|
310
350
|
status: "record-error",
|
|
311
|
-
error:
|
|
351
|
+
error: recordErrorMessage,
|
|
312
352
|
runRecordPath: result.runRecordPath,
|
|
313
353
|
});
|
|
314
354
|
break;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>测试成熟度报告 · L-5</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{--page:#f5f7fb;--surface:#fff;--surface-2:#f8fafd;--ink:#17243b;--muted:#718097;--line:#e4e9f1;--navy:#17365d;--blue:#4775ef;--blue-soft:#edf3ff;--green:#15815c;--green-soft:#e7f7f0;--amber:#c87918;--amber-soft:#fff4e5;--red:#d34661;--red-soft:#fff0f3;--shadow:0 8px 24px rgba(24,54,93,.055)}
|
|
9
|
+
*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 8% 0,#edf4ff 0,transparent 36rem),var(--page);color:var(--ink);font:15px/1.6 Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}
|
|
10
|
+
main{width:min(1180px,calc(100% - 56px));margin:auto;padding:42px 0 72px}.topbar{display:flex;align-items:flex-start;justify-content:space-between;gap:28px}.eyebrow{color:#71809a;font-size:12px;font-weight:800;letter-spacing:.16em}.title{margin:14px 0 8px;color:#101d34;font-size:clamp(34px,4.2vw,52px);font-weight:800;letter-spacing:-.055em;line-height:1.1}.meta{margin:0;color:var(--muted);font-size:15px}.decision{min-width:236px;padding:15px 18px;border:1px solid #efbdc8;border-radius:18px;background:linear-gradient(135deg,#fff8f9,#fff1f3);color:#c53b58}.decision-top{display:flex;align-items:center;gap:9px;font-size:17px;font-weight:800}.decision-dot{width:11px;height:11px;border-radius:50%;background:var(--red);box-shadow:0 0 0 5px rgba(211,70,97,.1)}.decision small{display:block;margin:7px 0 0 20px;color:#a95e70;font-size:12px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:32px}.stat{position:relative;min-height:146px;padding:23px 24px;overflow:hidden;background:var(--surface);border:1px solid var(--line);border-radius:17px;box-shadow:var(--shadow)}.stat:before{content:"";position:absolute;top:0;left:0;right:0;height:4px;background:var(--blue)}.stat:nth-child(2):before{background:var(--amber)}.stat:nth-child(3):before{background:#7589d9}.stat:nth-child(4):before{background:var(--green)}.stat-label{color:#748198;font-size:15px;font-weight:650}.stat-value{margin-top:23px;color:#13213a;font-size:38px;font-weight:800;letter-spacing:-.06em;line-height:1}.stat-foot{margin-top:9px;color:var(--muted);font-size:13px}.stat-foot.warn{color:var(--amber)}.stat-foot.good{color:var(--green)}.layout{display:grid;grid-template-columns:1.28fr .86fr;gap:20px;margin-top:20px}.panel{background:var(--surface);border:1px solid var(--line);border-radius:17px;box-shadow:var(--shadow)}.panel-pad{padding:24px}.panel-heading{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:21px}.panel-heading h2{margin:0;color:var(--navy);font-size:21px;letter-spacing:-.025em}.panel-note{color:var(--muted);font-size:13px}.gate{padding:16px 17px;border:1px solid #e4ebf4;border-radius:13px;background:linear-gradient(135deg,#fbfdff,#f7faff)}.gate+.gate{margin-top:10px}.gate-head{display:flex;align-items:center;gap:10px}.gate-name{flex:1;color:#263c5e;font-size:14px;font-weight:750}.gate-value{font-size:16px;font-weight:800}.gate-value.pass{color:var(--green)}.gate-value.warn{color:var(--amber)}.gate-value.fail{color:var(--red)}.meter{height:7px;margin:11px 0 8px;background:#e7edf5;border-radius:99px;overflow:hidden}.meter i{display:block;height:100%;border-radius:inherit;background:var(--blue)}.meter i.pass{background:var(--green)}.meter i.warn{background:#e9a040}.meter i.fail{background:var(--red)}.gate-foot{display:flex;justify-content:space-between;color:var(--muted);font-size:12px}.badge{display:inline-flex;align-items:center;padding:3px 9px;border-radius:999px;font-size:11px;font-weight:800}.badge.pass{color:var(--green);background:var(--green-soft)}.badge.warn{color:var(--amber);background:var(--amber-soft)}.badge.fail{color:var(--red);background:var(--red-soft)}.cause{padding:16px 0;border-bottom:1px solid var(--line)}.cause:first-of-type{padding-top:0}.cause:last-child{padding-bottom:0;border-bottom:0}.cause-head{display:flex;align-items:center;gap:10px}.cause-label{padding:4px 9px;border-radius:7px;background:var(--red-soft);color:var(--red);font-size:12px;font-weight:800}.cause-label.env{background:var(--amber-soft);color:var(--amber)}.cause-label.none{background:#eef2f7;color:#65738b}.cause-title{flex:1;color:#2a3c5a;font-size:15px;font-weight:700}.cause-count{font-size:18px;font-weight:800}.cause-bar{height:7px;margin-top:12px;background:#edf1f6;border-radius:99px;overflow:hidden}.cause-bar i{display:block;height:100%;width:76%;background:var(--red);border-radius:inherit}.cause-bar i.env{width:48%;background:#e9a040}.cause-bar i.none{width:24%;background:#8491a6}.full{margin-top:20px}.defects{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.defect{border:1px solid #f0c7d0;border-radius:13px;background:#fffafb;overflow:hidden}.defect summary{display:flex;align-items:center;gap:10px;padding:15px;cursor:pointer;list-style:none}.defect summary::-webkit-details-marker{display:none}.bug-id{color:#c43b58;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace}.bug-title{flex:1;color:var(--navy);font-size:14px;font-weight:750}.bug-body{padding:0 15px 15px;border-top:1px solid #f3dce1}.bug-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin-top:14px}.bug-field{padding:9px 10px;border-radius:8px;background:#fff}.bug-field label{display:block;color:var(--muted);font-size:11px}.bug-field span{display:block;margin-top:2px;color:var(--ink);font-size:12px;font-weight:650}.suggestions{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.suggestion{padding:17px;border:1px solid #e3eafa;border-radius:13px;background:linear-gradient(135deg,#fbfcff,#f4f7ff)}.priority{display:inline-grid;place-items:center;width:28px;height:24px;border-radius:7px;background:var(--blue-soft);color:var(--blue);font-size:11px;font-weight:850}.suggestion strong{display:block;margin-top:11px;color:var(--navy);font-size:14px}.suggestion span{display:block;margin-top:5px;color:var(--muted);font-size:12px}.details{margin-top:20px}.case{border-top:1px solid var(--line)}details summary{cursor:pointer;list-style:none}details summary::-webkit-details-marker{display:none}.case-summary{display:flex;align-items:center;gap:11px;padding:16px 2px}.case-id{padding:4px 9px;border-radius:6px;background:#edf2f8;color:var(--navy);font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace}.case-title{flex:1;color:#263c5e;font-weight:750}.case-meta{color:var(--muted);font-size:13px}.case-body{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;padding:0 0 18px}.case-box{padding:13px;border:1px solid var(--line);border-radius:10px;background:#fbfcfe}.case-box h3{margin:0 0 5px;color:var(--muted);font-size:12px}.case-box pre{margin:0;white-space:pre-wrap;color:#344054;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}.evidence{margin-top:10px;padding-top:9px;border-top:1px solid #e5ebf3}.evidence summary{color:var(--blue);font-size:12px;font-weight:750}.evidence-body{margin-top:7px;color:var(--muted);font-size:12px}.evidence-body code{color:var(--navy);font:11px ui-monospace,SFMono-Regular,Menlo,monospace}.footnote{margin:18px 0 0;color:var(--muted);font-size:12px}@media(max-width:900px){main{width:min(100% - 32px,1180px)}.stats{grid-template-columns:repeat(2,1fr)}.layout{grid-template-columns:1fr}.suggestions{grid-template-columns:1fr}}@media(max-width:560px){main{width:calc(100% - 20px);padding-top:25px}.topbar{display:block}.decision{margin-top:18px}.stats,.defects,.bug-fields,.case-body{grid-template-columns:1fr}.stat{min-height:126px;padding:19px}.stat-value{margin-top:18px;font-size:34px}.panel-pad{padding:19px}.panel-heading{align-items:flex-start;flex-direction:column}.case-meta{display:none}}
|
|
11
|
+
.topbar{position:relative;overflow:hidden;padding:34px 38px;border:1px solid #294b78;border-radius:26px;background:linear-gradient(125deg,#102849 0%,#173d6d 57%,#245b91 100%);box-shadow:0 18px 40px rgba(16,40,73,.18)}
|
|
12
|
+
.topbar:before,.topbar:after{content:"";position:absolute;border:1px solid rgba(165,211,255,.18);border-radius:50%;pointer-events:none}.topbar:before{width:370px;height:370px;right:-155px;top:-225px}.topbar:after{width:240px;height:240px;right:55px;bottom:-190px}
|
|
13
|
+
.topbar>div:first-child{position:relative;z-index:1}.eyebrow{color:#9fc6ee;font-size:11px;letter-spacing:.2em}.title{margin:12px 0 10px;color:#f7fbff;font-size:clamp(33px,4vw,48px);letter-spacing:-.06em}.meta{color:#bdd2e9;font-size:13px}
|
|
14
|
+
.decision{position:relative;z-index:1;min-width:220px;padding:16px 18px;border:1px solid rgba(255,177,192,.35);background:rgba(255,245,247,.1);box-shadow:0 8px 24px rgba(0,0,0,.08)}.decision-top{color:#fff1f4}.decision small{color:#f3bec8}
|
|
15
|
+
.stats{gap:12px;margin-top:18px}.stat{min-height:132px;padding:21px 22px;border:1px solid #e1e8f2;border-radius:15px;box-shadow:0 6px 18px rgba(25,53,92,.045)}.stat:before{height:3px}.stat-label{font-size:13px}.stat-value{margin-top:19px;font-size:35px}.stat-foot{font-size:12px}
|
|
16
|
+
.panel{border-color:#e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04)}.panel-pad{padding:22px}.panel-heading{margin-bottom:18px}.panel-heading h2{font-size:19px}.panel-heading h2:before{content:"";display:inline-block;width:4px;height:17px;margin:0 9px 0 0;vertical-align:-2px;border-radius:5px;background:var(--blue)}
|
|
17
|
+
.gate{padding:14px 15px;border-color:#e5ebf3;border-radius:11px;background:#fcfdff}.gate+.gate{margin-top:8px}.gate-name{font-size:13px}.gate-value{font-size:15px}.meter{height:6px;margin:10px 0 7px}.evidence summary{display:flex;align-items:center;gap:6px}.evidence summary:before{content:"+";display:inline-grid;place-items:center;width:15px;height:15px;border:1px solid currentColor;border-radius:50%;font-size:13px;line-height:1}.evidence[open] summary:before{content:"−"}
|
|
18
|
+
.cause{padding:14px 0}.cause-title{font-size:14px}.cause-count{font-size:16px}.cause-bar{height:5px;margin-top:10px}
|
|
19
|
+
.full{margin-top:16px}.defects{gap:10px}.defect{border-color:#ecd4d9;border-radius:11px;background:linear-gradient(135deg,#fffdfd,#fff8f9)}.defect summary{padding:14px}.bug-title{font-size:13px}.bug-body{padding:0 14px 14px}.bug-fields{gap:7px;margin-top:12px}.bug-field{padding:8px 9px}.bug-field span{font-size:11px}
|
|
20
|
+
.suggestions{gap:10px}.suggestion{padding:15px;border-color:#e0e8f5;border-radius:11px}.suggestion strong{margin-top:9px;font-size:13px}.suggestion span{font-size:11px}.priority{width:27px;height:23px}
|
|
21
|
+
.details{margin-top:16px}.case-summary{padding:14px 2px}.case-title{font-size:13px}.case-box{padding:12px;border-radius:9px}.case-box h3{font-size:11px}.case-box pre{font-size:11px}
|
|
22
|
+
details:not(.evidence) summary:after{content:"⌄";margin-left:auto;color:#91a0b5;font-size:16px;transition:transform .2s ease}details[open]:not(.evidence) summary:after{transform:rotate(180deg)}
|
|
23
|
+
.overview{display:grid;grid-template-columns:1fr 1.35fr;gap:12px;margin-top:18px}.overview-chart,.signal-chart{padding:20px 22px;border:1px solid #dfe7f1;border-radius:15px;background:rgba(255,255,255,.82);box-shadow:0 7px 20px rgba(25,53,92,.04)}.chart-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.chart-heading h2{margin:3px 0 0;color:var(--navy);font-size:17px;letter-spacing:-.02em}.chart-kicker{color:#8796aa;font-size:10px;font-weight:800;letter-spacing:.16em}.chart-caption{color:var(--muted);font-size:11px;white-space:nowrap}.donut-wrap{display:flex;align-items:center;gap:20px;margin-top:15px}.donut{width:116px;height:116px;flex:0 0 116px;transform:rotate(-90deg)}.donut circle{fill:none;stroke-width:12}.donut-track{stroke:#edf1f6}.donut-pass{stroke:var(--green);stroke-dasharray:267.7 289;stroke-linecap:round}.donut-fail{stroke:var(--red);stroke-dasharray:16 289;stroke-dashoffset:-267.7}.donut-center{position:absolute;display:flex;width:116px;height:116px;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}.donut-center strong{color:var(--navy);font-size:20px;letter-spacing:-.05em}.donut-center span{color:var(--muted);font-size:10px}.donut-wrap{position:relative}.outcome-list{display:grid;gap:6px;flex:1}.outcome-list div{display:grid;grid-template-columns:10px 1fr auto;align-items:center;gap:7px;color:var(--muted);font-size:12px}.outcome-list strong{color:var(--ink);font-size:14px}.legend-dot{width:8px;height:8px;border-radius:50%;background:var(--green)}.legend-dot.fail{background:var(--red)}.legend-dot.blocked{background:#aab5c4}.signal-chart{display:flex;flex-direction:column}.signal-row{margin-top:14px}.signal-row>div:first-child{display:flex;justify-content:space-between;align-items:baseline;color:#2b405f;font-size:12px}.signal-row strong{color:var(--navy);font-size:14px}.signal-track{position:relative;height:8px;margin:7px 0 4px;background:#edf1f6;border-radius:99px}.signal-track i{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#7d9bf5,var(--blue))}.signal-track i.good{background:linear-gradient(90deg,#52b88b,var(--green))}.signal-track b{position:absolute;top:-3px;width:2px;height:14px;background:#273b59;border-radius:2px}.signal-row small{color:var(--muted);font-size:10px}
|
|
24
|
+
@media(max-width:900px){.overview{grid-template-columns:1fr}.donut-wrap{max-width:310px}}
|
|
25
|
+
@media(max-width:560px){.topbar{padding:28px 23px;border-radius:21px}.stats{gap:9px}.stat{min-height:118px;padding:17px}.stat-value{font-size:31px}.panel-pad{padding:18px}.case-summary{gap:7px}.case-title{min-width:0}.case-summary .case-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<main>
|
|
30
|
+
<header class="topbar"><div><div class="eyebrow">BACKEND TEST · L-5 QUALITY VIEW</div><h1 class="title">测试成熟度报告</h1><p class="meta">my-webapp / Campaign R04 · commit 7d9a2f1 · 2026-07-24 10:42</p></div><div class="decision"><div class="decision-top"><i class="decision-dot"></i>L-5 not ready</div><small>3 个阻断项需要处理</small></div></header>
|
|
31
|
+
<section class="overview" aria-label="执行概览">
|
|
32
|
+
<div class="overview-chart">
|
|
33
|
+
<div class="chart-heading"><div><span class="chart-kicker">RUN OUTCOME</span><h2>本轮执行结果</h2></div><span class="chart-caption">18 个用例</span></div>
|
|
34
|
+
<div class="donut-wrap">
|
|
35
|
+
<svg class="donut" viewBox="0 0 120 120" role="img" aria-label="17 个通过,1 个失败,0 个阻塞">
|
|
36
|
+
<circle class="donut-track" cx="60" cy="60" r="46"></circle>
|
|
37
|
+
<circle class="donut-pass" cx="60" cy="60" r="46"></circle>
|
|
38
|
+
<circle class="donut-fail" cx="60" cy="60" r="46"></circle>
|
|
39
|
+
</svg>
|
|
40
|
+
<div class="donut-center"><strong>94.4%</strong><span>通过率</span></div>
|
|
41
|
+
<div class="outcome-list"><div><i class="legend-dot pass"></i><span>通过</span><strong>17</strong></div><div><i class="legend-dot fail"></i><span>失败</span><strong>1</strong></div><div><i class="legend-dot blocked"></i><span>阻塞</span><strong>0</strong></div></div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="signal-chart">
|
|
45
|
+
<div class="chart-heading"><div><span class="chart-kicker">QUALITY SIGNALS</span><h2>质量信号</h2></div><span class="chart-caption">按 L-5 门槛判定</span></div>
|
|
46
|
+
<div class="signal-row"><div><span>需求 / AC 覆盖</span><strong>85.7%</strong></div><div class="signal-track"><i style="width:85.7%"></i><b style="left:100%"></b></div><small>12 / 14 · 目标 100%</small></div>
|
|
47
|
+
<div class="signal-row"><div><span>自动化覆盖率</span><strong>88.9%</strong></div><div class="signal-track"><i style="width:88.9%"></i><b style="left:90%"></b></div><small>16 / 18 · 目标 ≥90%</small></div>
|
|
48
|
+
<div class="signal-row"><div><span>Line 代码覆盖</span><strong>82.4%</strong></div><div class="signal-track"><i class="good" style="width:82.4%"></i><b style="left:80%"></b></div><small>103 / 125 · 目标 ≥80%</small></div>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
<section class="layout">
|
|
52
|
+
<article class="panel panel-pad"><div class="panel-heading"><h2>L-5 门禁摘要</h2><span class="panel-note">阻断项与执行门槛</span></div>
|
|
53
|
+
<div class="gate"><div class="gate-head"><span class="gate-name">测试通过率</span><strong class="gate-value warn">94.4%</strong><span class="badge warn">未达标</span></div><div class="meter"><i class="warn" style="width:94.4%"></i></div><div class="gate-foot"><span>17 / 18 个用例</span><span>目标 100%</span></div><details class="evidence"><summary>查看计算依据</summary><div class="evidence-body">失败 1 条,错误 0 条。证据:<code>reports/backend-test-facts.md · pytest-exit=1</code></div></details></div>
|
|
54
|
+
<div class="gate"><div class="gate-head"><span class="gate-name">Critical 风险</span><strong class="gate-value fail">1</strong><span class="badge fail">阻断</span></div><div class="meter"><i class="fail" style="width:35%"></i></div><div class="gate-foot"><span>1 个阻断项</span><span>目标 0</span></div><details class="evidence"><summary>查看阻断原因</summary><div class="evidence-body">R-01:手机号注册校验返回错误业务结果。关联:<code>AC-001 · TC-017 · ProductBug</code></div></details></div>
|
|
55
|
+
</article>
|
|
56
|
+
<article class="panel panel-pad"><div class="panel-heading"><h2>失败诊断</h2><span class="panel-note">1 个失败 · 1 个错误</span></div>
|
|
57
|
+
<div class="cause"><div class="cause-head"><span class="cause-label">ProductBug</span><span class="cause-title">手机号注册校验</span><strong class="cause-count">1</strong></div><div class="cause-bar"><i></i></div><details class="evidence"><summary>查看诊断详情</summary><div class="evidence-body">期望 HTTP 400,实际 HTTP 200。关联:TC-017 / AC-001。</div></details></div>
|
|
58
|
+
<div class="cause"><div class="cause-head"><span class="cause-label env">EnvFailure</span><span class="cause-title">依赖服务连接</span><strong class="cause-count">1</strong></div><div class="cause-bar"><i class="env"></i></div><details class="evidence"><summary>查看环境详情</summary><div class="evidence-body">GET /health/dependency 超时 5000ms,未形成有效业务断言。</div></details></div>
|
|
59
|
+
<div class="cause"><div class="cause-head"><span class="cause-label none">Unknown</span><span class="cause-title">缺少有效证据</span><strong class="cause-count">0</strong></div><div class="cause-bar"><i class="none"></i></div><details class="evidence"><summary>查看判定规则</summary><div class="evidence-body">当前没有无法分类的失败记录。</div></details></div>
|
|
60
|
+
</article>
|
|
61
|
+
</section>
|
|
62
|
+
<section class="panel panel-pad full"><div class="panel-heading"><h2>缺陷记录</h2><span class="panel-note">2 条 · 已关联用例与证据</span></div><div class="defects">
|
|
63
|
+
<details class="defect"><summary><span class="bug-id">BUG-20260724-017</span><span class="bug-title">手机号注册校验未拦截非法格式</span><span class="badge fail">未修复</span></summary><div class="bug-body"><div class="bug-fields"><div class="bug-field"><label>级别</label><span>High · ProductBug</span></div><div class="bug-field"><label>模块</label><span>用户注册 / 手机号校验</span></div><div class="bug-field"><label>关联 AC / Case</label><span>AC-001 · TC-017</span></div><div class="bug-field"><label>发现版本</label><span>Campaign R04 · 7d9a2f1</span></div></div><details class="evidence"><summary>查看缺陷证据</summary><div class="evidence-body">期望 HTTP 400,实际 HTTP 200。<br>本地记录:<code>defects/BUG-20260724-017.md</code><br>关联证据:<code>reports/evidence/TC-017-response.json</code></div></details></div></details>
|
|
64
|
+
<details class="defect"><summary><span class="bug-id">BUG-20260724-018</span><span class="bug-title">依赖服务健康检查超时</span><span class="badge warn">待确认</span></summary><div class="bug-body"><div class="bug-fields"><div class="bug-field"><label>级别</label><span>Medium · EnvFailure</span></div><div class="bug-field"><label>模块</label><span>审批服务依赖检查</span></div><div class="bug-field"><label>关联 AC / Case</label><span>AC-004 · TC-018</span></div><div class="bug-field"><label>发现版本</label><span>Campaign R04 · 7d9a2f1</span></div></div><details class="evidence"><summary>查看环境证据</summary><div class="evidence-body">GET /health/dependency 超时 5000ms。<br>本地记录:<code>defects/BUG-20260724-018.md</code><br>建议:补充服务可用性和网络诊断信息。</div></details></div></details>
|
|
65
|
+
</div></section>
|
|
66
|
+
<section class="panel panel-pad full"><div class="panel-heading"><h2>下一轮回归建议</h2><span class="panel-note">3 条行动项</span></div><div class="suggestions">
|
|
67
|
+
<div class="suggestion"><span class="priority">P0</span><strong>修复注册校验后重跑 TC-017</strong><span>核对 AC-001、HTTP 400 与错误码。</span></div>
|
|
68
|
+
<div class="suggestion"><span class="priority">P1</span><strong>补充依赖服务健康检查</strong><span>记录 URL、超时配置、错误类型和时间戳。</span></div>
|
|
69
|
+
<div class="suggestion"><span class="priority">P2</span><strong>补齐缺失 AC 自动化映射</strong><span>覆盖 AC-013、AC-014,提升追踪完整度。</span></div>
|
|
70
|
+
</div></section>
|
|
71
|
+
<section class="panel panel-pad full details"><div class="panel-heading"><h2>用例执行明细</h2><span class="panel-note">共 3 条 · 点击展开</span></div>
|
|
72
|
+
<details class="case"><summary class="case-summary"><span class="case-id">TC-017</span><span class="case-title">手机号注册校验</span><span class="case-meta">失败 · 1.24s</span><span class="badge fail">FAIL</span></summary><div class="case-body"><div class="case-box"><h3>失败原因</h3><div>ProductBug · 返回 200,未展示预期格式错误提示。</div></div><div class="case-box"><h3>自动化脚本</h3><pre>testcase/test_register.py::test_TC_017</pre></div><div class="case-box"><h3>请求参数</h3><pre>POST /api/register · phone=138****8000</pre></div><div class="case-box"><h3>响应结果</h3><pre>200 OK · {"success":true,"message":"注册成功"}</pre></div></div></details>
|
|
73
|
+
<details class="case"><summary class="case-summary"><span class="case-id">TC-018</span><span class="case-title">依赖服务连接检查</span><span class="case-meta">错误 · 0.82s</span><span class="badge warn">ERROR</span></summary><div class="case-body"><div class="case-box"><h3>失败原因</h3><div>EnvFailure · 依赖服务连接超时,未形成有效业务断言。</div></div><div class="case-box"><h3>自动化脚本</h3><pre>testcase/test_dependency.py::test_TC_018</pre></div><div class="case-box"><h3>请求参数</h3><pre>GET /health/dependency · timeout=5000ms</pre></div><div class="case-box"><h3>响应结果</h3><pre>Unavailable · connection timed out</pre></div></div></details>
|
|
74
|
+
<details class="case"><summary class="case-summary"><span class="case-id">TC-019</span><span class="case-title">正常流程提交</span><span class="case-meta">通过 · 0.31s</span><span class="badge pass">PASS</span></summary><div class="case-body"><div class="case-box"><h3>验收场景</h3><div>提交合法业务数据后返回成功状态,并生成业务编号。</div></div><div class="case-box"><h3>自动化脚本</h3><pre>testcase/test_flow.py::test_TC_019</pre></div><div class="case-box"><h3>请求参数</h3><pre>POST /api/flows · {"name":"approval-flow"}</pre></div><div class="case-box"><h3>响应结果</h3><pre>201 Created · {"flowId":"flow-001"}</pre></div></div></details>
|
|
75
|
+
</section>
|
|
76
|
+
<p class="footnote">Demo 数据仅用于展示页面结构;实际报告以同一次测试执行产生的 HTML、Markdown 和 facts 为准。</p>
|
|
77
|
+
</main>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|