@tea-agent/loop-agent 0.24.8 → 0.24.10

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,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.24.10] - 2026-07-29
6
+
7
+ ### 修复
8
+
9
+ - Closeout 重验不再要求 `should`/partial AC 绑定完整 run evidence(仅 `covered` must AC 强制 hash 绑定)
10
+
11
+ ## [0.24.9] - 2026-07-29
12
+
13
+ ### 重点更新
14
+
15
+ - 后端测试执行失败时的诊断信息更加完善,避免出现无输出导致无法定位的问题
16
+ - 修复 Windows 环境下前端证据校验因长命令终端转义导致的崩溃
17
+ - 节点检查器新增「节点输入」只读页签,支持安全审查冻结的节点定义
18
+
5
19
  ### 新增
6
20
 
7
- - Observe / Inspect 节点检查器在「节点输出」左侧新增「节点输入」页签:只读投影冻结 `run.json` 顶层节点定义(任务正文、依赖、执行器摘要、边界)与可选 assembled prompt 指纹;完整 assembled prompt 与动态展开子节点仍不在本轮范围
21
+ - Observe / Inspect 节点检查器在「节点输出」左侧新增「节点输入」页签:只读投影冻结 run.json 顶层节点定义(任务正文、依赖、执行器摘要、边界)与可选 assembled prompt 指纹
22
+
23
+ ### 改进
24
+
25
+ - 后端测试节点执行失败时可观测性增强:写入包含映射脚本、PYTHON_BIN、pytest 版本、exit、html 是否存在与 STEP 等信息的诊断文件,并在节点 stderr 汇总失败摘要
26
+
27
+ ### 修复
28
+
29
+ - 修复前端证据校验在 Windows 下通过长 node -e 命令执行时的终端转义崩溃:改为由 runtime 内部直接校验,兼容 CMD、Git Bash、PowerShell 与 Unix shell
30
+
31
+ ## [0.24.9] - 2026-07-29
8
32
 
9
33
  ### 修复
10
34
 
11
- - 修复前端证据校验在 Windows 下通过长 `node -e` 命令执行时的终端转义崩溃:改为由 runtime 内部直接校验,兼容 CMD、Git Bash、PowerShell Unix shell
35
+ - Closeout 重验不再要求 `should`/partial AC 绑定完整 run evidence(仅 `covered` must AC 强制 hash 绑定)
12
36
 
13
37
  ## [0.24.8] - 2026-07-29
14
38
 
@@ -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
- 'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" >&2; exit 127; fi',
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
- return { ok: false, stdout: results.map((result) => result.stdout).join("\n"), stderr: failure.stderr, failureCategory: failure.failureCategory, durationMs: Date.now() - started };
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");
@@ -284,11 +284,15 @@ export async function validateDeliveryPackage(input) {
284
284
  blockers.push(`Acceptance coverage status is not derivable: ${ac.id}`);
285
285
  if (JSON.stringify([...item.blockedBy].sort()) !== JSON.stringify([...blockedBy].sort()))
286
286
  blockers.push(`Acceptance blockers changed: ${ac.id}`);
287
- for (const ref of expectedRefs)
288
- if (!item.evidence.some((candidate) => candidate.path === ref.path && candidate.sha256 === ref.sha256))
289
- blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
290
- if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
291
- blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
287
+ // partial / missing / blocked should-ACs intentionally omit full evidence lists
288
+ // (only must ACs covered by QA require run+QA hash binding).
289
+ if (expectedStatus === "covered") {
290
+ for (const ref of expectedRefs)
291
+ if (!item.evidence.some((candidate) => candidate.path === ref.path && candidate.sha256 === ref.sha256))
292
+ blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
293
+ if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
294
+ blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
295
+ }
292
296
  for (const ref of item.evidence)
293
297
  if (!await hashedRefValid(repoRoot, ref))
294
298
  blockers.push(`Acceptance evidence changed: ${item.acId}`);
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.24.8",
3
+ "version": "0.24.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",