@tea-agent/loop-agent 0.38.2 → 0.38.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 +18 -0
- package/README.md +2 -0
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +58 -1
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +56 -2
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
- package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-Bj2M8iO5.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
- package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DTZOKgAn.js} +142 -63
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-DdnapKIH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-BVjAT9b8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/observe/node-input.js +72 -3
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/state.js +2 -2
- package/dist/worker/observe/static/views/dag-inspector.js +51 -0
- package/dist/worker/observe/static/views/session-timeline.js +18 -5
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +56 -4
- package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/failure-category.js +4 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +140 -39
- package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +79 -101
- package/dist/workflows/dag/node-execution.js +119 -8
- package/dist/workflows/dag/prompt.js +15 -1
- package/dist/workflows/dag/rerun-plan.js +22 -3
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +23 -0
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/package.json +8 -4
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const FRONTEND_TEST_CONTEXT_REL = "testcase/frontend/rag/context.md";
|
|
5
|
+
export const FRONTEND_TEST_PROBE_REL = "testcase/frontend/rag/environment-probe.json";
|
|
6
|
+
const CONNECTION_REFUSED_HINT = "被测服务未在 baseUrl 监听(connection refused)。请先启动本地前端(例如 scripts/serve.sh 或项目约定的 npm start),确认可访问后再从 materialize-frontend-test-execution-shell 续跑。";
|
|
7
|
+
export function classifyFrontendBaseUrlProbeFailure(input) {
|
|
8
|
+
if (input.spawnError)
|
|
9
|
+
return { errorClass: "spawn-error" };
|
|
10
|
+
const exit = input.curlExit ?? 0;
|
|
11
|
+
if (exit === 7) {
|
|
12
|
+
return { errorClass: "connection-refused", hint: CONNECTION_REFUSED_HINT };
|
|
13
|
+
}
|
|
14
|
+
if (exit === 6) {
|
|
15
|
+
return {
|
|
16
|
+
errorClass: "dns-unresolved",
|
|
17
|
+
hint: "无法解析 baseUrl 主机名。确认 context.md 中的地址可在本机解析,或改用 127.0.0.1。",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (exit === 28) {
|
|
21
|
+
return {
|
|
22
|
+
errorClass: "connect-timeout",
|
|
23
|
+
hint: "连接 baseUrl 超时。确认被测服务已启动且端口可访问。",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (exit !== 0)
|
|
27
|
+
return { errorClass: `curl-exit-${exit}` };
|
|
28
|
+
if (input.httpStatus && Number.isFinite(input.httpStatus)) {
|
|
29
|
+
return { errorClass: `http-${input.httpStatus}` };
|
|
30
|
+
}
|
|
31
|
+
return { errorClass: "unreachable" };
|
|
32
|
+
}
|
|
33
|
+
function defaultCurlRunner(args) {
|
|
34
|
+
const result = spawnSync("curl", [...args], { encoding: "utf8" });
|
|
35
|
+
return {
|
|
36
|
+
status: result.status,
|
|
37
|
+
stdout: result.stdout ?? "",
|
|
38
|
+
stderr: result.stderr ?? "",
|
|
39
|
+
error: result.error,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function redactUrl(value) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(value);
|
|
45
|
+
parsed.username = "";
|
|
46
|
+
parsed.password = "";
|
|
47
|
+
parsed.search = "";
|
|
48
|
+
return parsed.toString();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return value.replace(/\/\/[^@\s]+@/g, "//");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function resolveBaseUrl(context) {
|
|
55
|
+
const patterns = [
|
|
56
|
+
/baseUrl\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
57
|
+
/base[-_ ]url\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
58
|
+
/playwright-cli open --browser=chrome\s+((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
59
|
+
/(https?:\/\/(?:localhost|127\.0\.0\.1)[^\s)}\],"']*)/i,
|
|
60
|
+
];
|
|
61
|
+
for (const pattern of patterns) {
|
|
62
|
+
const match = context.match(pattern);
|
|
63
|
+
if (match?.[1])
|
|
64
|
+
return match[1].replace(/[)}\],"'.`]+$/g, "");
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
async function writeProbe(workspaceRoot, probe) {
|
|
69
|
+
const probePath = path.join(workspaceRoot, FRONTEND_TEST_PROBE_REL);
|
|
70
|
+
const contextPath = path.join(workspaceRoot, FRONTEND_TEST_CONTEXT_REL);
|
|
71
|
+
await mkdir(path.dirname(probePath), { recursive: true });
|
|
72
|
+
await writeFile(probePath, `${JSON.stringify(probe, null, 2)}\n`, "utf8");
|
|
73
|
+
let context = "";
|
|
74
|
+
try {
|
|
75
|
+
context = await readFile(contextPath, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
context = "";
|
|
79
|
+
}
|
|
80
|
+
const line = `environmentProbe: ${probe.status}${probe.blockedReason ? ` (${probe.blockedReason})` : ""}`;
|
|
81
|
+
const next = /environmentProbe\s*[:=]/i.test(context)
|
|
82
|
+
? context.replace(/environmentProbe\s*[:=]\s*.*/i, line)
|
|
83
|
+
: `${context.trimEnd()}\n\n${line}\n`;
|
|
84
|
+
await writeFile(contextPath, next, "utf8");
|
|
85
|
+
}
|
|
86
|
+
export async function probeFrontendTestEnvironment(input) {
|
|
87
|
+
const curl = input.curl ?? defaultCurlRunner;
|
|
88
|
+
const contextPath = path.join(input.workspaceRoot, FRONTEND_TEST_CONTEXT_REL);
|
|
89
|
+
let context;
|
|
90
|
+
try {
|
|
91
|
+
context = await readFile(contextPath, "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { ok: false, stderr: `missing ${FRONTEND_TEST_CONTEXT_REL}` };
|
|
95
|
+
}
|
|
96
|
+
const rawUrl = resolveBaseUrl(context);
|
|
97
|
+
if (!rawUrl) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
stderr: "frontend-test preflight missing absolute baseUrl from context.md",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = new URL(rawUrl);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { ok: false, stderr: `baseUrl must be absolute http(s): ${rawUrl}` };
|
|
109
|
+
}
|
|
110
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
111
|
+
parsed.username ||
|
|
112
|
+
parsed.password ||
|
|
113
|
+
parsed.search ||
|
|
114
|
+
parsed.hash) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
stderr: `unsafe baseUrl from context.md: ${redactUrl(rawUrl)}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (/(?:^|\.)(?:www\.)?[^.]*?(?:prod|production)/i.test(parsed.hostname)) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
stderr: `production URL forbidden: ${redactUrl(rawUrl)}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const baseUrl = parsed.toString();
|
|
127
|
+
const safe = redactUrl(baseUrl);
|
|
128
|
+
const sourceMatch = context.match(/baseUrlSource\s*[:=]\s*([^\r\n]+)/i);
|
|
129
|
+
const baseUrlSource = sourceMatch ? sourceMatch[1].trim() : "context.md";
|
|
130
|
+
const failBlocked = async (reason, extra) => {
|
|
131
|
+
const probe = {
|
|
132
|
+
status: "unreachable",
|
|
133
|
+
blockedReason: reason,
|
|
134
|
+
baseUrlRedacted: extra.baseUrlRedacted ?? safe,
|
|
135
|
+
httpStatus: extra.httpStatus ?? null,
|
|
136
|
+
method: extra.method ?? null,
|
|
137
|
+
curlExit: extra.curlExit ?? null,
|
|
138
|
+
errorClass: extra.errorClass ?? null,
|
|
139
|
+
...(extra.hint ? { hint: extra.hint } : {}),
|
|
140
|
+
...(extra.baseUrlSource ? { baseUrlSource: extra.baseUrlSource } : {}),
|
|
141
|
+
};
|
|
142
|
+
await writeProbe(input.workspaceRoot, probe);
|
|
143
|
+
const payload = {
|
|
144
|
+
blockedReason: reason,
|
|
145
|
+
baseUrl: probe.baseUrlRedacted,
|
|
146
|
+
httpStatus: probe.httpStatus,
|
|
147
|
+
errorClass: probe.errorClass,
|
|
148
|
+
...(probe.hint ? { hint: probe.hint } : {}),
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
stderr: `frontend-test preflight blocked: ${JSON.stringify(payload)}`,
|
|
153
|
+
probe,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const curlCheck = curl(["--version"]);
|
|
157
|
+
if (curlCheck.error || curlCheck.status !== 0) {
|
|
158
|
+
return failBlocked("curl-unavailable", {
|
|
159
|
+
baseUrlRedacted: safe,
|
|
160
|
+
errorClass: "curl-missing",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const probeMethod = (method) => curl([
|
|
164
|
+
"-sS",
|
|
165
|
+
"-o",
|
|
166
|
+
"/dev/null",
|
|
167
|
+
"-w",
|
|
168
|
+
"%{http_code}",
|
|
169
|
+
"--connect-timeout",
|
|
170
|
+
"3",
|
|
171
|
+
"--max-time",
|
|
172
|
+
"8",
|
|
173
|
+
"-X",
|
|
174
|
+
method,
|
|
175
|
+
"-L",
|
|
176
|
+
"--max-redirs",
|
|
177
|
+
"3",
|
|
178
|
+
"--http1.1",
|
|
179
|
+
"--proto-redir",
|
|
180
|
+
"=http,https",
|
|
181
|
+
safe,
|
|
182
|
+
]);
|
|
183
|
+
let used = "HEAD";
|
|
184
|
+
let result = probeMethod("HEAD");
|
|
185
|
+
let code = String(result.stdout || "").trim();
|
|
186
|
+
let statusNum = Number.parseInt(code, 10);
|
|
187
|
+
const headRejected = result.status !== 0 || !statusNum || statusNum === 405 || statusNum === 501;
|
|
188
|
+
if (headRejected) {
|
|
189
|
+
used = "GET";
|
|
190
|
+
result = probeMethod("GET");
|
|
191
|
+
code = String(result.stdout || "").trim();
|
|
192
|
+
statusNum = Number.parseInt(code, 10);
|
|
193
|
+
}
|
|
194
|
+
const ok = statusNum >= 200 && statusNum < 400;
|
|
195
|
+
if (!ok) {
|
|
196
|
+
const classified = classifyFrontendBaseUrlProbeFailure({
|
|
197
|
+
spawnError: Boolean(result.error),
|
|
198
|
+
curlExit: result.status,
|
|
199
|
+
httpStatus: Number.isFinite(statusNum) ? statusNum : null,
|
|
200
|
+
});
|
|
201
|
+
return failBlocked("frontend-base-url-unreachable", {
|
|
202
|
+
baseUrlRedacted: safe,
|
|
203
|
+
httpStatus: Number.isFinite(statusNum) ? statusNum : null,
|
|
204
|
+
method: used,
|
|
205
|
+
curlExit: result.status,
|
|
206
|
+
errorClass: classified.errorClass,
|
|
207
|
+
hint: classified.hint,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const probe = {
|
|
211
|
+
status: "reachable",
|
|
212
|
+
blockedReason: null,
|
|
213
|
+
baseUrl: safe,
|
|
214
|
+
baseUrlRedacted: safe,
|
|
215
|
+
baseUrlSource,
|
|
216
|
+
httpStatus: statusNum,
|
|
217
|
+
method: used,
|
|
218
|
+
curlExit: result.status,
|
|
219
|
+
errorClass: null,
|
|
220
|
+
};
|
|
221
|
+
await writeProbe(input.workspaceRoot, probe);
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
stdout: `frontend-test-execution-v1 validated contextBaseUrl=${safe} source=${baseUrlSource} probe=reachable method=${used} httpStatus=${statusNum}`,
|
|
225
|
+
probe,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded ATX section/list parsing for frontend-test case Markdown.
|
|
3
|
+
* Generator cases use `##` headings; older fixtures use `###`.
|
|
4
|
+
*/
|
|
5
|
+
const ATX_HEADING = /^(#{1,3})\s+(\S.*)$/;
|
|
6
|
+
function headingMatches(title, names) {
|
|
7
|
+
const normalized = title.trim();
|
|
8
|
+
return names.some((name) => {
|
|
9
|
+
if (normalized === name)
|
|
10
|
+
return true;
|
|
11
|
+
return (normalized.startsWith(name) &&
|
|
12
|
+
(normalized.length === name.length ||
|
|
13
|
+
/[\s::与((-]/.test(normalized.charAt(name.length))));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function markdownSection(body, names) {
|
|
17
|
+
const lines = body.replace(/\r\n/g, "\n").split("\n");
|
|
18
|
+
let start = -1;
|
|
19
|
+
let startLevel = 0;
|
|
20
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
21
|
+
const match = ATX_HEADING.exec(lines[index]);
|
|
22
|
+
if (!match)
|
|
23
|
+
continue;
|
|
24
|
+
if (!headingMatches(match[2], names))
|
|
25
|
+
continue;
|
|
26
|
+
start = index;
|
|
27
|
+
startLevel = match[1].length;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
if (start < 0)
|
|
31
|
+
return "";
|
|
32
|
+
const collected = [];
|
|
33
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
34
|
+
const match = ATX_HEADING.exec(lines[index]);
|
|
35
|
+
if (match && match[1].length <= startLevel)
|
|
36
|
+
break;
|
|
37
|
+
collected.push(lines[index]);
|
|
38
|
+
}
|
|
39
|
+
return collected.join("\n").trim();
|
|
40
|
+
}
|
|
41
|
+
export function markdownList(value) {
|
|
42
|
+
const items = [];
|
|
43
|
+
for (const raw of value.replace(/\r\n/g, "\n").split("\n")) {
|
|
44
|
+
const listed = raw.match(/^\s*(?:\d+[.)]|[-*+])\s+(.*)$/);
|
|
45
|
+
if (listed) {
|
|
46
|
+
const text = listed[1].trim();
|
|
47
|
+
if (text)
|
|
48
|
+
items.push(text);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const continuation = raw.match(/^\s{2,}(\S.*)$/);
|
|
52
|
+
if (continuation && items.length > 0) {
|
|
53
|
+
items[items.length - 1] = `${items[items.length - 1]} ${continuation[1].trim()}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return items;
|
|
57
|
+
}
|
|
58
|
+
export function markdownH1Title(body) {
|
|
59
|
+
const match = /^#\s+(\S.*)$/m.exec(body.replace(/\r\n/g, "\n"));
|
|
60
|
+
return match?.[1]?.trim() ?? "";
|
|
61
|
+
}
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { readPlaywrightCliReceipts, summarizePlaywrightCliReceipts, } from "../../executors/pi-playwright-cli-tool.js";
|
|
7
7
|
import { validateFrontendCaseContent } from "./frontend-test-case-quality.js";
|
|
8
|
+
import { markdownH1Title, markdownList, markdownSection, } from "./frontend-test-markdown.js";
|
|
8
9
|
export const FRONTEND_TEST_RESULT_SCHEMA_ID = "frontend-test-result-v1";
|
|
9
10
|
const safeRelativePathSchema = z.string().min(1).refine((value) => !path.posix.isAbsolute(value) &&
|
|
10
11
|
!path.win32.isAbsolute(value) &&
|
|
@@ -220,34 +221,25 @@ async function loadCaseBrowserReceiptSummary(input) {
|
|
|
220
221
|
function sha256(content) {
|
|
221
222
|
return createHash("sha256").update(content).digest("hex");
|
|
222
223
|
}
|
|
223
|
-
function markdownSection(body, names) {
|
|
224
|
-
const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
225
|
-
const marker = new RegExp(`^###\\s+(?:${escaped})\\s*$`, "mi");
|
|
226
|
-
const hit = marker.exec(body);
|
|
227
|
-
if (!hit)
|
|
228
|
-
return "";
|
|
229
|
-
const rest = body.slice(hit.index + hit[0].length);
|
|
230
|
-
const next = /^###\s+/m.exec(rest);
|
|
231
|
-
return rest.slice(0, next?.index ?? rest.length).trim();
|
|
232
|
-
}
|
|
233
|
-
function markdownList(value) {
|
|
234
|
-
const items = value.split(/\r?\n/).map((line) => line.replace(/^\s*(?:\d+[.)]|[-*+])\s+/, "").trim()).filter(Boolean);
|
|
235
|
-
return items;
|
|
236
|
-
}
|
|
237
224
|
/**
|
|
238
225
|
* Read planned case facts from the case Markdown. Recognizes both English and
|
|
239
|
-
* Chinese headings
|
|
240
|
-
* test steps), alongside the legacy `操作步骤`/`Steps`
|
|
226
|
+
* Chinese headings at `##` or `###`, including `测试点` (test points) and
|
|
227
|
+
* `测试步骤` (planned test steps), alongside the legacy `操作步骤`/`Steps`
|
|
228
|
+
* headings. Numbered and bulleted lists are extracted; a missing 测试目的
|
|
229
|
+
* falls back to the case H1 title.
|
|
241
230
|
*/
|
|
242
231
|
async function readFrontendCaseContent(workspaceRoot, casePath) {
|
|
243
232
|
if (!safeRelativePathSchema.safeParse(casePath).success || !casePath.startsWith("testcase/frontend/cases/")) {
|
|
244
233
|
throw new Error(`unsafe frontend case path: ${casePath}`);
|
|
245
234
|
}
|
|
246
235
|
const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
|
|
236
|
+
const purpose = markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) ||
|
|
237
|
+
markdownH1Title(body).replace(/^FE-[^\s::]+[::]\s*/, "").trim() ||
|
|
238
|
+
"未提供测试目的";
|
|
247
239
|
return {
|
|
248
240
|
caseContent: {
|
|
249
|
-
purpose
|
|
250
|
-
preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
|
|
241
|
+
purpose,
|
|
242
|
+
preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件", "前置条件与重置"])),
|
|
251
243
|
steps: markdownList(markdownSection(body, ["Test Steps", "测试步骤", "Steps", "操作步骤"])),
|
|
252
244
|
expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
|
|
253
245
|
},
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
|
|
4
|
+
export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
|
|
5
|
+
const MINIMAL_STANDARD_SCENARIOS = {
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
id: "frontend-test-standard-scenarios-v1",
|
|
8
|
+
scenarios: [
|
|
9
|
+
{
|
|
10
|
+
id: "STD-FE-SMOKE-ENTRY",
|
|
11
|
+
title: "入口可打开",
|
|
12
|
+
category: "smoke",
|
|
13
|
+
priority: "must",
|
|
14
|
+
testPoints: ["open"],
|
|
15
|
+
minCases: 1,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
function isSafeRelativeDir(value) {
|
|
20
|
+
if (!value || path.isAbsolute(value) || path.win32.isAbsolute(value)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
24
|
+
}
|
|
25
|
+
export function listFrontendTestStandardScenarioCandidates(input) {
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const add = (relative) => {
|
|
28
|
+
const normalized = relative.replaceAll("\\", "/");
|
|
29
|
+
if (!seen.has(normalized))
|
|
30
|
+
seen.add(normalized);
|
|
31
|
+
};
|
|
32
|
+
add(`docs/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
33
|
+
const governanceRoot = input.governanceRoot?.trim().replaceAll("\\", "/").replace(/\/+$/, "");
|
|
34
|
+
if (governanceRoot && isSafeRelativeDir(governanceRoot)) {
|
|
35
|
+
add(`${governanceRoot}/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
36
|
+
}
|
|
37
|
+
add(`ai_workspace/loop-agent/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
38
|
+
return [...seen];
|
|
39
|
+
}
|
|
40
|
+
async function readGovernanceRoot(workspaceRoot) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = JSON.parse(await readFile(path.join(workspaceRoot, "harness.json"), "utf8"));
|
|
43
|
+
return typeof raw.governanceRoot === "string" ? raw.governanceRoot : undefined;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function copyFrontendTestStandardScenarios(input) {
|
|
50
|
+
const destRel = FRONTEND_TEST_STANDARD_SCENARIOS_DEST;
|
|
51
|
+
const destAbs = path.join(input.workspaceRoot, destRel);
|
|
52
|
+
const candidates = listFrontendTestStandardScenarioCandidates({
|
|
53
|
+
governanceRoot: await readGovernanceRoot(input.workspaceRoot),
|
|
54
|
+
});
|
|
55
|
+
await mkdir(path.dirname(destAbs), { recursive: true });
|
|
56
|
+
for (const relative of candidates) {
|
|
57
|
+
const sourceAbs = path.join(input.workspaceRoot, relative);
|
|
58
|
+
try {
|
|
59
|
+
await copyFile(sourceAbs, destAbs);
|
|
60
|
+
return { status: "copied", from: relative, to: destRel };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// try the next candidate
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await writeFile(destAbs, `${JSON.stringify(MINIMAL_STANDARD_SCENARIOS, null, 2)}\n`, "utf8");
|
|
67
|
+
return { status: "fallback", to: destRel };
|
|
68
|
+
}
|