@tea-agent/loop-agent 0.25.4 → 0.25.5
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/AGENTS.md +6 -0
- package/CHANGELOG.md +40 -0
- package/dist/commands/client-recovery.js +209 -62
- package/dist/executors/dag-pi-executor.js +80 -15
- package/dist/executors/model-routing.js +1 -1
- package/dist/executors/shell-executor.js +127 -0
- package/dist/executors/shell-write-guard.js +21 -7
- package/dist/worker/console/repo-fingerprint.js +7 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +964 -0
- package/dist/workflows/dag/backend-test-case-manifest.js +39 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +219 -16
- package/dist/workflows/dag/convergence/controller.js +134 -9
- package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
- package/dist/workflows/dag/init-hybrid.js +262 -75
- package/dist/workflows/dag/node-execution.js +64 -11
- package/dist/workflows/dag/prompt.js +118 -4
- package/dist/workflows/dag/retry-policy.js +5 -4
- package/dist/workflows/dag/scheduler.js +32 -5
- package/dist/workflows/dag/types.js +7 -4
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/architecture/dag-execution.md +7 -4
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/templates/agent-dag.base.json +1 -1
- package/docs/templates/agent-dag.final-verification.json +1 -1
- package/docs/templates/agent-dag.supervised-implementation.json +1 -1
- package/docs/templates/backend-test-dag.json +40 -13
- package/docs/templates/frontend-test-dag.json +32 -2
- package/docs/templates/hybrid-dag.json +1 -1
- package/examples/decision-gate-agent-dag.json +1 -1
- package/examples/example-dag.json +1 -1
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/model-routing.md +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { frontendTestResultContractSchema, } from "./frontend-test-result-contract.js";
|
|
4
|
+
function ratioMetric(numerator, denominator, threshold, label) {
|
|
5
|
+
if (denominator === 0) {
|
|
6
|
+
return { numerator, denominator, ratio: null, threshold, status: "unavailable", reason: `${label}-denominator-is-zero` };
|
|
7
|
+
}
|
|
8
|
+
const ratio = numerator / denominator;
|
|
9
|
+
return {
|
|
10
|
+
numerator,
|
|
11
|
+
denominator,
|
|
12
|
+
ratio,
|
|
13
|
+
threshold,
|
|
14
|
+
status: ratio >= threshold ? "pass" : "fail",
|
|
15
|
+
reason: ratio >= threshold ? null : `${label}-below-threshold`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function unavailable(reason, threshold) {
|
|
19
|
+
return { numerator: null, denominator: null, ratio: null, threshold, status: "unavailable", reason };
|
|
20
|
+
}
|
|
21
|
+
/** Compute the shared L-5 gates from the authoritative frontend result contract. */
|
|
22
|
+
export function computeFrontendL5ReportMetrics(result) {
|
|
23
|
+
const executed = result.totals.passed + result.totals.failed;
|
|
24
|
+
const acTotal = result.acceptanceCoverage.covered.length + result.acceptanceCoverage.missing.length;
|
|
25
|
+
const automationExecuted = executed;
|
|
26
|
+
const criticalRiskCount = result.acceptanceCoverage.missing.length + result.advisoryFindings.filter((finding) => /critical|blocking|unsafe|missing-ac/i.test(finding.ruleId)).length;
|
|
27
|
+
const metrics = {
|
|
28
|
+
passRate: ratioMetric(result.totals.passed, executed, 1, "pass-rate"),
|
|
29
|
+
acCoverage: ratioMetric(result.acceptanceCoverage.covered.length, acTotal, 1, "ac-coverage"),
|
|
30
|
+
automationCoverage: ratioMetric(automationExecuted, result.totals.cases, 0.9, "automation-coverage"),
|
|
31
|
+
lineCoverage: unavailable("frontend-code-coverage-missing", 0.8),
|
|
32
|
+
branchCoverage: unavailable("frontend-code-coverage-missing", 0.7),
|
|
33
|
+
skipped: {
|
|
34
|
+
numerator: result.totals.blocked,
|
|
35
|
+
denominator: result.totals.blocked,
|
|
36
|
+
ratio: result.totals.blocked === 0 ? 1 : 0,
|
|
37
|
+
threshold: 1,
|
|
38
|
+
status: result.totals.blocked === 0 ? "pass" : "fail",
|
|
39
|
+
reason: result.totals.blocked === 0 ? null : "skipped-tests-present",
|
|
40
|
+
},
|
|
41
|
+
criticalRisks: {
|
|
42
|
+
numerator: criticalRiskCount,
|
|
43
|
+
denominator: criticalRiskCount,
|
|
44
|
+
ratio: criticalRiskCount === 0 ? 1 : 0,
|
|
45
|
+
threshold: 1,
|
|
46
|
+
status: criticalRiskCount === 0 ? "pass" : "fail",
|
|
47
|
+
reason: criticalRiskCount === 0 ? null : "critical-risk-present",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const blockingItems = Object.entries(metrics)
|
|
51
|
+
.filter(([, metric]) => metric.status !== "pass")
|
|
52
|
+
.map(([name, metric]) => `${name}:${metric.reason ?? metric.status}`);
|
|
53
|
+
return {
|
|
54
|
+
status: blockingItems.length === 0 ? "ready" : "not-ready",
|
|
55
|
+
metrics,
|
|
56
|
+
blockingItems,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function escapeHtml(value) {
|
|
60
|
+
return String(value ?? "")
|
|
61
|
+
.replaceAll("&", "&")
|
|
62
|
+
.replaceAll("<", "<")
|
|
63
|
+
.replaceAll(">", ">")
|
|
64
|
+
.replaceAll('"', """)
|
|
65
|
+
.replaceAll("'", "'");
|
|
66
|
+
}
|
|
67
|
+
function metricValue(metric) {
|
|
68
|
+
if (metric.ratio === null)
|
|
69
|
+
return "unavailable";
|
|
70
|
+
return `${(metric.ratio * 100).toFixed(1)}%`;
|
|
71
|
+
}
|
|
72
|
+
function metricRow(label, metric) {
|
|
73
|
+
return `| ${label} | ${metric.numerator ?? "-"} / ${metric.denominator ?? "-"} | ${metricValue(metric)} | ${metric.status.toUpperCase()} | ${metric.reason ?? "-"} |`;
|
|
74
|
+
}
|
|
75
|
+
function renderMarkdown(result, metrics) {
|
|
76
|
+
const m = metrics.metrics;
|
|
77
|
+
return [
|
|
78
|
+
"# 前端测试 L-5 报告",
|
|
79
|
+
"",
|
|
80
|
+
`- L-5 结论:**${metrics.status === "ready" ? "READY" : "NOT READY"}**`,
|
|
81
|
+
`- Result v1 outcome:${result.outcome}`,
|
|
82
|
+
"",
|
|
83
|
+
"| 指标 | 分子 / 分母 | 比例 | 状态 | 原因 |",
|
|
84
|
+
"| --- | ---: | ---: | --- | --- |",
|
|
85
|
+
metricRow("测试通过率", m.passRate),
|
|
86
|
+
metricRow("AC 验收覆盖", m.acCoverage),
|
|
87
|
+
metricRow("自动化覆盖率", m.automationCoverage),
|
|
88
|
+
metricRow("Line 代码覆盖", m.lineCoverage),
|
|
89
|
+
metricRow("Branch 代码覆盖", m.branchCoverage),
|
|
90
|
+
metricRow("阻塞/跳过用例", m.skipped),
|
|
91
|
+
metricRow("Critical 风险", m.criticalRisks),
|
|
92
|
+
"",
|
|
93
|
+
"## 判定规则",
|
|
94
|
+
"",
|
|
95
|
+
"测试通过率=100%、AC 覆盖=100%、自动化覆盖率≥90%、Line≥80%、Branch≥70%、阻塞/跳过=0 且无 Critical 风险时才为 READY。",
|
|
96
|
+
"前端 DAG 当前没有可信的 line/branch 代码覆盖产物,因此这两项保持 unavailable,不得推断。",
|
|
97
|
+
"",
|
|
98
|
+
"## 阻塞项",
|
|
99
|
+
"",
|
|
100
|
+
...(metrics.blockingItems.length ? metrics.blockingItems.map((item) => `- ${item}`) : ["- 无"]),
|
|
101
|
+
"",
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
function renderHtml(result, metrics) {
|
|
105
|
+
const rows = Object.entries(metrics.metrics)
|
|
106
|
+
.map(([name, metric]) => `<tr><td>${escapeHtml(name)}</td><td>${metric.numerator ?? "-"} / ${metric.denominator ?? "-"}</td><td>${escapeHtml(metricValue(metric))}</td><td class="${metric.status}">${escapeHtml(metric.status.toUpperCase())}</td><td>${escapeHtml(metric.reason ?? "-")}</td></tr>`)
|
|
107
|
+
.join("");
|
|
108
|
+
const blocking = metrics.blockingItems.length
|
|
109
|
+
? metrics.blockingItems.map((item) => `<li>${escapeHtml(item)}</li>`).join("")
|
|
110
|
+
: "<li>无</li>";
|
|
111
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none';style-src 'unsafe-inline'"><title>前端测试 L-5 报告</title><style>body{font:15px system-ui,"Microsoft YaHei",sans-serif;background:#f5f7fb;color:#172033;margin:0}main{max-width:1100px;margin:auto;padding:32px}.card{background:#fff;border-radius:14px;padding:24px;margin:16px 0;box-shadow:0 6px 24px #10182812}.decision{font-size:28px;font-weight:800;color:${metrics.status === "ready" ? "#067647" : "#b42318"}}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:12px;border-bottom:1px solid #e4e7ec}.pass{color:#067647}.fail{color:#b42318}.unavailable{color:#946200}code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}</style></head><body><main><section class="card"><h1>前端测试 L-5 报告</h1><div class="decision">${metrics.status === "ready" ? "READY" : "NOT READY"}</div><p>Result v1 outcome:<code>${escapeHtml(result.outcome)}</code></p></section><section class="card"><h2>指标</h2><table><thead><tr><th>指标</th><th>分子 / 分母</th><th>比例</th><th>状态</th><th>原因</th></tr></thead><tbody>${rows}</tbody></table></section><section class="card"><h2>阻塞项</h2><ul>${blocking}</ul><p>前端 DAG 当前没有可信的 line/branch 代码覆盖产物,因此这两项为 unavailable,不得推断。</p></section></main></body></html>`;
|
|
112
|
+
}
|
|
113
|
+
async function writePairAtomic(markdownPath, markdown, htmlPath, html) {
|
|
114
|
+
await mkdir(path.dirname(markdownPath), { recursive: true });
|
|
115
|
+
const nonce = `${process.pid}-${Date.now()}`;
|
|
116
|
+
const markdownTemp = `${markdownPath}.${nonce}.tmp`;
|
|
117
|
+
const htmlTemp = `${htmlPath}.${nonce}.tmp`;
|
|
118
|
+
try {
|
|
119
|
+
await writeFile(markdownTemp, markdown, "utf8");
|
|
120
|
+
await writeFile(htmlTemp, html, "utf8");
|
|
121
|
+
await rename(markdownTemp, markdownPath);
|
|
122
|
+
await rename(htmlTemp, htmlPath);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await rm(markdownTemp, { force: true });
|
|
126
|
+
await rm(htmlTemp, { force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function renderFrontendTestL5Report(input) {
|
|
130
|
+
const resultPath = path.join(input.runDir, "contracts", "frontend-test-result.json");
|
|
131
|
+
const result = frontendTestResultContractSchema.parse(JSON.parse(await readFile(resultPath, "utf8")));
|
|
132
|
+
const metrics = computeFrontendL5ReportMetrics(result);
|
|
133
|
+
const outputDir = path.join(input.workspaceRoot, "testcase", "frontend", "reports");
|
|
134
|
+
const markdownPath = path.join(outputDir, "frontend-test-l5-dashboard.md");
|
|
135
|
+
const htmlPath = path.join(outputDir, "frontend-test-l5-dashboard.html");
|
|
136
|
+
await writePairAtomic(markdownPath, renderMarkdown(result, metrics), htmlPath, renderHtml(result, metrics));
|
|
137
|
+
return { metrics, markdownPath, htmlPath };
|
|
138
|
+
}
|