@tea-agent/loop-agent 0.39.0-next.31 → 0.39.0-next.32
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 +2 -0
- package/dist/build-stamp.json +2 -2
- package/dist/executors/dag-pi-executor.js +7 -4
- package/dist/executors/pi-playwright-cli-tool.js +23 -16
- package/dist/executors/shell-executor.js +25 -8
- package/dist/task/config-types.js +6 -0
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-gauhD9kD.js → abnfDiagram-N423BO3Z-B_Eawmjz.js} +1 -1
- package/dist/worker/console/static/assets/{arc-pm3c2Rsl.js → arc-CAX7-_9s.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-D_JAg3wH.js → architectureDiagram-T3A2C74G-kSHUPU8D.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Dk7-ex52.js → blockDiagram-VBNYF7ZC-7uygSzId.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Uval9oRI.js → c4Diagram-5PPSVZJV-B7VBFqY8.js} +1 -1
- package/dist/worker/console/static/assets/channel-BqOf3G6O.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BSYjHs-v.js → chunk-2GRJ4B5K-CToVTe0G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-KGUuBXI8.js → chunk-2Q5K7J3B-C3Y6NsZJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-JqAtxD9N.js → chunk-5RXB4S5H-D__FOJmB.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D6EJOVQK.js → chunk-5VM5RSS4-DnbdnPQh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-B1JriSjm.js → chunk-6Q2QTUOP-fxcnbtmR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D62b96Jd.js → chunk-GF5L2VYU-DkWP4DCN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BsNKEC_r.js → chunk-JWPE2WC7-3crbQ-b9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B56v6-ZP.js → chunk-KBJHAD2P-AW5fUBHx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-D2KPnd8K.js → chunk-RYQCIY6F-BsiQcbN7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-kzKSCaaL.js → chunk-XXDRQBXY-CuRhHBel.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BGuxW-g8.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BGuxW-g8.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C6N6KNJd.js → cose-bilkent-JH36ORCC-VNMSbhR1.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DiyymT2D.js → cynefin-VYW2F7L2-9VXNKhPA.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BeKAXvf3.js → cynefinDiagram-MW4NZA55-DnA3i2aF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-TkIdXfPF.js → dagre-VZM6K2ZE-CXf1ON53.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BFmSG3ks.js → diagram-7IWD3JNH-DkHu5QHM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqpJT2Fs.js → diagram-B4RE2ZJO-b3DVvxzf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-r5ATN43t.js → diagram-LBJQPF4R-CMtGrmDr.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DEcSCMuR.js → diagram-Q27KOJAE-CTUKQdiN.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-VOvYRLVc.js → diagram-UB23O5K3-XwXry4X0.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D7f4Cb1u.js → ebnfDiagram-BXEA7PRR-Dn6cLJ4_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-ziUf3U0G.js → erDiagram-JOGREHBK-MLibOYKC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BoRdQ-1y.js → flowDiagram-UKHOOZJN-C3ErZkth.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-B1-GncNm.js → ganttDiagram-PKOTCBZU-CN8OlZId.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CTW-ph8W.js → gitGraphDiagram-DS77QQ5N-Cq4huIY9.js} +1 -1
- package/dist/worker/console/static/assets/{index-DEbyrEfW.js → index-DSWsdkbR.js} +52 -52
- package/dist/worker/console/static/assets/index-XZm1O8NF.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BFA337tZ.js → infoDiagram-6WML65LV-BG_CYrDS.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BtEujgRR.js → ishikawaDiagram-WSZJBQD7-DCWDQKJF.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C8ZwD2Jp.js → journeyDiagram-NVQOT4AX-DG_VS4XD.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Ck-z8B11.js → kanban-definition-27J2QSJJ-DkmTDctP.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CAyXjQjN.js → linear-C1PpUkLo.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CcWKn6tS.js → mermaid.core-BXExQJbm.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-C2NuDO7E.js → mindmap-definition-FAOFIHXS-gUlCo9JV.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BFub3PDz.js → pegDiagram-VL7TDLO6-JoegdCzT.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BJx5THZ5.js → pieDiagram-7S7Q4E2Y-CrJnvJe_.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DhalkDWV.js → quadrantDiagram-CIZ2JOQS-BHZqrxkB.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CVcBM36o.js → railroadDiagram-AXF67PYL-D2uwo4iG.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D_vy4n0w.js → requirementDiagram-LRYGKXZP-Cjg0rXAB.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DGYdJL1D.js → sankeyDiagram-W5VNT64P-0VbDOPz_.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C1xJFg3k.js → sequenceDiagram-SI44F4Z6-S2UAiI-8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-D_Fc43df.js → sizeCapture-X5ZJPWSS-DQYI8pSg.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BhGbIjbu.js → stateDiagram-OKZ733FA-EIDdI0hz.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BF-RJ1_H.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DVkNcO-V.js → swimlanes-SLNWSIFB-CfukQalg.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-NswIVCtn.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQU9ufV0.js → timeline-definition-Z64GVDOM-BOQ3dOWp.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BCQhRmYB.js → vennDiagram-T6HMQDX7-ChhOpX0T.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DWerOyHO.js → wardleyDiagram-T6FBY63Y-BVYvTZDn.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DxLeebQU.js → xychartDiagram-ELKLHX3M-CPRbN4R2.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
- package/dist/workflows/dag/frontend-test-case-checklist.js +4 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +6 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +6 -3
- package/dist/workflows/dag/frontend-test-environment-probe.js +10 -7
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +28 -14
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +3 -1
- package/dist/workflows/dag/init-hybrid.js +58 -13
- package/dist/workflows/dag/types.js +12 -0
- package/dist/workflows/dag/validate.js +23 -19
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +4 -4
- package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/playwright-cli-case-generator/SKILL.md +10 -8
- package/dist/worker/console/static/assets/channel-CVrnUdZl.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +0 -1
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +0 -8
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Frontend-test artifact layout (configurable testRoot).
|
|
4
|
+
*
|
|
5
|
+
* Resolved once from `task.frontendTest.testRoot` at DAG compile time and
|
|
6
|
+
* frozen on the spec for the whole run. Writers, gates, Playwright fences
|
|
7
|
+
* and TaskSpec allowedPaths all derive from this single object.
|
|
8
|
+
*
|
|
9
|
+
* Default layout keeps the historical frozen contract:
|
|
10
|
+
* testcase/frontend/rag/**
|
|
11
|
+
* testcase/frontend/cases/**
|
|
12
|
+
* testcase/frontend/evidence/**
|
|
13
|
+
* testcase/frontend/reports/**
|
|
14
|
+
* testcase/frontend/fixtures/**
|
|
15
|
+
*/
|
|
16
|
+
export const FRONTEND_TEST_DEFAULT_TEST_ROOT = "testcase/frontend";
|
|
17
|
+
const FORBIDDEN_TEST_ROOT_PREFIXES = [
|
|
18
|
+
"src",
|
|
19
|
+
"app",
|
|
20
|
+
"lib",
|
|
21
|
+
"apps",
|
|
22
|
+
"packages",
|
|
23
|
+
".harness",
|
|
24
|
+
".git",
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
"docs",
|
|
29
|
+
"website",
|
|
30
|
+
"scripts",
|
|
31
|
+
"test",
|
|
32
|
+
"smoke",
|
|
33
|
+
"knowledge",
|
|
34
|
+
"features",
|
|
35
|
+
"dogfood",
|
|
36
|
+
"examples",
|
|
37
|
+
"bin",
|
|
38
|
+
];
|
|
39
|
+
function normalizeRelPosix(value, label) {
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
throw new Error(`frontendTest.${label} must not be empty`);
|
|
43
|
+
if (trimmed.includes("\\")) {
|
|
44
|
+
throw new Error(`frontendTest.${label} must use forward slashes: ${value}`);
|
|
45
|
+
}
|
|
46
|
+
if (path.isAbsolute(trimmed) || /^[A-Za-z]:/.test(trimmed)) {
|
|
47
|
+
throw new Error(`frontendTest.${label} must be a repository-relative path: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
const segments = trimmed.split("/").filter((segment) => segment.length > 0);
|
|
50
|
+
if (segments.some((segment) => segment === ".." || segment === ".")) {
|
|
51
|
+
throw new Error(`frontendTest.${label} must not contain "." or ".." segments: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
if (segments.some((segment) => segment.startsWith("."))) {
|
|
54
|
+
throw new Error(`frontendTest.${label} must not target hidden directories: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
if (segments.some((segment) => !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(segment))) {
|
|
57
|
+
throw new Error(`frontendTest.${label} contains unsafe path segments: ${value}`);
|
|
58
|
+
}
|
|
59
|
+
return segments.join("/");
|
|
60
|
+
}
|
|
61
|
+
function assertTestRootAllowed(testRoot) {
|
|
62
|
+
const topSegment = testRoot.split("/")[0];
|
|
63
|
+
if (FORBIDDEN_TEST_ROOT_PREFIXES.some((forbidden) => topSegment === forbidden || topSegment.startsWith(`${forbidden}.`))) {
|
|
64
|
+
throw new Error(`frontendTest.testRoot must not target production/governance trees: ${testRoot}`);
|
|
65
|
+
}
|
|
66
|
+
if (topSegment.startsWith(".")) {
|
|
67
|
+
throw new Error(`frontendTest.testRoot must not target hidden directories: ${testRoot}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the frozen frontend-test artifact layout from task config.
|
|
72
|
+
* Throws on unsafe or ambiguous configuration (fail-closed).
|
|
73
|
+
*/
|
|
74
|
+
export function resolveFrontendTestLayout(config) {
|
|
75
|
+
const testRoot = config?.testRoot === undefined || config?.testRoot === null
|
|
76
|
+
? FRONTEND_TEST_DEFAULT_TEST_ROOT
|
|
77
|
+
: normalizeRelPosix(config.testRoot, "testRoot");
|
|
78
|
+
assertTestRootAllowed(testRoot);
|
|
79
|
+
const isDefault = testRoot === FRONTEND_TEST_DEFAULT_TEST_ROOT;
|
|
80
|
+
return {
|
|
81
|
+
testRoot,
|
|
82
|
+
ragDir: `${testRoot}/rag`,
|
|
83
|
+
casesDir: `${testRoot}/cases`,
|
|
84
|
+
evidenceDir: `${testRoot}/evidence`,
|
|
85
|
+
reportsDir: `${testRoot}/reports`,
|
|
86
|
+
fixturesDir: `${testRoot}/fixtures`,
|
|
87
|
+
isDefault,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Historical default layout. Old DAGs without frontendTestLayout use this. */
|
|
91
|
+
export function defaultFrontendTestLayout() {
|
|
92
|
+
return resolveFrontendTestLayout(undefined);
|
|
93
|
+
}
|
|
94
|
+
export function frontendTestLayoutFromSpec(spec) {
|
|
95
|
+
const frozen = spec?.frontendTestLayout;
|
|
96
|
+
if (!frozen)
|
|
97
|
+
return defaultFrontendTestLayout();
|
|
98
|
+
return resolveFrontendTestLayout({ testRoot: frozen.testRoot });
|
|
99
|
+
}
|
|
100
|
+
const DEFAULT_PREFIX = `${FRONTEND_TEST_DEFAULT_TEST_ROOT}/`;
|
|
101
|
+
/**
|
|
102
|
+
* Longest-prefix rewrite of historical `testcase/frontend/` paths.
|
|
103
|
+
* No-op when layout.isDefault so default DAGs stay byte-identical.
|
|
104
|
+
* Never replaces the short token `testcase/` (would collide with backend-test).
|
|
105
|
+
*/
|
|
106
|
+
export function applyFrontendTestLayoutToText(text, layout) {
|
|
107
|
+
if (layout.isDefault)
|
|
108
|
+
return text;
|
|
109
|
+
const replacement = `${layout.testRoot}/`;
|
|
110
|
+
let output = "";
|
|
111
|
+
for (let i = 0; i < text.length;) {
|
|
112
|
+
if (text.startsWith(replacement, i)) {
|
|
113
|
+
output += replacement;
|
|
114
|
+
i += replacement.length;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (text.startsWith(DEFAULT_PREFIX, i)) {
|
|
118
|
+
output += replacement;
|
|
119
|
+
i += DEFAULT_PREFIX.length;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
output += text[i];
|
|
123
|
+
i += 1;
|
|
124
|
+
}
|
|
125
|
+
return output;
|
|
126
|
+
}
|
|
127
|
+
export function frontendTestCasePath(layout, caseId) {
|
|
128
|
+
return `${layout.casesDir}/${caseId}.md`;
|
|
129
|
+
}
|
|
130
|
+
export function frontendTestEvidenceDir(layout, caseId) {
|
|
131
|
+
return `${layout.evidenceDir}/${caseId}/`;
|
|
132
|
+
}
|
|
133
|
+
export function frontendTestEvidencePrefix(layout, caseId) {
|
|
134
|
+
return `${layout.evidenceDir}/${caseId}`;
|
|
135
|
+
}
|
|
136
|
+
export function frontendTestManifestPath(layout) {
|
|
137
|
+
return `${layout.casesDir}/manifest.json`;
|
|
138
|
+
}
|
|
139
|
+
export function frontendTestManifestDraftPath(layout) {
|
|
140
|
+
return `${layout.casesDir}/manifest.draft.json`;
|
|
141
|
+
}
|
|
142
|
+
export function frontendTestContextRel(layout) {
|
|
143
|
+
return `${layout.ragDir}/context.md`;
|
|
144
|
+
}
|
|
145
|
+
export function frontendTestCoverageMapRel(layout) {
|
|
146
|
+
return `${layout.ragDir}/coverage-map.md`;
|
|
147
|
+
}
|
|
148
|
+
export function frontendTestProbeRel(layout) {
|
|
149
|
+
return `${layout.ragDir}/environment-probe.json`;
|
|
150
|
+
}
|
|
151
|
+
export function frontendTestStandardScenariosRel(layout) {
|
|
152
|
+
return `${layout.ragDir}/standard-scenarios.v1.json`;
|
|
153
|
+
}
|
|
154
|
+
export function frontendTestUiAnchorsRel(layout) {
|
|
155
|
+
return `${layout.ragDir}/ui-anchors.md`;
|
|
156
|
+
}
|
|
157
|
+
export function escapeRegexLiteral(value) {
|
|
158
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
}
|
|
@@ -6,6 +6,7 @@ import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-s
|
|
|
6
6
|
import { readPlaywrightCliReceipts, summarizePlaywrightCliReceipts, } from "../../executors/pi-playwright-cli-tool.js";
|
|
7
7
|
import { validateFrontendCaseContent } from "./frontend-test-case-quality.js";
|
|
8
8
|
import { markdownH1Title, markdownList, markdownSection, } from "./frontend-test-markdown.js";
|
|
9
|
+
import { defaultFrontendTestLayout, frontendTestEvidencePrefix, frontendTestManifestPath, } from "./frontend-test-layout.js";
|
|
9
10
|
export const FRONTEND_TEST_RESULT_SCHEMA_ID = "frontend-test-result-v1";
|
|
10
11
|
/**
|
|
11
12
|
* Truncated acceptance-id shapes observed in generator output: a bare
|
|
@@ -124,12 +125,14 @@ function isSafeEvidenceShellPath(value) {
|
|
|
124
125
|
* backslash interpretation for the former long `node -e` command.
|
|
125
126
|
*/
|
|
126
127
|
export async function validateFrontendCaseEvidence(input) {
|
|
127
|
-
const
|
|
128
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
129
|
+
const manifestRel = frontendTestManifestPath(layout);
|
|
130
|
+
const manifestPath = path.join(input.workspaceRoot, manifestRel);
|
|
128
131
|
try {
|
|
129
132
|
await stat(manifestPath);
|
|
130
133
|
}
|
|
131
134
|
catch {
|
|
132
|
-
throw new Error(
|
|
135
|
+
throw new Error(`missing ${manifestRel}`);
|
|
133
136
|
}
|
|
134
137
|
let manifest;
|
|
135
138
|
try {
|
|
@@ -147,7 +150,7 @@ export async function validateFrontendCaseEvidence(input) {
|
|
|
147
150
|
const item = rawCase;
|
|
148
151
|
const id = typeof item?.caseId === "string" ? item.caseId : "?";
|
|
149
152
|
const dir = item?.evidenceDir;
|
|
150
|
-
const prefix =
|
|
153
|
+
const prefix = frontendTestEvidencePrefix(layout, id);
|
|
151
154
|
if (!item ||
|
|
152
155
|
typeof item.caseId !== "string" ||
|
|
153
156
|
typeof dir !== "string" ||
|
|
@@ -191,12 +194,15 @@ export async function validateFrontendCaseEvidence(input) {
|
|
|
191
194
|
continue;
|
|
192
195
|
}
|
|
193
196
|
const currentPrefix = `${prefix}/`;
|
|
194
|
-
if (evidencePath.startsWith(
|
|
197
|
+
if ((evidencePath.startsWith(`${layout.evidenceDir}/`) ||
|
|
198
|
+
evidencePath.startsWith("testcase/frontend/evidence/")) &&
|
|
199
|
+
!evidencePath.startsWith(currentPrefix)) {
|
|
195
200
|
hardFail = true;
|
|
196
201
|
issues.push({ ruleId: "cross-case-evidence-path", caseId: id, detail: evidencePath });
|
|
197
202
|
continue;
|
|
198
203
|
}
|
|
199
|
-
const target = evidencePath.startsWith(
|
|
204
|
+
const target = evidencePath.startsWith(`${layout.testRoot}/`) ||
|
|
205
|
+
evidencePath.startsWith("testcase/")
|
|
200
206
|
? path.join(input.workspaceRoot, evidencePath)
|
|
201
207
|
: path.join(input.workspaceRoot, dir, evidencePath);
|
|
202
208
|
if (!(await isNonEmptyFile(target))) {
|
|
@@ -343,8 +349,10 @@ function sha256(content) {
|
|
|
343
349
|
* headings. Numbered and bulleted lists are extracted; a missing 测试目的
|
|
344
350
|
* falls back to the case H1 title.
|
|
345
351
|
*/
|
|
346
|
-
async function readFrontendCaseContent(workspaceRoot, casePath) {
|
|
347
|
-
if (!safeRelativePathSchema.safeParse(casePath).success ||
|
|
352
|
+
async function readFrontendCaseContent(workspaceRoot, casePath, layout) {
|
|
353
|
+
if (!safeRelativePathSchema.safeParse(casePath).success ||
|
|
354
|
+
!(casePath.startsWith(`${layout.casesDir}/`) ||
|
|
355
|
+
casePath.startsWith("testcase/frontend/cases/"))) {
|
|
348
356
|
throw new Error(`unsafe frontend case path: ${casePath}`);
|
|
349
357
|
}
|
|
350
358
|
const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
|
|
@@ -397,7 +405,7 @@ function assertInside(root, candidate, label) {
|
|
|
397
405
|
throw new Error(`${label} escapes its allowed root`);
|
|
398
406
|
}
|
|
399
407
|
}
|
|
400
|
-
async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
|
|
408
|
+
async function readEvidenceFile(repoRoot, evidenceRoot, relative, layout) {
|
|
401
409
|
if (!safeRelativePathSchema.safeParse(relative).success) {
|
|
402
410
|
throw new Error(`unsafe evidence path: ${relative}`);
|
|
403
411
|
}
|
|
@@ -409,12 +417,16 @@ async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
|
|
|
409
417
|
const normalized = relative;
|
|
410
418
|
const evidenceRootRel = path.relative(repoRootResolved, evidenceRootResolved).replaceAll("\\", "/");
|
|
411
419
|
const isCurrentCaseRepoPath = normalized === evidenceRootRel || normalized.startsWith(`${evidenceRootRel}/`);
|
|
412
|
-
const isRepoRelative = normalized.startsWith(
|
|
420
|
+
const isRepoRelative = (normalized.startsWith(`${layout.testRoot}/`) ||
|
|
421
|
+
normalized.startsWith("testcase/")) &&
|
|
422
|
+
!isCurrentCaseRepoPath;
|
|
413
423
|
const absolute = isRepoRelative
|
|
414
424
|
? path.resolve(repoRootResolved, normalized)
|
|
415
425
|
: path.resolve(evidenceRootResolved, normalized);
|
|
416
426
|
assertInside(repoRootResolved, absolute, "evidence path");
|
|
417
|
-
if (isRepoRelative &&
|
|
427
|
+
if (isRepoRelative &&
|
|
428
|
+
(normalized.startsWith(`${layout.evidenceDir}/`) ||
|
|
429
|
+
normalized.startsWith("testcase/frontend/evidence/"))) {
|
|
418
430
|
throw new Error(`evidence path escapes evidence root: ${relative}`);
|
|
419
431
|
}
|
|
420
432
|
if (!isRepoRelative)
|
|
@@ -437,7 +449,8 @@ export async function materializeFrontendTestResult(input) {
|
|
|
437
449
|
if (!sourceBinding?.taskId || !sourceBinding.sources?.length) {
|
|
438
450
|
throw new Error("frontend-test result requires frozen sourceBinding");
|
|
439
451
|
}
|
|
440
|
-
const
|
|
452
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
453
|
+
const manifestPath = frontendTestManifestPath(layout);
|
|
441
454
|
const manifestAbsolute = path.resolve(input.workspaceRoot, manifestPath);
|
|
442
455
|
let manifest;
|
|
443
456
|
try {
|
|
@@ -453,6 +466,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
453
466
|
const contentQuality = await validateFrontendCaseContent({
|
|
454
467
|
workspaceRoot: input.workspaceRoot,
|
|
455
468
|
requiredAcIds: sourceBinding.requirementIds ?? [],
|
|
469
|
+
layout,
|
|
456
470
|
});
|
|
457
471
|
advisoryFindings.push(...contentQuality.findings);
|
|
458
472
|
const requiredIds = new Set(sourceBinding.requirementIds ?? []);
|
|
@@ -464,7 +478,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
464
478
|
advisoryFindings.push({ ruleId: "invalid-manifest-case", detail: "manifest case is missing caseId, acIds, or evidenceDir" });
|
|
465
479
|
continue;
|
|
466
480
|
}
|
|
467
|
-
const evidencePrefix =
|
|
481
|
+
const evidencePrefix = frontendTestEvidencePrefix(layout, item.caseId);
|
|
468
482
|
if (!(item.evidenceDir === evidencePrefix || item.evidenceDir.startsWith(`${evidencePrefix}/`))) {
|
|
469
483
|
throw new Error(`unsafe evidenceDir for ${item.caseId}`);
|
|
470
484
|
}
|
|
@@ -495,7 +509,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
495
509
|
if (typeof evidencePath !== "string" || evidencePath === "execution.md" && evidence.some((entry) => entry.path.endsWith("/execution.md")))
|
|
496
510
|
continue;
|
|
497
511
|
try {
|
|
498
|
-
evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath));
|
|
512
|
+
evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath, layout));
|
|
499
513
|
}
|
|
500
514
|
catch (error) {
|
|
501
515
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -541,7 +555,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
541
555
|
}
|
|
542
556
|
}
|
|
543
557
|
}
|
|
544
|
-
const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
|
|
558
|
+
const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath, layout);
|
|
545
559
|
// Parse bounded fixed sections from execution.md. Missing optional
|
|
546
560
|
// summary/steps are advisory only and never downgrade a passed status.
|
|
547
561
|
let executionSummary = "";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { defaultFrontendTestLayout, frontendTestStandardScenariosRel, } from "./frontend-test-layout.js";
|
|
3
4
|
export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
|
|
4
5
|
export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
|
|
5
6
|
const MINIMAL_STANDARD_SCENARIOS = {
|
|
@@ -47,7 +48,8 @@ async function readGovernanceRoot(workspaceRoot) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
export async function copyFrontendTestStandardScenarios(input) {
|
|
50
|
-
const
|
|
51
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
52
|
+
const destRel = frontendTestStandardScenariosRel(layout);
|
|
51
53
|
const destAbs = path.join(input.workspaceRoot, destRel);
|
|
52
54
|
const candidates = listFrontendTestStandardScenarioCandidates({
|
|
53
55
|
governanceRoot: await readGovernanceRoot(input.workspaceRoot),
|
|
@@ -28,6 +28,7 @@ import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
|
28
28
|
import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
|
|
29
29
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
30
30
|
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
31
|
+
import { applyFrontendTestLayoutToText, resolveFrontendTestLayout, } from "./frontend-test-layout.js";
|
|
31
32
|
import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
|
|
32
33
|
import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
|
|
33
34
|
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
@@ -4798,11 +4799,57 @@ function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
|
4798
4799
|
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4799
4800
|
spec.backendTestLayout = layout;
|
|
4800
4801
|
}
|
|
4802
|
+
/** Rewrite layout-dependent strings across a compiled frontend-test DAG spec. */
|
|
4803
|
+
function applyFrontendTestLayoutToDagSpec(spec, layout) {
|
|
4804
|
+
if (layout.isDefault) {
|
|
4805
|
+
spec.frontendTestLayout = layout;
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
const rewrite = (value) => applyFrontendTestLayoutToText(value, layout);
|
|
4809
|
+
for (const task of spec.tasks) {
|
|
4810
|
+
task.subtask_prompt = rewrite(task.subtask_prompt);
|
|
4811
|
+
if (task.outputContract)
|
|
4812
|
+
task.outputContract = rewrite(task.outputContract);
|
|
4813
|
+
task.writeSet = task.writeSet?.map(rewrite);
|
|
4814
|
+
task.allowedPaths = task.allowedPaths?.map(rewrite);
|
|
4815
|
+
task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
|
|
4816
|
+
if (task.dynamicExpansion?.childTask) {
|
|
4817
|
+
const child = task.dynamicExpansion.childTask;
|
|
4818
|
+
if (typeof child.subtaskPromptTemplate === "string") {
|
|
4819
|
+
child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
|
|
4820
|
+
}
|
|
4821
|
+
if (typeof child.outputContract === "string") {
|
|
4822
|
+
child.outputContract = rewrite(child.outputContract);
|
|
4823
|
+
}
|
|
4824
|
+
if (Array.isArray(child.allowedPaths)) {
|
|
4825
|
+
child.allowedPaths = child.allowedPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4826
|
+
}
|
|
4827
|
+
if (Array.isArray(child.writeSet)) {
|
|
4828
|
+
child.writeSet = child.writeSet.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4829
|
+
}
|
|
4830
|
+
if (Array.isArray(child.forbiddenPaths)) {
|
|
4831
|
+
child.forbiddenPaths = child.forbiddenPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
if (task.shell?.commands) {
|
|
4835
|
+
task.shell.commands = task.shell.commands.map(rewrite);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
spec.globalConstraints = spec.globalConstraints?.map(rewrite);
|
|
4839
|
+
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4840
|
+
spec.frontendTestLayout = layout;
|
|
4841
|
+
}
|
|
4842
|
+
function allowedPathsCoverFrontendTestRoot(allowedPaths, testRoot) {
|
|
4843
|
+
const probe = `${testRoot}/_layout_probe_`;
|
|
4844
|
+
return allowedPaths.some((pattern) => pathMatchesPattern(testRoot, pattern) ||
|
|
4845
|
+
pathMatchesPattern(probe, pattern));
|
|
4846
|
+
}
|
|
4801
4847
|
// ---------------------------------------------------------------------------
|
|
4802
4848
|
// Frontend browser-test RAG DAG template
|
|
4803
4849
|
// ---------------------------------------------------------------------------
|
|
4804
4850
|
function buildFrontendTestHybridDag(sources) {
|
|
4805
4851
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
4852
|
+
const layout = resolveFrontendTestLayout(rawFrontendTest);
|
|
4806
4853
|
const config = {
|
|
4807
4854
|
// Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
|
|
4808
4855
|
maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
|
|
@@ -4827,9 +4874,9 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4827
4874
|
// Optional project-local UI anchor ledger: only mention it in prompts when it
|
|
4828
4875
|
// exists so ledger-less projects keep generating without noise.
|
|
4829
4876
|
const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
|
|
4830
|
-
existsSync(path.join(sources.repoRoot, "
|
|
4877
|
+
existsSync(path.join(sources.repoRoot, layout.ragDir, "ui-anchors.md")));
|
|
4831
4878
|
const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
|
|
4832
|
-
?
|
|
4879
|
+
? `Also read ${layout.ragDir}/ui-anchors.md (UI anchor ledger). Every passing find assertion in generated cases must quote a ledger row whose 状态 is 有效 for the matching page x state section; rows marked 不可断言/失效 must not be used as passing assertions. When an AC names a control absent from the ledger section for that state, emit a blocked case note (blockedReason unique-control-unavailable) instead of exploratory find steps, and follow ledger 备注 alternatives (split-node short literals) exactly.`
|
|
4833
4880
|
: "";
|
|
4834
4881
|
const reviewMode = config.reviewMode;
|
|
4835
4882
|
const blockingReview = reviewMode === "blocking";
|
|
@@ -4837,22 +4884,19 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4837
4884
|
const maxRerunAttempts = config.maxRerunAttempts;
|
|
4838
4885
|
const enableRetrospect = config.reports?.retrospect === true;
|
|
4839
4886
|
const enableL5Report = config.reports?.l5 !== false;
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
pattern === "**");
|
|
4843
|
-
if (!hasFrontendTestWriteScope) {
|
|
4844
|
-
throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
|
|
4887
|
+
if (!allowedPathsCoverFrontendTestRoot(sources.taskConfig.allowedPaths, layout.testRoot)) {
|
|
4888
|
+
throw new Error(`frontend-test requires task.json allowedPaths to cover "${layout.testRoot}/**" (or an explicit containing glob).`);
|
|
4845
4889
|
}
|
|
4846
4890
|
const forbidden = commonForbiddenPaths(sources);
|
|
4847
|
-
const ragWriteSet = [
|
|
4891
|
+
const ragWriteSet = [`${layout.ragDir}/**`];
|
|
4848
4892
|
const caseDraftWriteSet = [
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4893
|
+
`${layout.casesDir}/FE-*.md`,
|
|
4894
|
+
`${layout.casesDir}/index.md`,
|
|
4895
|
+
`${layout.casesDir}/manifest.draft.json`,
|
|
4852
4896
|
];
|
|
4853
4897
|
// The shell materializer alone owns the final manifest boundary.
|
|
4854
|
-
const casesWriteSet = [
|
|
4855
|
-
const evidenceRoot =
|
|
4898
|
+
const casesWriteSet = [`${layout.casesDir}/**`];
|
|
4899
|
+
const evidenceRoot = layout.evidenceDir;
|
|
4856
4900
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
4857
4901
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
4858
4902
|
const checklistScript = [
|
|
@@ -5444,6 +5488,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5444
5488
|
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
5445
5489
|
tasks,
|
|
5446
5490
|
};
|
|
5491
|
+
applyFrontendTestLayoutToDagSpec(spec, layout);
|
|
5447
5492
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
5448
5493
|
parseDagSpec(spec);
|
|
5449
5494
|
assertValidDagSpec(spec);
|
|
@@ -978,6 +978,18 @@ export const dagSpecSchema = z
|
|
|
978
978
|
isDefault: z.boolean(),
|
|
979
979
|
})
|
|
980
980
|
.optional(),
|
|
981
|
+
/** Frozen frontend-test artifact layout; absent = historical testcase/frontend/ layout. */
|
|
982
|
+
frontendTestLayout: z
|
|
983
|
+
.object({
|
|
984
|
+
testRoot: z.string(),
|
|
985
|
+
ragDir: z.string(),
|
|
986
|
+
casesDir: z.string(),
|
|
987
|
+
evidenceDir: z.string(),
|
|
988
|
+
reportsDir: z.string(),
|
|
989
|
+
fixturesDir: z.string(),
|
|
990
|
+
isDefault: z.boolean(),
|
|
991
|
+
})
|
|
992
|
+
.optional(),
|
|
981
993
|
/** Bound user shared-setup document (plan B); absent = no shared setup references expected. */
|
|
982
994
|
backendTestSharedSetup: z
|
|
983
995
|
.object({
|
|
@@ -5,6 +5,7 @@ import { resolveRepairTaskForGate } from "./repair-artifact.js";
|
|
|
5
5
|
import { topoSortToRanks } from "./topo.js";
|
|
6
6
|
import { isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "./retry-policy.js";
|
|
7
7
|
import { isBackendTestCompletenessRetryCandidate } from "./backend-test-writer-completeness.js";
|
|
8
|
+
import { defaultFrontendTestLayout, frontendTestLayoutFromSpec, } from "./frontend-test-layout.js";
|
|
8
9
|
const GOVERNANCE_WARNING_TYPES = new Set([
|
|
9
10
|
"read-only-missing-artifacts-forbidden",
|
|
10
11
|
"read-only-prompt-mentions-artifact-writes",
|
|
@@ -680,22 +681,23 @@ function validateWriterOutcomePolicyTaskConfig(task, issues) {
|
|
|
680
681
|
});
|
|
681
682
|
}
|
|
682
683
|
}
|
|
683
|
-
function isFrontendEvidenceWriteSet(writeSet) {
|
|
684
|
+
function isFrontendEvidenceWriteSet(writeSet, layout = defaultFrontendTestLayout()) {
|
|
684
685
|
const entries = writeSet ?? [];
|
|
685
686
|
if (entries.length === 0)
|
|
686
687
|
return false;
|
|
688
|
+
const prefix = `${layout.evidenceDir}/`;
|
|
689
|
+
const glob = `${layout.evidenceDir}/**`;
|
|
687
690
|
return entries.every((entry) => {
|
|
688
691
|
const normalized = entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
689
|
-
return
|
|
690
|
-
normalized.startsWith("testcase/frontend/evidence/"));
|
|
692
|
+
return normalized === glob || normalized.startsWith(prefix);
|
|
691
693
|
});
|
|
692
694
|
}
|
|
693
|
-
function isFrontendBrowserExecutorTask(task) {
|
|
695
|
+
function isFrontendBrowserExecutorTask(task, layout = defaultFrontendTestLayout()) {
|
|
694
696
|
const skills = task.skills ?? [];
|
|
695
697
|
return (task.executor === "pi" &&
|
|
696
698
|
task.toolProfile === "write" &&
|
|
697
699
|
skills.includes("playwright-cli") &&
|
|
698
|
-
isFrontendEvidenceWriteSet(task.writeSet));
|
|
700
|
+
isFrontendEvidenceWriteSet(task.writeSet, layout));
|
|
699
701
|
}
|
|
700
702
|
function taskMentionsPlaywrightCliContract(task) {
|
|
701
703
|
const prompt = `${task.subtask_prompt}\n${task.outputContract ?? ""}`;
|
|
@@ -707,10 +709,11 @@ function taskMentionsPlaywrightCliContract(task) {
|
|
|
707
709
|
* - capability is granted without skill/prompt contract or outside evidence writeSet
|
|
708
710
|
* - non-browser nodes receive command capability
|
|
709
711
|
*/
|
|
710
|
-
function validateCommandPolicyTaskConfig(task, issues) {
|
|
712
|
+
function validateCommandPolicyTaskConfig(task, issues, spec) {
|
|
713
|
+
const layout = frontendTestLayoutFromSpec(spec);
|
|
711
714
|
const policy = resolveDagCommandPolicy(task.commandPolicy);
|
|
712
715
|
const allowsPlaywright = dagCommandPolicyAllows(task.commandPolicy, "playwright-cli");
|
|
713
|
-
const isBrowserExecutor = isFrontendBrowserExecutorTask(task);
|
|
716
|
+
const isBrowserExecutor = isFrontendBrowserExecutorTask(task, layout);
|
|
714
717
|
if (isBrowserExecutor && !allowsPlaywright) {
|
|
715
718
|
issues.push({
|
|
716
719
|
type: "invalid-command-policy",
|
|
@@ -741,10 +744,10 @@ function validateCommandPolicyTaskConfig(task, issues) {
|
|
|
741
744
|
message: `task ${task.id} playwright-cli command capability requires prompt/outputContract to reference playwright_cli`,
|
|
742
745
|
});
|
|
743
746
|
}
|
|
744
|
-
if (!isFrontendEvidenceWriteSet(task.writeSet)) {
|
|
747
|
+
if (!isFrontendEvidenceWriteSet(task.writeSet, layout)) {
|
|
745
748
|
issues.push({
|
|
746
749
|
type: "invalid-command-policy",
|
|
747
|
-
message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under
|
|
750
|
+
message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under ${layout.evidenceDir}/`,
|
|
748
751
|
});
|
|
749
752
|
}
|
|
750
753
|
if (task.toolProfile !== "write") {
|
|
@@ -755,7 +758,7 @@ function validateCommandPolicyTaskConfig(task, issues) {
|
|
|
755
758
|
}
|
|
756
759
|
}
|
|
757
760
|
}
|
|
758
|
-
function validateDynamicChildCommandPolicy(task, issues) {
|
|
761
|
+
function validateDynamicChildCommandPolicy(task, issues, spec) {
|
|
759
762
|
const expansion = task.dynamicExpansion;
|
|
760
763
|
if (!expansion)
|
|
761
764
|
return;
|
|
@@ -776,7 +779,7 @@ function validateDynamicChildCommandPolicy(task, issues) {
|
|
|
776
779
|
writeSet: child.writeSet,
|
|
777
780
|
outputContract: child.outputContract,
|
|
778
781
|
};
|
|
779
|
-
validateCommandPolicyTaskConfig(synthetic, issues);
|
|
782
|
+
validateCommandPolicyTaskConfig(synthetic, issues, spec);
|
|
780
783
|
}
|
|
781
784
|
function validateDecisionGateTaskConfig(task, issues) {
|
|
782
785
|
if (!task.decisionGate?.enabled) {
|
|
@@ -883,12 +886,13 @@ export function collectForbiddenExecutorIssues(spec, forbiddenExecutors) {
|
|
|
883
886
|
}));
|
|
884
887
|
}
|
|
885
888
|
/** Revalidate a rendered dynamic child before it enters state or executes. */
|
|
886
|
-
export function assertValidMaterializedDagTask(task) {
|
|
889
|
+
export function assertValidMaterializedDagTask(task, parentSpec) {
|
|
887
890
|
const issues = [];
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
891
|
+
const spec = {
|
|
892
|
+
version: 2,
|
|
893
|
+
tasks: [task],
|
|
894
|
+
frontendTestLayout: parentSpec?.frontendTestLayout,
|
|
895
|
+
};
|
|
892
896
|
validateTaskWritePolicy(task, spec, issues);
|
|
893
897
|
validateShellTaskConfig(task, spec, issues);
|
|
894
898
|
validateStaticTaskConfig(task, issues);
|
|
@@ -896,7 +900,7 @@ export function assertValidMaterializedDagTask(task) {
|
|
|
896
900
|
validateRetryPolicyTaskConfig(task, issues);
|
|
897
901
|
validateOutputProtocolTaskConfig(task, issues);
|
|
898
902
|
validateWriterOutcomePolicyTaskConfig(task, issues);
|
|
899
|
-
validateCommandPolicyTaskConfig(task, issues);
|
|
903
|
+
validateCommandPolicyTaskConfig(task, issues, spec);
|
|
900
904
|
if (issues.length > 0) {
|
|
901
905
|
throw new Error(`invalid materialized dynamic child ${task.id}: ${issues.map((issue) => issue.message).join("; ")}`);
|
|
902
906
|
}
|
|
@@ -952,8 +956,8 @@ export function validateDagSpec(spec) {
|
|
|
952
956
|
validateRetryPolicyTaskConfig(task, issues);
|
|
953
957
|
validateOutputProtocolTaskConfig(task, issues);
|
|
954
958
|
validateWriterOutcomePolicyTaskConfig(task, issues);
|
|
955
|
-
validateCommandPolicyTaskConfig(task, issues);
|
|
956
|
-
validateDynamicChildCommandPolicy(task, issues);
|
|
959
|
+
validateCommandPolicyTaskConfig(task, issues, spec);
|
|
960
|
+
validateDynamicChildCommandPolicy(task, issues, spec);
|
|
957
961
|
validateProjectGovernanceTaskConfig(task, spec, issues);
|
|
958
962
|
validatePiExtensionsTaskConfig(task, issues);
|
|
959
963
|
validateFailureAwareDependsOn(task, spec, issues);
|
|
@@ -15,7 +15,7 @@ LLM review (when `frontendTest.reviewMode=blocking`) must not invent blocking ru
|
|
|
15
15
|
| `unknown-ac` | When task `sourceBinding.requirementIds` lists ACs, every `acIds` entry must be in that set |
|
|
16
16
|
| `case-id-shape` | `caseId` matches `FE-<FEATURE>-<NNN>-...` (never `AC-FE-*`) |
|
|
17
17
|
| `case-id-is-ac` | Do not use acceptance id as `caseId` / filename |
|
|
18
|
-
| `case-path-mismatch` | `casePath === testcase/frontend/cases/{caseId}.md
|
|
18
|
+
| `case-path-mismatch` | `casePath === {casesDir}/{caseId}.md`(默认 `testcase/frontend/cases/{caseId}.md`) |
|
|
19
19
|
| `case-file-missing` | `casePath` exists |
|
|
20
20
|
|
|
21
21
|
## Tool guidance (non-blocking)
|
|
@@ -40,5 +40,5 @@ LLM review (when `frontendTest.reviewMode=blocking`) must not invent blocking ru
|
|
|
40
40
|
|
|
41
41
|
## Pipeline vs quality
|
|
42
42
|
|
|
43
|
-
- **Pipeline acceptance**: final `
|
|
43
|
+
- **Pipeline acceptance**: final `{reportsDir}/frontend-test-retrospect-*.md` exists after result materialize (default `testcase/frontend/reports/`)
|
|
44
44
|
- **Quality**: `frontend-test-result-v1.outcome=passed` with 0 blocked/failed (opt-in via `frontendTest.strictOutcomeGate`)
|
package/harness.json
CHANGED
|
@@ -82,9 +82,9 @@
|
|
|
82
82
|
"executors": {
|
|
83
83
|
"pi": {
|
|
84
84
|
"description": "Pi 负责规划、评审、诊断;当 DAG toolProfile=write 时也可做有界写入。模型按复杂度三档配置,支持 provider/model 字符串或带 thinking 的对象;斜杠前为 Pi provider,后为 modelId,勿只写裸 modelId。",
|
|
85
|
-
"LOW": "
|
|
86
|
-
"MED": "
|
|
87
|
-
"HIGH": "
|
|
85
|
+
"LOW": "deepseek/deepseek-v4-flash",
|
|
86
|
+
"MED": "deepseek/deepseek-v4-flash",
|
|
87
|
+
"HIGH": "deepseek/deepseek-v4-flash"
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fe-test-ui-scout
|
|
3
|
-
description: 在写 frontend-test Wave PRD/回归目录之前,对真实运行的被测页面做只读锚点侦察,产出并维护 testcase/frontend/rag/ui-anchors.md
|
|
3
|
+
description: 在写 frontend-test Wave PRD/回归目录之前,对真实运行的被测页面做只读锚点侦察,产出并维护 {testRoot}/rag/ui-anchors.md 锚点台账(默认 testcase/frontend/rag/ui-anchors.md)。用 playwright-cli snapshot/find 逐字面验证命中数与唯一性,防止 PRD 验收标准引用不存在或不唯一的文案。触发词:frontend-test、锚点、台账、ui-anchors、侦察、写 PRD 前、snapshot/find 校准、Wave PRD。
|
|
4
4
|
references:
|
|
5
5
|
- path: references/ledger-schema.md
|
|
6
6
|
required: true
|
|
@@ -13,7 +13,7 @@ references:
|
|
|
13
13
|
## 定位
|
|
14
14
|
|
|
15
15
|
写 Wave PRD / 校准回归目录 AC **之前**的前置侦察。产出锚点台账
|
|
16
|
-
`testcase/frontend/rag/ui-anchors.md
|
|
16
|
+
`{testRoot}/rag/ui-anchors.md`(默认 `testcase/frontend/rag/ui-anchors.md`):每个「页面 × 状态」下真实可见、
|
|
17
17
|
`find` 可命中的字面清单。PRD 的每条 `find "..."` 断言必须能在台账找到对应行;
|
|
18
18
|
台账没有的字面禁止写进验收标准。
|
|
19
19
|
|
|
@@ -23,7 +23,7 @@ references:
|
|
|
23
23
|
|
|
24
24
|
## 输入
|
|
25
25
|
|
|
26
|
-
- 被测 baseUrl(来自 `
|
|
26
|
+
- 被测 baseUrl(来自 `{testRoot}/rag/context.md` 的 `environmentProbe=reachable` URL;默认 `testcase/frontend/rag/context.md`)
|
|
27
27
|
- 目标 Wave 的 AC 清单(回归目录中本 Wave 范围的行)
|
|
28
28
|
- 既有台账(增量更新,不整表重写)
|
|
29
29
|
|
|
@@ -56,7 +56,7 @@ references:
|
|
|
56
56
|
|
|
57
57
|
## 输出
|
|
58
58
|
|
|
59
|
-
- `testcase/frontend/rag/ui-anchors.md
|
|
59
|
+
- `{testRoot}/rag/ui-anchors.md`(增量更新;默认 `testcase/frontend/rag/ui-anchors.md`)
|
|
60
60
|
- 侦察小结(对话内输出,不落盘;结论回写 PRD 草稿的锚点引用)
|
|
61
61
|
|
|
62
62
|
## References
|