@tea-agent/loop-agent 0.33.1 → 0.33.3
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 +26 -0
- package/dist/workflows/dag/frontend-test-html-report.js +38 -23
- package/dist/workflows/dag/frontend-test-result-contract.js +77 -6
- package/dist/workflows/dag/init-hybrid.js +150 -50
- package/dist/workflows/dag/retry-policy.js +5 -0
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.json +11 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/package.json +1 -1
- package/skills/analyze-product-dependencies/SKILL.md +74 -33
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +16 -11
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +20 -10
- package/skills/analyze-product-dependencies/references/example.md +9 -9
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +93 -18
- package/skills/analyze-product-dependencies/references/input-contract.md +27 -4
- package/skills/analyze-product-dependencies/references/kb-integration.md +64 -0
- package/skills/analyze-product-dependencies/references/scouting-rules.md +25 -10
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +247 -54
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +122 -29
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +94 -36
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +37 -23
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +35 -43
- package/skills/analyze-product-requirements/SKILL.md +98 -43
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +8 -12
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +28 -14
- package/skills/analyze-product-requirements/references/example.md +24 -6
- package/skills/analyze-product-requirements/references/forward-test-cases.md +87 -9
- package/skills/analyze-product-requirements/references/kb-integration.md +56 -0
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +19 -10
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +21 -12
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +45 -14
- package/skills/analyze-product-requirements/scripts/compute-source-identity.mjs +35 -0
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +337 -29
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +38 -7
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +41 -29
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +33 -22
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +43 -24
- package/skills/playwright-cli/SKILL.md +7 -0
- package/skills/analyze-product-dependencies/agents/openai.yaml +0 -4
- package/skills/analyze-product-requirements/agents/openai.yaml +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.33.3] - 2026-08-10
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 修复 Windows 环境下 frontend-test 配置 URL 冻结异常的问题,确保跨平台地址识别行为一致
|
|
10
|
+
|
|
11
|
+
### 改进
|
|
12
|
+
|
|
13
|
+
- 改进 frontend-test 的 Windows URL 回归测试,使其具备更好的跨平台可移植性
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
|
|
17
|
+
- 修复 Windows 环境下 frontend-test 配置 URL(如 baseUrl、targetUrl、loginUrl)无法正确冻结的问题,冲突候选现安全 fail closed,仅在完全未配置时回退 localhost
|
|
18
|
+
|
|
19
|
+
## [0.33.2] - 2026-08-10
|
|
20
|
+
|
|
21
|
+
### 重点更新
|
|
22
|
+
|
|
23
|
+
- 更新 analyze-product 技能
|
|
24
|
+
- 针对结构化产物的可恢复失败自动重试
|
|
25
|
+
|
|
26
|
+
### 改进
|
|
27
|
+
|
|
28
|
+
- 更新 analyze-product 技能
|
|
29
|
+
- 结构化产物在遇到可恢复失败时会自动进行重试
|
|
30
|
+
|
|
5
31
|
## [0.33.1] - 2026-08-10
|
|
6
32
|
|
|
7
33
|
### 重点更新
|
|
@@ -25,21 +25,24 @@ function writeMetric(label, value, color = "#172033") {
|
|
|
25
25
|
}
|
|
26
26
|
function renderCaseCard(item) {
|
|
27
27
|
const colors = statusColor(item.status);
|
|
28
|
-
const rerun = typeof item.rerunAttempt === "number" &&
|
|
29
|
-
item.rerunAttempt > 0
|
|
28
|
+
const rerun = typeof item.rerunAttempt === "number" && item.rerunAttempt > 0
|
|
30
29
|
? `<span class="badge" style="color:#175cd3;background:#eff8ff">重跑 ${item.rerunAttempt}</span>`
|
|
31
30
|
: "";
|
|
32
31
|
const error = item.status === "passed"
|
|
33
32
|
? ""
|
|
34
33
|
: `<div class="error-box"><strong>原因分析</strong><p>${escapeHtml(item.errorAnalysis ?? item.blockedReason ?? "用例未能完成执行。")}</p></div>`;
|
|
35
|
-
const
|
|
34
|
+
const testPoints = item.testPoints ?? [];
|
|
35
|
+
const executionSummary = item.executionSummary ?? "";
|
|
36
|
+
const executionSteps = item.executionSteps ?? [];
|
|
37
|
+
const hasExecution = executionSummary.trim().length > 0 || executionSteps.length > 0;
|
|
38
|
+
const execution = !hasExecution
|
|
36
39
|
? ""
|
|
37
|
-
: `<div class="
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const testPointsHtml =
|
|
40
|
+
: `<div class="execution-box"><strong>执行记录</strong>${executionSummary.trim().length > 0
|
|
41
|
+
? `<p>${escapeHtml(executionSummary)}</p>`
|
|
42
|
+
: ""}${executionSteps.length > 0
|
|
43
|
+
? listHtml(executionSteps)
|
|
44
|
+
: ""}</div>`;
|
|
45
|
+
const testPointsHtml = testPoints.length
|
|
43
46
|
? `<div><h3>测试点</h3>${listHtml(testPoints)}</div>`
|
|
44
47
|
: "";
|
|
45
48
|
return `<details class="case-card" data-status="${item.status}" ${item.status !== "passed" ? "open" : ""}>
|
|
@@ -53,8 +56,8 @@ function renderCaseCard(item) {
|
|
|
53
56
|
<div><h3>测试步骤</h3>${listHtml(item.caseContent.steps)}</div>
|
|
54
57
|
<div><h3>预期结果</h3>${listHtml(item.caseContent.expectedResults)}</div>
|
|
55
58
|
</div>
|
|
56
|
-
${error}
|
|
57
59
|
${execution}
|
|
60
|
+
${error}
|
|
58
61
|
</div>
|
|
59
62
|
</details>`;
|
|
60
63
|
}
|
|
@@ -87,18 +90,30 @@ export async function renderFrontendTestHtmlReport(input) {
|
|
|
87
90
|
? (result.acceptanceCoverage.covered.length / result.sourceBinding.requirementIds.length) * 100
|
|
88
91
|
: 0;
|
|
89
92
|
const outcomeColors = result.outcome === "passed" ? statusColor("passed") : result.outcome === "failed" ? statusColor("failed") : statusColor("blocked");
|
|
90
|
-
const markdownCases = result.cases.map((item) =>
|
|
91
|
-
|
|
92
|
-
""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
const markdownCases = result.cases.map((item) => {
|
|
94
|
+
const testPoints = item.testPoints ?? [];
|
|
95
|
+
const executionSummary = item.executionSummary ?? "";
|
|
96
|
+
const executionSteps = item.executionSteps ?? [];
|
|
97
|
+
return [
|
|
98
|
+
`## ${item.caseId}`,
|
|
99
|
+
"",
|
|
100
|
+
`- 执行结果:${statusLabel(item.status)}`,
|
|
101
|
+
`- 验收标准:${item.acIds.join("、")}`,
|
|
102
|
+
...(typeof item.rerunAttempt === "number" && item.rerunAttempt > 0
|
|
103
|
+
? [`- 重跑次数:${item.rerunAttempt}`]
|
|
104
|
+
: []),
|
|
105
|
+
"",
|
|
106
|
+
"### 测试目的", "", item.caseContent.purpose,
|
|
107
|
+
...(testPoints.length ? ["", "### 测试点", "", listMarkdown(testPoints)] : []),
|
|
108
|
+
"", "### 前置条件", "", listMarkdown(item.caseContent.preconditions),
|
|
109
|
+
"", "### 测试步骤", "", listMarkdown(item.caseContent.steps),
|
|
110
|
+
"", "### 预期结果", "", listMarkdown(item.caseContent.expectedResults),
|
|
111
|
+
...(executionSummary.trim().length || executionSteps.length
|
|
112
|
+
? ["", "### 执行记录", "", executionSummary.trim(), ...(executionSteps.length ? ["", listMarkdown(executionSteps)] : [])]
|
|
113
|
+
: []),
|
|
114
|
+
...(item.status === "passed" ? [] : ["", "### 错误分析", "", item.errorAnalysis ?? item.blockedReason ?? "用例未能完成执行。"]),
|
|
115
|
+
];
|
|
116
|
+
}).map((lines) => lines.filter((line) => line !== undefined).join("\n")).join("\n\n");
|
|
102
117
|
const markdown = [
|
|
103
118
|
"# 前端功能测试报告", "",
|
|
104
119
|
`- 测试结论:${outcomeLabel}`,
|
|
@@ -117,7 +132,7 @@ export async function renderFrontendTestHtmlReport(input) {
|
|
|
117
132
|
? `<section class="panel"><div class="section-heading"><h2>失败概览</h2><span>${failedCount} 条未通过用例</span></div><div class="failure-list">${result.cases.filter((item) => item.status !== "passed").map((item) => `<div class="failure-row"><span class="case-id">${escapeHtml(item.caseId)}</span><span>${escapeHtml(item.errorAnalysis ?? item.blockedReason ?? "用例未能完成执行。")}</span></div>`).join("")}</div></section>`
|
|
118
133
|
: "";
|
|
119
134
|
const html = `<!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>前端功能测试报告</title><style>
|
|
120
|
-
body{margin:0;background:radial-gradient(circle at 8% 0,#edf4ff 0,transparent 36rem),#f5f7fb;color:#17243b;font:15px/1.6 Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif}main{max-width:1180px;margin:0 auto;padding:42px 28px 72px}.hero{padding:30px 34px;border:1px solid #294b78;border-radius:24px;background:linear-gradient(125deg,#102849 0%,#173d6d 57%,#245b91 100%);box-shadow:0 18px 40px rgba(16,40,73,.18);display:flex;align-items:flex-start;justify-content:space-between;gap:28px}.eyebrow{color:#9fc6ee;font-size:11px;font-weight:800;letter-spacing:.2em}.hero h1{margin:10px 0 8px;color:#f7fbff;font-size:clamp(28px,3.8vw,44px);line-height:1.1}.hero p{margin:0;color:#bdd2e9;font-size:13px}.decision{min-width:190px;padding:15px 17px;border:1px solid ${outcomeColors.border};border-radius:16px;background:${outcomeColors.bg};color:${outcomeColors.fg}}.decision strong{display:block;font-size:17px}.decision span{display:block;margin-top:4px;font-size:12px}.panel{background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px;margin-top:18px}.metrics{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-top:18px}.metric{position:relative;min-height:92px;padding:17px 18px;background:#fff;border:1px solid #e1e8f2;border-radius:13px}.metric:before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:#4775ef;border-radius:13px 13px 0 0}.metric span{display:block;color:#748198;font-size:12px}.metric strong{display:block;margin-top:12px;font-size:28px;line-height:1;font-weight:800}.section-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px}.section-heading h2{margin:0;color:#17365d;font-size:19px}.section-heading span{color:#718097;font-size:12px}.signal-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.signal{padding:14px;border:1px solid #e4e9f1;border-radius:11px;background:#fbfcfe}.signal header{display:flex;justify-content:space-between;color:#2a3c5a;font-size:12px}.bar{height:7px;margin:10px 0 6px;background:#edf1f6;border-radius:99px;overflow:hidden}.bar i{display:block;height:100%;border-radius:inherit}.signal small{color:#718097;font-size:11px}.case-card{border:1px solid #e4e9f1;border-radius:13px;background:#fff;overflow:hidden;margin-top:10px}.case-card summary{display:flex;align-items:center;gap:12px;padding:14px 16px;cursor:pointer;list-style:none}.case-card summary::-webkit-details-marker{display:none}.case-id{color:#2b3a55;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap}.case-title{flex:1;min-width:0;color:#172033;font-weight:650;overflow:hidden;text-overflow:ellipsis}.badge{padding:3px 10px;border-radius:999px;font-size:11px;font-weight:800;white-space:nowrap}.chevron{color:#aab2bf}.case-body{padding:3px 16px 17px;border-top:1px solid #f0f3f7}.case-meta{margin-top:12px;padding:9px 11px;background:#f6f8fa;border-radius:8px;color:#667085;font-size:11px}.case-meta strong{margin-left:8px;color:#344054}.case-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:13px}.case-grid>div{padding:11px 12px;border:1px solid #edf0f4;border-radius:9px;background:#fbfcfe}.case-grid h3{margin:0 0 5px;color:#667085;font-size:11px}.case-grid p,.case-grid ol{margin:0;color:#475467;font-size:13px}.case-grid ol{padding-left:20px}.muted{color:#98a2b3!important}.error-box{margin-top:13px;padding:11px 13px;border:1px solid #fda29b;border-radius:9px;background:#fef3f2;color:#b42318}.error-box strong{font-size:12px}.error-box p{margin:4px 0 0;white-space:pre-wrap;font-size:13px}.failure-list{display:grid;gap:9px}.failure-row{display:flex;align-items:baseline;gap:12px;padding:10px 12px;border-left:4px solid #d34661;border-radius:8px;background:#fff8f9;color:#475467;font-size:13px}.success-note{padding:11px 13px;border-radius:9px;background:#ecfdf3;color:#067647;font-size:13px}.finding-list{padding:11px 13px;border-radius:9px;background:#fffaeb;color:#946200;font-size:13px}.finding-list ol{margin:5px 0 0;padding-left:20px}@media(max-width:850px){.hero{display:block}.decision{margin-top:20px;min-width:0}.metrics{grid-template-columns:repeat(2,1fr)}.signal-grid,.case-grid{grid-template-columns:1fr}}@media(max-width:520px){main{padding:22px 14px 42px}.metrics{grid-template-columns:1fr}.case-card summary{flex-wrap:wrap}.case-title{order:3;flex-basis:100%}}
|
|
135
|
+
body{margin:0;background:radial-gradient(circle at 8% 0,#edf4ff 0,transparent 36rem),#f5f7fb;color:#17243b;font:15px/1.6 Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif}main{max-width:1180px;margin:0 auto;padding:42px 28px 72px}.hero{padding:30px 34px;border:1px solid #294b78;border-radius:24px;background:linear-gradient(125deg,#102849 0%,#173d6d 57%,#245b91 100%);box-shadow:0 18px 40px rgba(16,40,73,.18);display:flex;align-items:flex-start;justify-content:space-between;gap:28px}.eyebrow{color:#9fc6ee;font-size:11px;font-weight:800;letter-spacing:.2em}.hero h1{margin:10px 0 8px;color:#f7fbff;font-size:clamp(28px,3.8vw,44px);line-height:1.1}.hero p{margin:0;color:#bdd2e9;font-size:13px}.decision{min-width:190px;padding:15px 17px;border:1px solid ${outcomeColors.border};border-radius:16px;background:${outcomeColors.bg};color:${outcomeColors.fg}}.decision strong{display:block;font-size:17px}.decision span{display:block;margin-top:4px;font-size:12px}.panel{background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px;margin-top:18px}.metrics{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-top:18px}.metric{position:relative;min-height:92px;padding:17px 18px;background:#fff;border:1px solid #e1e8f2;border-radius:13px}.metric:before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:#4775ef;border-radius:13px 13px 0 0}.metric span{display:block;color:#748198;font-size:12px}.metric strong{display:block;margin-top:12px;font-size:28px;line-height:1;font-weight:800}.section-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px}.section-heading h2{margin:0;color:#17365d;font-size:19px}.section-heading span{color:#718097;font-size:12px}.signal-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.signal{padding:14px;border:1px solid #e4e9f1;border-radius:11px;background:#fbfcfe}.signal header{display:flex;justify-content:space-between;color:#2a3c5a;font-size:12px}.bar{height:7px;margin:10px 0 6px;background:#edf1f6;border-radius:99px;overflow:hidden}.bar i{display:block;height:100%;border-radius:inherit}.signal small{color:#718097;font-size:11px}.case-card{border:1px solid #e4e9f1;border-radius:13px;background:#fff;overflow:hidden;margin-top:10px}.case-card summary{display:flex;align-items:center;gap:12px;padding:14px 16px;cursor:pointer;list-style:none}.case-card summary::-webkit-details-marker{display:none}.case-id{color:#2b3a55;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap}.case-title{flex:1;min-width:0;color:#172033;font-weight:650;overflow:hidden;text-overflow:ellipsis}.badge{padding:3px 10px;border-radius:999px;font-size:11px;font-weight:800;white-space:nowrap}.chevron{color:#aab2bf}.case-body{padding:3px 16px 17px;border-top:1px solid #f0f3f7}.case-meta{margin-top:12px;padding:9px 11px;background:#f6f8fa;border-radius:8px;color:#667085;font-size:11px}.case-meta strong{margin-left:8px;color:#344054}.case-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:13px}.case-grid>div{padding:11px 12px;border:1px solid #edf0f4;border-radius:9px;background:#fbfcfe}.case-grid h3{margin:0 0 5px;color:#667085;font-size:11px}.case-grid p,.case-grid ol{margin:0;color:#475467;font-size:13px}.case-grid ol{padding-left:20px}.muted{color:#98a2b3!important}.error-box{margin-top:13px;padding:11px 13px;border:1px solid #fda29b;border-radius:9px;background:#fef3f2;color:#b42318}.execution-box{margin-top:13px;padding:11px 13px;border:1px solid #b9d6f5;border-radius:9px;background:#f5f9ff;color:#1f4e79}.execution-box strong{font-size:12px}.execution-box p{margin:4px 0 0;white-space:pre-wrap;font-size:13px}.execution-box ol{margin:6px 0 0;padding-left:20px;color:#28456b;font-size:13px}.error-box strong{font-size:12px}.error-box p{margin:4px 0 0;white-space:pre-wrap;font-size:13px}.failure-list{display:grid;gap:9px}.failure-row{display:flex;align-items:baseline;gap:12px;padding:10px 12px;border-left:4px solid #d34661;border-radius:8px;background:#fff8f9;color:#475467;font-size:13px}.success-note{padding:11px 13px;border-radius:9px;background:#ecfdf3;color:#067647;font-size:13px}.finding-list{padding:11px 13px;border-radius:9px;background:#fffaeb;color:#946200;font-size:13px}.finding-list ol{margin:5px 0 0;padding-left:20px}@media(max-width:850px){.hero{display:block}.decision{margin-top:20px;min-width:0}.metrics{grid-template-columns:repeat(2,1fr)}.signal-grid,.case-grid{grid-template-columns:1fr}}@media(max-width:520px){main{padding:22px 14px 42px}.metrics{grid-template-columns:1fr}.case-card summary{flex-wrap:wrap}.case-title{order:3;flex-basis:100%}}
|
|
121
136
|
|
|
122
137
|
.filter-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0 16px}
|
|
123
138
|
.filter-label{font-size:13px;color:#475467}
|
|
@@ -37,6 +37,7 @@ export const frontendTestResultContractSchema = z.object({
|
|
|
37
37
|
rerunAttempt: z.number().int().nonnegative().optional(),
|
|
38
38
|
testPoints: z.array(z.string()).optional(),
|
|
39
39
|
executionSummary: z.string().optional(),
|
|
40
|
+
executionSteps: z.array(z.string()).optional(),
|
|
40
41
|
caseContent: z.object({
|
|
41
42
|
purpose: z.string(),
|
|
42
43
|
preconditions: z.array(z.string()),
|
|
@@ -233,18 +234,56 @@ function markdownList(value) {
|
|
|
233
234
|
const items = value.split(/\r?\n/).map((line) => line.replace(/^\s*(?:\d+[.)]|[-*+])\s+/, "").trim()).filter(Boolean);
|
|
234
235
|
return items;
|
|
235
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Read planned case facts from the case Markdown. Recognizes both English and
|
|
239
|
+
* Chinese headings, including `测试点` (test points) and `测试步骤` (planned
|
|
240
|
+
* test steps), alongside the legacy `操作步骤`/`Steps` headings.
|
|
241
|
+
*/
|
|
236
242
|
async function readFrontendCaseContent(workspaceRoot, casePath) {
|
|
237
243
|
if (!safeRelativePathSchema.safeParse(casePath).success || !casePath.startsWith("testcase/frontend/cases/")) {
|
|
238
244
|
throw new Error(`unsafe frontend case path: ${casePath}`);
|
|
239
245
|
}
|
|
240
246
|
const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
|
|
241
247
|
return {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
caseContent: {
|
|
249
|
+
purpose: markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) || "未提供测试目的",
|
|
250
|
+
preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
|
|
251
|
+
steps: markdownList(markdownSection(body, ["Test Steps", "测试步骤", "Steps", "操作步骤"])),
|
|
252
|
+
expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
|
|
253
|
+
},
|
|
254
|
+
testPoints: markdownList(markdownSection(body, ["Test Points", "测试点"])),
|
|
246
255
|
};
|
|
247
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Parse bounded fixed sections from execution.md. Only a small allowlist of
|
|
259
|
+
* headings is recognized and each section is length-capped so model prose
|
|
260
|
+
* cannot flood the report. Missing sections return empty values (advisory only).
|
|
261
|
+
*/
|
|
262
|
+
const EXECUTION_SUMMARY_MAX_LENGTH = 1200;
|
|
263
|
+
const EXECUTION_STEPS_MAX_ITEMS = 40;
|
|
264
|
+
function parseExecutionMarkdown(body) {
|
|
265
|
+
const summarySection = markdownSection(body, [
|
|
266
|
+
"Execution Summary",
|
|
267
|
+
"Actual Execution Summary",
|
|
268
|
+
"执行摘要",
|
|
269
|
+
"实际执行摘要",
|
|
270
|
+
"执行总结",
|
|
271
|
+
]);
|
|
272
|
+
const rawSummary = summarySection.replace(/\r\n/g, "\n").trim();
|
|
273
|
+
const summary = rawSummary.length > EXECUTION_SUMMARY_MAX_LENGTH
|
|
274
|
+
? `${rawSummary.slice(0, EXECUTION_SUMMARY_MAX_LENGTH - 1)}…`
|
|
275
|
+
: rawSummary;
|
|
276
|
+
const stepsSection = markdownSection(body, [
|
|
277
|
+
"Actual Steps",
|
|
278
|
+
"Actual Execution Steps",
|
|
279
|
+
"Execution Steps",
|
|
280
|
+
"实际执行步骤",
|
|
281
|
+
"实际步骤",
|
|
282
|
+
"执行步骤",
|
|
283
|
+
]);
|
|
284
|
+
const steps = markdownList(stepsSection).slice(0, EXECUTION_STEPS_MAX_ITEMS);
|
|
285
|
+
return { summary, steps };
|
|
286
|
+
}
|
|
248
287
|
function assertInside(root, candidate, label) {
|
|
249
288
|
const relative = path.relative(root, candidate);
|
|
250
289
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
@@ -371,7 +410,23 @@ export async function materializeFrontendTestResult(input) {
|
|
|
371
410
|
});
|
|
372
411
|
}
|
|
373
412
|
}
|
|
374
|
-
const caseContent = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
|
|
413
|
+
const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
|
|
414
|
+
// Parse bounded fixed sections from execution.md. Missing optional
|
|
415
|
+
// summary/steps are advisory only and never downgrade a passed status.
|
|
416
|
+
let executionSummary = "";
|
|
417
|
+
let executionSteps = [];
|
|
418
|
+
try {
|
|
419
|
+
const executionBody = await readFile(path.join(evidenceRoot, "execution.md"), "utf8");
|
|
420
|
+
const parsed = parseExecutionMarkdown(executionBody);
|
|
421
|
+
executionSummary = parsed.summary;
|
|
422
|
+
executionSteps = parsed.steps;
|
|
423
|
+
if (!executionSummary && executionSteps.length === 0) {
|
|
424
|
+
advisoryFindings.push({ ruleId: "execution-summary-unavailable", caseId: item.caseId, detail: "execution.md has no bounded execution summary/steps; reporting remains status-driven" });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
advisoryFindings.push({ ruleId: "execution-summary-unavailable", caseId: item.caseId, detail: "execution.md summary could not be parsed; reporting remains status-driven" });
|
|
429
|
+
}
|
|
375
430
|
const explicitAnalysis = typeof resultRaw.errorAnalysis === "string" && resultRaw.errorAnalysis.trim()
|
|
376
431
|
? resultRaw.errorAnalysis.trim()
|
|
377
432
|
: typeof resultRaw.errorSummary === "string" && resultRaw.errorSummary.trim()
|
|
@@ -380,7 +435,23 @@ export async function materializeFrontendTestResult(input) {
|
|
|
380
435
|
const errorAnalysis = status === "passed"
|
|
381
436
|
? undefined
|
|
382
437
|
: explicitAnalysis ?? (status === "blocked" ? `用例因 ${blockedReason ?? "未知原因"} 未能完成执行。` : "用例执行失败,但执行结果未提供详细错误分析。");
|
|
383
|
-
|
|
438
|
+
const rerunAttemptRaw = resultRaw.rerunAttempt;
|
|
439
|
+
const rerunAttempt = typeof rerunAttemptRaw === "number" && Number.isFinite(rerunAttemptRaw) && rerunAttemptRaw >= 0
|
|
440
|
+
? Math.trunc(rerunAttemptRaw)
|
|
441
|
+
: undefined;
|
|
442
|
+
cases.push({
|
|
443
|
+
caseId: item.caseId,
|
|
444
|
+
acIds: item.acIds,
|
|
445
|
+
status,
|
|
446
|
+
evidence,
|
|
447
|
+
caseContent,
|
|
448
|
+
...(blockedReason ? { blockedReason } : {}),
|
|
449
|
+
...(errorAnalysis ? { errorAnalysis } : {}),
|
|
450
|
+
...(rerunAttempt !== undefined ? { rerunAttempt } : {}),
|
|
451
|
+
...(testPoints.length > 0 ? { testPoints } : {}),
|
|
452
|
+
...(executionSummary ? { executionSummary } : {}),
|
|
453
|
+
...(executionSteps.length > 0 ? { executionSteps } : {}),
|
|
454
|
+
});
|
|
384
455
|
}
|
|
385
456
|
const missing = [...requiredIds].filter((id) => !covered.has(id));
|
|
386
457
|
const totals = {
|
|
@@ -3939,34 +3939,129 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3939
3939
|
assertValidDagSpec(spec);
|
|
3940
3940
|
return spec;
|
|
3941
3941
|
}
|
|
3942
|
+
// Explicit allowlist of controller-owned reference filenames that may declare
|
|
3943
|
+
// a non-production browser URL. Hash-bound task sources are the only authority;
|
|
3944
|
+
// model-authored RAG/case/evidence files are never consulted.
|
|
3945
|
+
const FRONTEND_BASE_URL_DOCUMENT_ALLOWLIST = [
|
|
3946
|
+
/(?:^|[\\/])config\.md$/i,
|
|
3947
|
+
/(?:^|[\\/])environment\.md$/i,
|
|
3948
|
+
/(?:^|[\\/])env\.md$/i,
|
|
3949
|
+
/(?:^|[\\/])urls?\.md$/i,
|
|
3950
|
+
/(?:^|[\\/])references\.md$/i,
|
|
3951
|
+
/(?:^|[\\/])task-reference\.md$/i,
|
|
3952
|
+
/(?:^|[\\/])task-source\.md$/i,
|
|
3953
|
+
];
|
|
3954
|
+
// Explicit allowlist of URL keys. The semantic name drives a deterministic
|
|
3955
|
+
// priority so two conflicting same-priority URLs fail closed instead of being
|
|
3956
|
+
// silently picked.
|
|
3957
|
+
const FRONTEND_BASE_URL_KEY_ALLOWLIST = [
|
|
3958
|
+
// The primary frontend browser entry point is the strongest signal.
|
|
3959
|
+
{ name: "baseurl", priority: 0 },
|
|
3960
|
+
{ name: "base url", priority: 0 },
|
|
3961
|
+
{ name: "targeturl", priority: 0 },
|
|
3962
|
+
{ name: "target url", priority: 0 },
|
|
3963
|
+
{ name: "frontendbaseurl", priority: 1 },
|
|
3964
|
+
{ name: "frontend base url", priority: 1 },
|
|
3965
|
+
{ name: "frontendurl", priority: 1 },
|
|
3966
|
+
{ name: "frontend url", priority: 1 },
|
|
3967
|
+
{ name: "loginurl", priority: 2 },
|
|
3968
|
+
{ name: "login url", priority: 2 },
|
|
3969
|
+
];
|
|
3970
|
+
const DEFAULT_FRONTEND_BASE_URL = "http://localhost:5173/";
|
|
3971
|
+
const DEFAULT_FRONTEND_BASE_URL_SOURCE = "default-localhost-5173";
|
|
3972
|
+
function isAllowedFrontendBaseUrlDocument(documentPath) {
|
|
3973
|
+
return FRONTEND_BASE_URL_DOCUMENT_ALLOWLIST.some((pattern) => pattern.test(documentPath));
|
|
3974
|
+
}
|
|
3975
|
+
function normalizeFrontendBaseUrlKey(raw) {
|
|
3976
|
+
// Normalize separators: kebab/snake/space variants collapse to a comparable
|
|
3977
|
+
// token. Lowercase so casing differences do not bypass the allowlist.
|
|
3978
|
+
return raw
|
|
3979
|
+
.toLowerCase()
|
|
3980
|
+
.replace(/[_\-]+/g, " ")
|
|
3981
|
+
.replace(/\s+/g, " ")
|
|
3982
|
+
.trim();
|
|
3983
|
+
}
|
|
3942
3984
|
/**
|
|
3943
3985
|
* Resolve the browser origin only from controller-owned task source bytes.
|
|
3944
3986
|
* Model-authored RAG/case/evidence files are intentionally excluded.
|
|
3987
|
+
*
|
|
3988
|
+
* Safety contract:
|
|
3989
|
+
* - Only hash-bound controller reference documents on the filename allowlist
|
|
3990
|
+
* may declare a URL.
|
|
3991
|
+
* - Only the explicit key allowlist (baseUrl / targetUrl / loginUrl + common
|
|
3992
|
+
* case/separator variants) is recognized.
|
|
3993
|
+
* - Candidates are ranked deterministically by key priority then by document
|
|
3994
|
+
* order; equal-priority conflicting URLs fail closed.
|
|
3995
|
+
* - The resolved URL must be non-production http(s) without credentials, query,
|
|
3996
|
+
* or fragment. Only when zero candidates exist does the localhost default
|
|
3997
|
+
* apply.
|
|
3945
3998
|
*/
|
|
3946
3999
|
export function resolveControllerFrontendBaseUrl(sources) {
|
|
3947
|
-
const
|
|
3948
|
-
.
|
|
3949
|
-
.
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
const
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
4000
|
+
const allowedKeyByNormalized = new Map(FRONTEND_BASE_URL_KEY_ALLOWLIST.map((variant) => [
|
|
4001
|
+
variant.name,
|
|
4002
|
+
variant.priority,
|
|
4003
|
+
]));
|
|
4004
|
+
const candidates = [];
|
|
4005
|
+
const documents = sources.referenceDocuments ?? [];
|
|
4006
|
+
documents.forEach((document, documentIndex) => {
|
|
4007
|
+
if (!isAllowedFrontendBaseUrlDocument(document.path))
|
|
4008
|
+
return;
|
|
4009
|
+
const source = toDagSourcePath(sources, document.path);
|
|
4010
|
+
// Only accept `key: value` / `key = value` lines. A colon or equals
|
|
4011
|
+
// sign after a known key bounds the URL; free-form prose is ignored.
|
|
4012
|
+
const keyPattern = /^[ ]*(?:#>*)?[ ]*([A-Za-z][A-Za-z0-9 _\-]*?)[ ]*[:=][ ]*["'`]?((?:https?:)?\/\/[\S]+?)(?:["'`])?[ ]*(?:#.*)?$/gm;
|
|
4013
|
+
let match;
|
|
4014
|
+
while ((match = keyPattern.exec(document.markdown)) !== null) {
|
|
4015
|
+
const rawKey = match[1].trim();
|
|
4016
|
+
const normalizedKey = normalizeFrontendBaseUrlKey(rawKey);
|
|
4017
|
+
const priority = allowedKeyByNormalized.get(normalizedKey);
|
|
4018
|
+
if (priority === undefined)
|
|
4019
|
+
continue;
|
|
4020
|
+
let rawUrl = match[2].replace(/[)\]},.;]+$/, "");
|
|
4021
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(rawUrl)) {
|
|
4022
|
+
rawUrl = `https://${rawUrl.replace(/^\/\/+/, "")}`;
|
|
3960
4023
|
}
|
|
3961
|
-
|
|
4024
|
+
candidates.push({
|
|
4025
|
+
url: rawUrl,
|
|
4026
|
+
source,
|
|
4027
|
+
keyName: normalizedKey,
|
|
4028
|
+
priority,
|
|
4029
|
+
documentIndex,
|
|
4030
|
+
});
|
|
3962
4031
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
4032
|
+
});
|
|
4033
|
+
if (candidates.length === 0) {
|
|
4034
|
+
return {
|
|
4035
|
+
baseUrl: DEFAULT_FRONTEND_BASE_URL,
|
|
4036
|
+
baseUrlSource: DEFAULT_FRONTEND_BASE_URL_SOURCE,
|
|
4037
|
+
};
|
|
3966
4038
|
}
|
|
4039
|
+
// Deterministic ranking: lowest priority number first, then earliest
|
|
4040
|
+
// document, then earliest occurrence in that document.
|
|
4041
|
+
candidates.sort((left, right) => {
|
|
4042
|
+
if (left.priority !== right.priority) {
|
|
4043
|
+
return left.priority - right.priority;
|
|
4044
|
+
}
|
|
4045
|
+
if (left.documentIndex !== right.documentIndex) {
|
|
4046
|
+
return left.documentIndex - right.documentIndex;
|
|
4047
|
+
}
|
|
4048
|
+
return 0;
|
|
4049
|
+
});
|
|
4050
|
+
const bestPriority = candidates[0].priority;
|
|
4051
|
+
const bestPriorityCandidates = candidates.filter((candidate) => candidate.priority === bestPriority);
|
|
4052
|
+
// Fail closed: two distinct URLs at the same priority level are ambiguous.
|
|
4053
|
+
const distinctUrls = new Set(bestPriorityCandidates.map((candidate) => candidate.url));
|
|
4054
|
+
if (distinctUrls.size > 1) {
|
|
4055
|
+
const conflictingSources = bestPriorityCandidates
|
|
4056
|
+
.map((candidate) => `${candidate.source}:${candidate.keyName}`)
|
|
4057
|
+
.join(", ");
|
|
4058
|
+
throw new Error(`frontend-test controller baseUrl is ambiguous (${conflictingSources})`);
|
|
4059
|
+
}
|
|
4060
|
+
const selected = bestPriorityCandidates[0];
|
|
4061
|
+
const baseUrlSource = selected.source;
|
|
3967
4062
|
let parsed;
|
|
3968
4063
|
try {
|
|
3969
|
-
parsed = new URL(
|
|
4064
|
+
parsed = new URL(selected.url);
|
|
3970
4065
|
}
|
|
3971
4066
|
catch {
|
|
3972
4067
|
throw new Error(`frontend-test controller baseUrl is invalid (${baseUrlSource})`);
|
|
@@ -4399,10 +4494,28 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4399
4494
|
},
|
|
4400
4495
|
},
|
|
4401
4496
|
});
|
|
4402
|
-
|
|
4497
|
+
let rerunDependency = "execute-frontend-cases-map";
|
|
4498
|
+
for (let round = 1; round <= maxRerunAttempts; round += 1) {
|
|
4499
|
+
const selectorId = round === 1
|
|
4500
|
+
? "select-frontend-rerun-candidates-shell"
|
|
4501
|
+
: `select-frontend-rerun-candidates-round${round}-shell`;
|
|
4502
|
+
const mapId = round === 1
|
|
4503
|
+
? "rerun-frontend-cases-map"
|
|
4504
|
+
: `rerun-frontend-cases-round${round}-map`;
|
|
4505
|
+
const candidateArtifact = round === 1
|
|
4506
|
+
? "rerun-candidates.json"
|
|
4507
|
+
: `rerun-candidates-round${round}.json`;
|
|
4508
|
+
const selectCommand = [
|
|
4509
|
+
"const fs=require('fs'),path=require('path');",
|
|
4510
|
+
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
4511
|
+
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
4512
|
+
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
4513
|
+
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
4514
|
+
`fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
4515
|
+
].join("");
|
|
4403
4516
|
tasks.push({
|
|
4404
|
-
id:
|
|
4405
|
-
depends_on: [
|
|
4517
|
+
id: selectorId,
|
|
4518
|
+
depends_on: [rerunDependency],
|
|
4406
4519
|
role: "verifier",
|
|
4407
4520
|
executor: "shell",
|
|
4408
4521
|
complexity: "LOW",
|
|
@@ -4410,42 +4523,33 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4410
4523
|
writeSet: ["testcase/frontend/evidence/**"],
|
|
4411
4524
|
allowedPaths: [...casesWriteSet, "testcase/frontend/evidence/**"],
|
|
4412
4525
|
forbiddenPaths: forbidden,
|
|
4413
|
-
outputContract:
|
|
4414
|
-
subtask_prompt: "Select frontend-test cases eligible for bounded rerun.",
|
|
4526
|
+
outputContract: `Stdout JSON {cases} for blocked or missing-result-file cases eligible for rerun round ${round}/${maxRerunAttempts}.`,
|
|
4527
|
+
subtask_prompt: "Select frontend-test cases eligible for this bounded rerun round.",
|
|
4415
4528
|
shell: {
|
|
4416
|
-
commands: [
|
|
4417
|
-
[
|
|
4418
|
-
"node -e",
|
|
4419
|
-
JSON.stringify("const fs=require('fs'),path=require('path');const manifestPath='testcase/frontend/cases/manifest.json';if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(e){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" +
|
|
4420
|
-
maxRerunAttempts +
|
|
4421
|
-
";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/rerun-candidates.json',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));"),
|
|
4422
|
-
].join(" "),
|
|
4423
|
-
],
|
|
4529
|
+
commands: [["node -e", JSON.stringify(selectCommand)].join(" ")],
|
|
4424
4530
|
cwd: ".",
|
|
4425
4531
|
timeoutMs: 120_000,
|
|
4426
4532
|
},
|
|
4427
4533
|
}, {
|
|
4428
|
-
id:
|
|
4429
|
-
depends_on: [
|
|
4534
|
+
id: mapId,
|
|
4535
|
+
depends_on: [selectorId],
|
|
4430
4536
|
role: "verifier",
|
|
4431
4537
|
executor: "static",
|
|
4432
4538
|
complexity: "LOW",
|
|
4433
4539
|
writePolicy: "none",
|
|
4434
4540
|
allowedPaths: [],
|
|
4435
4541
|
forbiddenPaths: forbidden,
|
|
4436
|
-
outputContract:
|
|
4542
|
+
outputContract: `Serial rerun round ${round}/${maxRerunAttempts} of blocked or missing-result frontend cases.`,
|
|
4437
4543
|
subtask_prompt: "Expand rerun candidates into serial browser case children.",
|
|
4438
|
-
static: {
|
|
4439
|
-
resultMarkdown: "Frontend case rerun map expansion barrier.",
|
|
4440
|
-
},
|
|
4544
|
+
static: { resultMarkdown: `Frontend case rerun round ${round} map expansion barrier.` },
|
|
4441
4545
|
dynamicExpansion: {
|
|
4442
4546
|
type: "map_agent",
|
|
4443
|
-
workflowNodeId:
|
|
4444
|
-
itemsFrom:
|
|
4547
|
+
workflowNodeId: mapId,
|
|
4548
|
+
itemsFrom: `$.nodes['${selectorId}'].output.cases`,
|
|
4445
4549
|
itemName: "case",
|
|
4446
4550
|
maxItems: config.maxCasesPerBatch,
|
|
4447
4551
|
maxExpandedNodes: config.maxCasesPerBatch,
|
|
4448
|
-
childIdPrefix:
|
|
4552
|
+
childIdPrefix: `rerun-frontend-case-r${round}`,
|
|
4449
4553
|
workspaceTemplate: "{{case.evidenceDir}}",
|
|
4450
4554
|
tolerateChildFailures: true,
|
|
4451
4555
|
tokenBudget: {
|
|
@@ -4457,10 +4561,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4457
4561
|
role: "implementer",
|
|
4458
4562
|
skills: ["playwright-cli"],
|
|
4459
4563
|
toolProfile: "write",
|
|
4460
|
-
commandPolicy: {
|
|
4461
|
-
mode: "capability-allowlist",
|
|
4462
|
-
capabilities: ["playwright-cli"],
|
|
4463
|
-
},
|
|
4564
|
+
commandPolicy: { mode: "capability-allowlist", capabilities: ["playwright-cli"] },
|
|
4464
4565
|
complexity: "MED",
|
|
4465
4566
|
writePolicy: "exclusive",
|
|
4466
4567
|
writeGuardPolicy: "tools-only",
|
|
@@ -4472,23 +4573,22 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4472
4573
|
],
|
|
4473
4574
|
forbiddenPaths: forbidden,
|
|
4474
4575
|
writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
|
|
4475
|
-
outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, tokens, and rerunAttempt.",
|
|
4576
|
+
outputContract: "Compact JSON <=1200 characters with final case status, evidence paths, error summary, tokens, and rerunAttempt.",
|
|
4476
4577
|
subtaskPromptTemplate: [
|
|
4477
|
-
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite authoritative
|
|
4578
|
+
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
|
|
4478
4579
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
4479
4580
|
`Start via playwright_cli command=open with args [--browser=chrome, ${controllerFrontend.baseUrl}]. Passed requires open → find → cleanup receipts.`,
|
|
4480
|
-
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}.",
|
|
4581
|
+
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
|
|
4481
4582
|
].join("\n\n"),
|
|
4482
4583
|
},
|
|
4483
4584
|
},
|
|
4484
4585
|
});
|
|
4586
|
+
rerunDependency = mapId;
|
|
4485
4587
|
}
|
|
4486
4588
|
tasks.push({
|
|
4487
4589
|
id: "finalize-frontend-test-result-shell",
|
|
4488
4590
|
depends_on: [
|
|
4489
|
-
|
|
4490
|
-
? "rerun-frontend-cases-map"
|
|
4491
|
-
: "execute-frontend-cases-map",
|
|
4591
|
+
rerunDependency,
|
|
4492
4592
|
],
|
|
4493
4593
|
role: "verifier",
|
|
4494
4594
|
executor: "shell",
|
|
@@ -19,6 +19,8 @@ export const DEFAULT_DAG_RETRY_CATEGORIES = [
|
|
|
19
19
|
];
|
|
20
20
|
export const STRUCTURED_OUTPUT_RETRY_CATEGORY = "output-too-large";
|
|
21
21
|
export const PROTOCOL_INVALID_RETRY_CATEGORY = "protocol-invalid";
|
|
22
|
+
/** Recoverable model artifact/schema formatting failure on read-only structured nodes. */
|
|
23
|
+
export const STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY = "invalid-output";
|
|
22
24
|
/** Retry only a proven no-op from an explicitly opt-in bounded Pi writer. */
|
|
23
25
|
export const WRITER_EMPTY_DIFF_RETRY_CATEGORY = "writer-empty-diff";
|
|
24
26
|
/** Retry when a backend-test writer finished but Completeness Gate found missing/broken targets. */
|
|
@@ -26,16 +28,19 @@ export const INCOMPLETE_WRITE_SET_RETRY_CATEGORY = "incomplete-write-set";
|
|
|
26
28
|
export const STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES = [
|
|
27
29
|
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
28
30
|
STRUCTURED_OUTPUT_RETRY_CATEGORY,
|
|
31
|
+
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
29
32
|
];
|
|
30
33
|
/** Categories allowed on nodes that declare a machine-readable outputProtocol. */
|
|
31
34
|
export const PROTOCOL_AWARE_DAG_RETRY_CATEGORIES = [
|
|
32
35
|
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
33
36
|
PROTOCOL_INVALID_RETRY_CATEGORY,
|
|
37
|
+
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
34
38
|
];
|
|
35
39
|
export const ALL_DAG_RETRY_CATEGORIES = [
|
|
36
40
|
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
37
41
|
STRUCTURED_OUTPUT_RETRY_CATEGORY,
|
|
38
42
|
PROTOCOL_INVALID_RETRY_CATEGORY,
|
|
43
|
+
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
39
44
|
WRITER_EMPTY_DIFF_RETRY_CATEGORY,
|
|
40
45
|
INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
|
|
41
46
|
];
|
|
@@ -52,7 +52,7 @@ Each case must require the executor to persist, even when blocked:
|
|
|
52
52
|
- `testcase/frontend/evidence/<case-id>/execution.md`
|
|
53
53
|
- `testcase/frontend/evidence/<case-id>/case-result.json`
|
|
54
54
|
|
|
55
|
-
`case-result.json` must be valid JSON containing the matching `caseId`, `status` (`passed`, `failed`, or `blocked`), and an `evidencePaths` array. A blocked result must include a non-empty `blockedReason`, such as `isolated-test-environment-unavailable` or `token-budget-exhausted`, and must never claim or imply a pass. `execution.md` records attempted or blocked steps, base-URL safety decision, fixture/reset and request-observation availability, timestamps, and the evidence-file list. Available evidence files are required only when actually produced and must stay under the same case evidence directory.
|
|
55
|
+
`case-result.json` must be valid JSON containing the matching `caseId`, final `status` (`passed`, `failed`, or `blocked`), and an `evidencePaths` array. A rerun must overwrite this same file with its final status and integer `rerunAttempt`; the final report consumes that latest file. A blocked result must include a non-empty `blockedReason`, such as `isolated-test-environment-unavailable` or `token-budget-exhausted`, and must never claim or imply a pass. `execution.md` records attempted or blocked steps, base-URL safety decision, fixture/reset and request-observation availability, timestamps, and the evidence-file list. When available, use fixed headings `### 执行摘要` and `### 实际执行步骤` so the bounded deterministic report extractor can show actual execution facts. Available evidence files are required only when actually produced and must stay under the same case evidence directory.
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
## Standard scenarios (controller)
|