@tea-agent/loop-agent 0.44.0-next.12 → 0.44.0-next.13
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 +15 -2
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init.js +1 -0
- package/dist/executors/dag-pi-executor.js +11 -6
- package/dist/executors/pi-writer-tool-policy.js +60 -4
- package/dist/executors/playwright-cli-launcher.js +31 -45
- package/dist/executors/shell-write-guard.js +24 -67
- package/dist/governance/exec-plans.js +10 -1
- package/dist/task/config-types.js +4 -2
- package/dist/task/source-prepare/build-draft.js +1 -0
- package/dist/task/source-prepare/path-policy.js +17 -7
- package/dist/worker/console/chat/browser-automation.js +343 -38
- package/dist/worker/console/chat/browser-content.js +74 -0
- package/dist/worker/console/chat/browser-diagnostics.js +250 -0
- package/dist/worker/console/chat/browser-evaluation.js +58 -0
- package/dist/worker/console/chat/browser-operation-queue.js +19 -0
- package/dist/worker/console/chat/browser-pick.js +113 -0
- package/dist/worker/console/chat/browser-routes.js +33 -5
- package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
- package/dist/worker/console/chat/browser-storage.js +89 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
- package/dist/worker/console/chat/pi-runtime.js +1 -1
- package/dist/worker/console/chat/repo-browser.js +80 -2
- package/dist/worker/console/chat/repo-preview-kind.js +155 -0
- package/dist/worker/console/chat/session-mode.js +1 -1
- package/dist/worker/console/chat/session-store.js +4 -2
- package/dist/worker/console/chat/tools.js +12 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-BA7qCL_r.js} +1 -1
- package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-Df09ajwL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CpF9sw5R.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-BtPys8ja.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DZPnTWqT.js} +1 -1
- package/dist/worker/console/static/assets/channel-DiTNLZUm.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-BD9D4h1p.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-Svn8yBmn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-B8EbQj-S.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-tENZpcpF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-DBx71Lco.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-1v_QamMz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-BAMCNNBG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BvuEury-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-YqFuHp8s.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-ouI5T7Mr.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-Bri0_Izk.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-e8Vhhi4X.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-tSIXGQiJ.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-Rl-sF5pt.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-Db6ddPz8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Jwd_Xw5S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-CeC_8InC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-LKPRaNq8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-DlVQytMW.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-DnfJZwO-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-3axVSXca.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-xe04-_L-.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-hpmRINiD.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js} +1 -1
- package/dist/worker/console/static/assets/index-C_8_gWal.css +1 -0
- package/dist/worker/console/static/assets/{index-CTMZsDmh.js → index-DybcVXCu.js} +102 -100
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-BF6ksWTX.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-DXisdeD7.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BWu65wmu.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-hfNhHqAI.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-D6HTfQcp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-Iv91R1WP.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CfPChwjR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-B7TcoqHz.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-ouyPKMtQ.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-B9tuyLbh.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-Ck4q9nJs.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-wXLX5Dnp.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-D2XWw6_m.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-DzxKiug4.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-DWUqK5Us.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-D5Pa4O5g.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-VH7cRnsJ.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-C4HM5NoZ.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-Bu-4hOlA.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-CasFEvvl.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M-D-gpwPSA.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 +57 -9
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
- package/dist/worker/console/workspace-context.js +1 -0
- package/dist/worker/materialize/harness-task-lineage.js +4 -2
- package/dist/worker/observe/static/console-theme.css +33 -29
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
- package/dist/workflows/dag/backend-test-layout.js +27 -7
- package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
- package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
- package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
- package/dist/workflows/dag/frontend-test-layout.js +41 -12
- package/dist/workflows/dag/hybrid/sources.js +21 -2
- package/dist/workflows/dag/hybrid/templates/backend-test.js +65 -50
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +58 -37
- package/dist/workflows/dag/test-artifact-namespace.js +73 -0
- package/dist/workflows/dag/validate.js +22 -3
- package/docs/templates/README.md +4 -4
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
- package/docs/templates/backend-test-dag.json +708 -708
- package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
- package/docs/templates/frontend-test-dag.json +46 -37
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
- package/package.json +7 -2
- 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 +4 -4
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
|
@@ -5,6 +5,7 @@ import { assertValidDagSpec } from "../../validate.js";
|
|
|
5
5
|
import { DEFAULT_DAG_EXECUTOR_MODELS, DEFAULT_DAG_OUTPUT_LANGUAGE, parseDagSpec } from "../../types.js";
|
|
6
6
|
import { pathMatchesPattern } from "../../../../shared/git-progress.js";
|
|
7
7
|
import { applyFrontendTestLayoutToText, resolveFrontendTestLayout } from "../../frontend-test-layout.js";
|
|
8
|
+
import { isHarnessNamespacePath } from "../../test-artifact-namespace.js";
|
|
8
9
|
import { buildFrontendTestOutcomeGateShellSnippet } from "../../frontend-test-result-contract.js";
|
|
9
10
|
import { GENERATED_DAG_RUNTIME_CONTRACT, HYBRID_DEFAULTS, STANDARD_GLOBAL_CONSTRAINTS, buildDagSourceBinding, buildSourceContextBlock, extractObjective, extractSuccessCriteria, resolveDagVerifyStrategy } from "../sources.js";
|
|
10
11
|
import { applyDefaultReadOnlyRetryPolicy, commonForbiddenPaths, commonReadOnlyPaths, stampGeneratedArtifactBindings, } from "./shared.js";
|
|
@@ -62,7 +63,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
62
63
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
63
64
|
const checklistScript = [
|
|
64
65
|
"const fs=require('fs'),path=require('path');",
|
|
65
|
-
|
|
66
|
+
`const root='${layout.casesDir}';`,
|
|
66
67
|
"const draft=path.join(root,'manifest.draft.json');",
|
|
67
68
|
"const final=path.join(root,'manifest.json');",
|
|
68
69
|
"const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
|
|
@@ -84,8 +85,8 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
84
85
|
" if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
|
|
85
86
|
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
86
87
|
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
87
|
-
" if(typeof c.caseId==='string'&&casePath!=='
|
|
88
|
-
" const body=fs.readFileSync(casePath,'utf8');",
|
|
88
|
+
" if(typeof c.caseId==='string'&&casePath!=='${layout.casesDir}/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal ${layout.casesDir}/'+c.caseId+'.md'});",
|
|
89
|
+
" const body=fs.readFileSync(casePath,'utf8').replace(/\\bplaywright_cli\\b/g,'playwright-cli');",
|
|
89
90
|
" if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome <absolute-url>; playwright-cli is strongly recommended for browser execution'});",
|
|
90
91
|
,
|
|
91
92
|
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+(https?:\\/\\/\\S+)/i);",
|
|
@@ -117,7 +118,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
117
118
|
JSON.stringify([
|
|
118
119
|
"const fs=require('fs'),path=require('path');",
|
|
119
120
|
"function fail(ruleId,detail){ const msg=JSON.stringify({ruleId:ruleId,detail:String(detail||'')}); console.error('frontend-test manifest blocked: '+msg); throw new Error('frontend-test manifest blocked: '+ruleId+(detail?(': '+detail):'')); }",
|
|
120
|
-
|
|
121
|
+
`const draft='${layout.casesDir}/manifest.draft.json',file='${layout.casesDir}/manifest.json',tmp=file+'.tmp';`,
|
|
121
122
|
"if(!fs.existsSync(draft)) fail('draft-missing','missing '+draft);",
|
|
122
123
|
"let manifest; try{manifest=JSON.parse(fs.readFileSync(draft,'utf8'));}catch(e){fail('draft-invalid-json',e&&e.message||e);}",
|
|
123
124
|
"if(manifest.schemaVersion!==1) fail('draft-schema','schemaVersion must be 1');",
|
|
@@ -138,8 +139,8 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
138
139
|
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
|
|
139
140
|
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) fail('ac-mapping','invalid acIds for '+c.caseId);",
|
|
140
141
|
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))) fail('ac-id-truncated','acIds entry is a truncated acceptance id (missing feature prefix or sequence number): '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
` c.casePath='${layout.casesDir}/'+c.caseId+'.md';`,
|
|
143
|
+
` c.evidenceDir='${layout.evidenceDir}/'+c.caseId+'/';`,
|
|
143
144
|
" for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) fail('unsafe-path',k+': '+String(v)); }",
|
|
144
145
|
" if(!fs.existsSync(c.casePath)) fail('case-file-missing','missing case file '+c.casePath+' (filename must equal caseId.md)');",
|
|
145
146
|
" if(seenCasePath.has(c.casePath)) fail('duplicate-case-path',c.casePath); seenCasePath.add(c.casePath);",
|
|
@@ -158,7 +159,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
158
159
|
"const runDir=process.env.HARNESS_DAG_RUN_DIR; if(!runDir)throw new Error('missing HARNESS_DAG_RUN_DIR');",
|
|
159
160
|
"const resultPath=path.join(runDir,'contracts','frontend-test-result.json'); if(!fs.existsSync(resultPath))throw new Error('missing '+resultPath);",
|
|
160
161
|
"const r=JSON.parse(fs.readFileSync(resultPath,'utf8')); const findings=Array.isArray(r.advisoryFindings)?r.advisoryFindings:[];",
|
|
161
|
-
|
|
162
|
+
`const reports='${layout.reportsDir}'; fs.mkdirSync(reports,{recursive:true});`,
|
|
162
163
|
"const esc=v=>String(v??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\\\"','"');",
|
|
163
164
|
"const labels={cases:'用例总数',passed:'通过',failed:'失败',blocked:'阻塞'}; const totals=r.totals||{}; const missing=r.acceptanceCoverage&&Array.isArray(r.acceptanceCoverage.missing)?r.acceptanceCoverage.missing:[];",
|
|
164
165
|
"const md=['# 前端测试质量建议报告','','> Advisory:以下 finding 用于改进测试资产和证据质量,不阻塞后续流程。','','## 执行摘要','',...Object.entries(labels).map(([k,v])=>'- '+v+': '+Number(totals[k]||0)),'- 执行结果: '+String(r.outcome||'unknown'),'- 缺失 AC: '+(missing.join(', ')||'无'),'','## 建议项','',...(findings.length?findings.map(f=>'- ['+f.ruleId+']'+(f.caseId?' '+f.caseId:'')+': '+f.detail):['- 未发现建议项。']),''];",
|
|
@@ -197,7 +198,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
197
198
|
writeSet: ragWriteSet,
|
|
198
199
|
allowedPaths: [...ragWriteSet],
|
|
199
200
|
forbiddenPaths: forbidden,
|
|
200
|
-
outputContract:
|
|
201
|
+
outputContract: `Write ${layout.ragDir}/standard-scenarios.v1.json for generate-time standard scenario coverage. Copy docs/templates or harness.json governanceRoot templates (including ai_workspace/loop-agent/templates) when present; otherwise write the minimal STD-FE-SMOKE-ENTRY fallback.`,
|
|
201
202
|
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package from docs/templates, governanceRoot/templates, or the init-projected ai_workspace/loop-agent/templates path.",
|
|
202
203
|
shell: {
|
|
203
204
|
commands: [],
|
|
@@ -218,10 +219,10 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
218
219
|
writeSet: ragWriteSet,
|
|
219
220
|
allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
|
|
220
221
|
forbiddenPaths: forbidden,
|
|
221
|
-
outputContract:
|
|
222
|
+
outputContract: `Write short ${layout.ragDir}/context.md and coverage-map.md with a model-resolved non-production baseUrl/baseUrlSource from task source config, environmentProbe=pending, and capability notes.`,
|
|
222
223
|
subtask_prompt: [
|
|
223
224
|
"Build the frontend test RAG package (keep it short).",
|
|
224
|
-
|
|
225
|
+
`Read task source, routes/components/API or Mock facts, and execution contract. Write only ${layout.ragDir}/context.md and coverage-map.md.`,
|
|
225
226
|
"Prefer fixed fields: baseUrl, baseUrlSource, environmentProbe, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
|
|
226
227
|
"Base URL resolution (required): read the bound task source and config/environment references. Prefer TARGET_URL/targetUrl as the concrete tested entry, then BASE_URL/baseUrl or FRONTEND_BASE_URL/frontendUrl, then LOGIN_URL/loginUrl; accept common casing and underscore/kebab/space variants. Choose one absolute non-production http(s) URL, never API-only or production hosts. If no usable URL exists, use http://localhost:5173. Write `baseUrl: <url>`, `baseUrlSource: <bound source path>|default-localhost-5173`, and `environmentProbe: pending`. Include exact start prefix: playwright-cli open --browser=chrome <resolved-base-url> with the concrete selected URL.",
|
|
227
228
|
buildSourceContextBlock(sources),
|
|
@@ -238,7 +239,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
238
239
|
allowedPaths: [...ragWriteSet],
|
|
239
240
|
forbiddenPaths: forbidden,
|
|
240
241
|
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable with errorClass (connection-refused / dns-unresolved / connect-timeout / http-N / curl-exit-N). Node ERROR so generate/map do not run. Does not start the app.",
|
|
241
|
-
subtask_prompt:
|
|
242
|
+
subtask_prompt: `Parse the resolved absolute baseUrl from ${layout.ragDir}/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. Connection refused (curl 7) records errorClass=connection-refused and tells the operator to start the local app (scripts/serve.sh or npm start) then rerun from this node. 4xx/5xx/DNS/timeout/TLS => blockedReason frontend-base-url-unreachable with a distinct errorClass. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.`,
|
|
242
243
|
shell: {
|
|
243
244
|
commands: [],
|
|
244
245
|
frontendTestEnvironmentProbe: {},
|
|
@@ -261,22 +262,22 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
261
262
|
outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1 only; the exclusive shell materializer promotes the validated draft to manifest.json. Do not write manifest.json or test source code.",
|
|
262
263
|
subtask_prompt: [
|
|
263
264
|
"Use skill playwright-cli-case-generator.",
|
|
264
|
-
|
|
265
|
+
`Read ${layout.ragDir}/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.`,
|
|
265
266
|
...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
|
|
266
|
-
|
|
267
|
+
`Read only ${layout.ragDir}/context.md, ${layout.ragDir}/coverage-map.md, and the draft case paths ${layout.casesDir}/FE-*.md, ${layout.casesDir}/index.md, and ${layout.casesDir}/manifest.draft.json. Write only those same draft paths. Do not write ${layout.casesDir}/manifest.json.`,
|
|
267
268
|
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
268
269
|
"HARD ID CONTRACT (do not confuse these):",
|
|
269
270
|
"- caseId / filename MUST be FE-<FEATURE>-<NNN>-<dimension> (example FE-LOGIN-001-core). NEVER use AC-FE-* as caseId or filename.",
|
|
270
271
|
"- acIds MUST list acceptance criteria only: AC-FE-* / AC-* from the declared task list (example AC-FE-001). NEVER put FE-* case ids into acIds.",
|
|
271
|
-
|
|
272
|
+
`- casePath MUST equal ${layout.casesDir}/<caseId>.md; evidenceDir MUST equal ${layout.evidenceDir}/<caseId>/. Materialize will rewrite paths, but files must already use caseId filenames.`,
|
|
272
273
|
`Declared acceptance ids for this task (use only these in acIds when non-empty): ${declaredAcIds.length > 0 ? declaredAcIds.join(", ") : "(none extracted - still use AC-* shape, never FE-* case ids)"}.`,
|
|
273
274
|
"dimensions: core|boundary|flow|backend only.",
|
|
274
275
|
"Prefer a small smoke suite (default max roughly 4-8 cases unless task frontendTest.maxCasesPerBatch is higher). Never invent unavailable API fields or credentials. Do not create pytest or Playwright source.",
|
|
275
276
|
"HARD playwright-cli-only: every browser step must use repo skill playwright-cli declared commands only. Forbidden: bare `playwright`, `npx playwright`, `playwright test`, `@playwright/test`, Node Playwright API, or generating Playwright/Pytest source. No fallback when playwright-cli is unavailable - case must instruct blocked evidence playwright-cli-unavailable.",
|
|
276
|
-
|
|
277
|
+
`Copy the resolved absolute baseUrl from ${layout.ragDir}/context.md after the environment probe has marked it reachable. Every browser start command must be exactly: playwright-cli open --browser=chrome <resolved-base-url-from-context.md> with that concrete URL. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.`,
|
|
277
278
|
"HARD dynamic refs: executable playwright-cli lines must never contain an angle-bracket token such as <fresh-ref> or descriptive <...> placeholder. Use only shell-safe documentation placeholders `eX`, `eY`, ...; each means the real `eNN` ref parsed from the immediately preceding latest `snapshot`. Write a fresh snapshot before every element reference. eX/eY are never literal structured-tool arguments; a later snapshot invalidates prior refs, so never reuse stale refs.",
|
|
278
279
|
"HARD file-output argv: use canonical `--filename` only. Screenshot uses `playwright-cli screenshot --filename final.png` (a real ref may precede the flag); PDF uses `playwright-cli pdf --filename final.pdf`; snapshot without filename is response-only and a snapshot file uses `playwright-cli snapshot --filename snapshot.txt`. Never generate `playwright-cli screenshot <path>`, use `--path`, `--output`, or `--file`, or pass an output path as a positional target.",
|
|
279
|
-
|
|
280
|
+
`Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under ${layout.evidenceDir}/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.`,
|
|
280
281
|
"U/D data ownership (required for modify/delete): (1) Only mutate data whose ownership is proven by current login identity + observable UI/API owner fields - never by name/guessed id/list order alone. (2) If current user has no data, create tagged cleanable data in current-user context, then U/D, then cleanup+verify. (3) Else only task-authorized Mock, labeled as Mock (not real backend proof). (4) If ownership unverifiable and create/Mock unavailable: write blocked with blockedReason current-user-data-unavailable | data-ownership-unverifiable | safe-test-data-setup-unavailable - do not risk cross-user data. (5) Never touch other users, shared fixtures, production, or non-cleanable data.",
|
|
281
282
|
].join("\n\n"),
|
|
282
283
|
},
|
|
@@ -307,7 +308,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
307
308
|
allowedPaths: [...ragWriteSet, ...caseDraftWriteSet],
|
|
308
309
|
forbiddenPaths: forbidden,
|
|
309
310
|
outputContract: "Apply the one permitted frontend case revision to FE-*.md, index.md, and manifest.draft.json only; the exclusive shell materializer remains the sole writer of manifest.json. No browser execution or evidence writes.",
|
|
310
|
-
subtask_prompt:
|
|
311
|
+
subtask_prompt: `This is the only permitted case revision. Read the first review findings and the RAG package. Revise only ${layout.casesDir}/FE-*.md, ${layout.casesDir}/index.md, and ${layout.casesDir}/manifest.draft.json; preserve traceable AC mappings. Preserve the dynamic-ref contract: executable playwright-cli lines use only eX/eY-style shell-safe documentation placeholders, never <...>; each placeholder is resolved from the immediately preceding latest snapshot and stale refs are not reused. Do not write ${layout.casesDir}/manifest.json, execute a browser, or write evidence. HARD: Never delete case files; only edit in place or add missing cases. Preserve the full planned suite, index.md, and manifest.draft.json.`,
|
|
311
312
|
}, {
|
|
312
313
|
id: "review-frontend-cases-final-pi",
|
|
313
314
|
depends_on: ["revise-frontend-cases-pi"],
|
|
@@ -411,9 +412,9 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
411
412
|
writePolicy: "exclusive",
|
|
412
413
|
writeGuardPolicy: "tools-only",
|
|
413
414
|
allowedPaths: [
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
`${layout.casesDir}/{{case.caseId}}.md`,
|
|
416
|
+
`${layout.ragDir}/context.md`,
|
|
417
|
+
`${layout.ragDir}/coverage-map.md`,
|
|
417
418
|
`${evidenceRoot}/{{case.caseId}}/**`,
|
|
418
419
|
],
|
|
419
420
|
forbiddenPaths: forbidden,
|
|
@@ -422,7 +423,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
422
423
|
subtaskPromptTemplate: [
|
|
423
424
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
424
425
|
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
425
|
-
|
|
426
|
+
`1) Read the concrete baseUrl from ${layout.ragDir}/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal \`eX\`/\`eY\`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.`,
|
|
426
427
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
427
428
|
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
428
429
|
].join("\n\n"),
|
|
@@ -442,11 +443,11 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
442
443
|
: `rerun-candidates-round${round}.json`;
|
|
443
444
|
const selectCommand = [
|
|
444
445
|
"const fs=require('fs'),path=require('path');",
|
|
445
|
-
|
|
446
|
+
`const manifestPath='${layout.casesDir}/manifest.json';`,
|
|
446
447
|
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
447
448
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
448
|
-
|
|
449
|
-
`fs.mkdirSync('
|
|
449
|
+
`for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('${layout.evidenceDir}/'+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==='failed')reason='failed-retry';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'||reason==='failed-retry'||missing)&&attempt<` + String(round) + `;if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('${layout.casesDir}/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}`,
|
|
450
|
+
`fs.mkdirSync('${layout.evidenceDir}',{recursive:true});fs.writeFileSync('${layout.evidenceDir}/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
450
451
|
].join("");
|
|
451
452
|
tasks.push({
|
|
452
453
|
id: selectorId,
|
|
@@ -455,8 +456,8 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
455
456
|
executor: "shell",
|
|
456
457
|
complexity: "LOW",
|
|
457
458
|
writePolicy: "exclusive",
|
|
458
|
-
writeSet: [
|
|
459
|
-
allowedPaths: [...casesWriteSet,
|
|
459
|
+
writeSet: [`${evidenceRoot}/**`],
|
|
460
|
+
allowedPaths: [...casesWriteSet, `${evidenceRoot}/**`],
|
|
460
461
|
forbiddenPaths: forbidden,
|
|
461
462
|
outputContract: `Stdout JSON {cases} for blocked or missing-result-file cases eligible for rerun round ${round}/${maxRerunAttempts}.`,
|
|
462
463
|
subtask_prompt: "Select frontend-test cases eligible for this bounded rerun round.",
|
|
@@ -501,9 +502,9 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
501
502
|
writePolicy: "exclusive",
|
|
502
503
|
writeGuardPolicy: "tools-only",
|
|
503
504
|
allowedPaths: [
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
505
|
+
`${layout.casesDir}/{{case.caseId}}.md`,
|
|
506
|
+
`${layout.ragDir}/context.md`,
|
|
507
|
+
`${layout.ragDir}/coverage-map.md`,
|
|
507
508
|
`${evidenceRoot}/{{case.caseId}}/**`,
|
|
508
509
|
],
|
|
509
510
|
forbiddenPaths: forbidden,
|
|
@@ -512,7 +513,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
512
513
|
subtaskPromptTemplate: [
|
|
513
514
|
"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.",
|
|
514
515
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
515
|
-
|
|
516
|
+
`Read the concrete baseUrl from ${layout.ragDir}/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.`,
|
|
516
517
|
"Unique-control early stop: if the first post-navigation snapshot lacks the control an AC names (and no setup-required gate appeared), one find returning 0 matches is sufficient evidence - write status=blocked blockedReason unique-control-unavailable instead of exploratory retries.",
|
|
517
518
|
"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.",
|
|
518
519
|
].join("\n\n"),
|
|
@@ -531,7 +532,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
531
532
|
complexity: "LOW",
|
|
532
533
|
writePolicy: "exclusive",
|
|
533
534
|
writeSet: [`${evidenceRoot}/**`],
|
|
534
|
-
allowedPaths: [
|
|
535
|
+
allowedPaths: [`${layout.casesDir}/**`, `${evidenceRoot}/**`],
|
|
535
536
|
forbiddenPaths: forbidden,
|
|
536
537
|
outputContract: "Evidence validation (advisory missing/malformed does not fail the node; hard-fail only path escape) then hash-bound frontend-test-result-v1. Node success means result-v1 was written, not that all cases passed.",
|
|
537
538
|
subtask_prompt: "Validate case evidence then materialize authoritative frontend-test-result-v1. Missing/malformed case evidence is advisory; only unsafe evidence paths hard-fail. Do not use Pi prose as input.",
|
|
@@ -549,8 +550,8 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
549
550
|
executor: "shell",
|
|
550
551
|
complexity: "LOW",
|
|
551
552
|
writePolicy: "exclusive",
|
|
552
|
-
writeSet: [
|
|
553
|
-
allowedPaths: [
|
|
553
|
+
writeSet: [`${layout.reportsDir}/**`],
|
|
554
|
+
allowedPaths: [`${layout.testRoot}/**`],
|
|
554
555
|
forbiddenPaths: forbidden,
|
|
555
556
|
outputContract: "Deterministic L-5 (optional) plus main frontend-test-report.md/html from frontend-test-result-v1. Pipeline acceptance = result-v1 + main HTML.",
|
|
556
557
|
subtask_prompt: "Render operational reports from frontend-test-result-v1 only. Do not invent coverage. Main HTML is required; L-5 follows frontendTest.reports.l5.",
|
|
@@ -596,10 +597,10 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
596
597
|
complexity: "MED",
|
|
597
598
|
writePolicy: "exclusive",
|
|
598
599
|
writeGuardPolicy: "tools-only",
|
|
599
|
-
writeSet: [
|
|
600
|
-
allowedPaths: [
|
|
600
|
+
writeSet: [`${layout.reportsDir}/**`],
|
|
601
|
+
allowedPaths: [`${layout.testRoot}/**`],
|
|
601
602
|
forbiddenPaths: forbidden,
|
|
602
|
-
outputContract:
|
|
603
|
+
outputContract: `Optional retrospective under ${layout.reportsDir}/frontend-test-retrospect-<date>.md. Not required for pipeline acceptance.`,
|
|
603
604
|
subtask_prompt: "Write optional frontend-test retrospective after result + reports. Do not recompute L-5. Pipeline success does not require this file.",
|
|
604
605
|
});
|
|
605
606
|
}
|
|
@@ -617,7 +618,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
617
618
|
"Environment preflight must parse the retrieve-authored context.md baseUrl, reject unsafe/non-production violations, and curl-probe it before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
|
|
618
619
|
"U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
|
|
619
620
|
"Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
|
|
620
|
-
|
|
621
|
+
`Pipeline acceptance for frontend-test is frontend-test-result-v1 plus the main report under ${layout.reportsDir}/frontend-test-report.html; case pass rate and outcome=passed are quality signals; retrospect is opt-in (frontendTest.reports.retrospect).`,
|
|
621
622
|
blockingReview
|
|
622
623
|
? "frontendTest.reviewMode=blocking: a frontend case review must emit VERDICT: pass before checklist/manifest materialization; request-revision blocks browser execution."
|
|
623
624
|
: "frontendTest.reviewMode is off|advisory by default: mechanical checklist-shell gates materialize/execute; LLM review is not a hard browser gate.",
|
|
@@ -650,12 +651,32 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
650
651
|
tasks,
|
|
651
652
|
};
|
|
652
653
|
applyFrontendTestLayoutToDagSpec(spec, layout);
|
|
654
|
+
applyFrontendTestHarnessRootGuard(spec, layout);
|
|
653
655
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
654
656
|
stampGeneratedArtifactBindings(spec);
|
|
655
657
|
parseDagSpec(spec);
|
|
656
658
|
assertValidDagSpec(spec);
|
|
657
659
|
return spec;
|
|
658
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* D4: when the frozen frontend-test root lives under `.harness/` the whole tree
|
|
663
|
+
* is Git-ignored, so Git-status write guards would see empty diffs for real
|
|
664
|
+
* writes. Flip every file-writing node (and the dynamic browser child) from
|
|
665
|
+
* tools-only to the bounded filesystem snapshot channel instead.
|
|
666
|
+
*/
|
|
667
|
+
export function applyFrontendTestHarnessRootGuard(spec, layout) {
|
|
668
|
+
if (!isHarnessNamespacePath(layout.testRoot))
|
|
669
|
+
return;
|
|
670
|
+
for (const task of spec.tasks) {
|
|
671
|
+
if (task.writeGuardPolicy === "tools-only") {
|
|
672
|
+
task.writeGuardPolicy = "filesystem-only";
|
|
673
|
+
}
|
|
674
|
+
const child = task.dynamicExpansion?.childTask;
|
|
675
|
+
if (child?.writeGuardPolicy === "tools-only") {
|
|
676
|
+
child.writeGuardPolicy = "filesystem-only";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
659
680
|
export function applyFrontendTestLayoutToDagSpec(spec, layout) {
|
|
660
681
|
if (layout.isDefault) {
|
|
661
682
|
spec.frontendTestLayout = layout;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test-artifact namespace policy (D3).
|
|
3
|
+
*
|
|
4
|
+
* Both dedicated test workflows (backend-test, frontend-test) default to a
|
|
5
|
+
* canonical root under `.harness/testcase/`. That root is the ONLY `.harness`
|
|
6
|
+
* subtree those workflows may write; every other `.harness/**` path stays
|
|
7
|
+
* fail-closed for controller-owned facts.
|
|
8
|
+
*
|
|
9
|
+
* This module is the single source of truth shared by:
|
|
10
|
+
* - frontend/backend layout resolvers (canonical default roots)
|
|
11
|
+
* - task source-prepare path assessment (precise ancestor-gate exception)
|
|
12
|
+
* - DAG validators / writeSet gates / writer tool policies / shell guards
|
|
13
|
+
*
|
|
14
|
+
* It must stay dependency-free (pure functions + constants only) so both
|
|
15
|
+
* `src/task` and `src/workflows` can import it without cycles.
|
|
16
|
+
*/
|
|
17
|
+
/** Canonical backend-test artifact root (repo-relative, posix). */
|
|
18
|
+
export const HARNESS_TESTCASE_ROOT = ".harness/testcase";
|
|
19
|
+
/** Canonical frontend-test artifact root (repo-relative, posix). */
|
|
20
|
+
export const HARNESS_TESTCASE_FRONTEND_ROOT = `${HARNESS_TESTCASE_ROOT}/frontend`;
|
|
21
|
+
/** taskKinds whose dedicated DAGs own the test-artifact namespace. */
|
|
22
|
+
export const TEST_ARTIFACT_TASK_KINDS = [
|
|
23
|
+
"backend-test",
|
|
24
|
+
"frontend-test",
|
|
25
|
+
];
|
|
26
|
+
export function isTestArtifactTaskKind(value) {
|
|
27
|
+
return (typeof value === "string" &&
|
|
28
|
+
TEST_ARTIFACT_TASK_KINDS.includes(value));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Canonical test-artifact root for a dedicated test taskKind.
|
|
32
|
+
* backend-test -> `.harness/testcase`; frontend-test -> `.harness/testcase/frontend`.
|
|
33
|
+
*/
|
|
34
|
+
export function canonicalTestArtifactRoot(kind) {
|
|
35
|
+
return kind === "frontend-test"
|
|
36
|
+
? HARNESS_TESTCASE_FRONTEND_ROOT
|
|
37
|
+
: HARNESS_TESTCASE_ROOT;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True when a repo-relative posix path is covered by the test-artifact
|
|
41
|
+
* allowance for the given kind (exact canonical root or beneath it).
|
|
42
|
+
*/
|
|
43
|
+
export function isTestArtifactPath(kind, repoRelativePosix) {
|
|
44
|
+
const root = canonicalTestArtifactRoot(kind);
|
|
45
|
+
return (repoRelativePosix === root ||
|
|
46
|
+
repoRelativePosix.startsWith(`${root}/`));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* True when a repo-relative posix path is covered by EITHER dedicated test
|
|
50
|
+
* namespace (both kinds). Used by guards that authorize the exception but do
|
|
51
|
+
* not know which workflow owns the path.
|
|
52
|
+
*/
|
|
53
|
+
export function isAnyTestArtifactPath(repoRelativePosix) {
|
|
54
|
+
return (isTestArtifactPath("backend-test", repoRelativePosix) ||
|
|
55
|
+
isTestArtifactPath("frontend-test", repoRelativePosix));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* True when a repo-relative posix path sits anywhere beneath `.harness/`
|
|
59
|
+
* (controller/runtime namespace) — regardless of test-artifact allowance.
|
|
60
|
+
*/
|
|
61
|
+
export function isHarnessNamespacePath(repoRelativePosix) {
|
|
62
|
+
return (repoRelativePosix === ".harness" ||
|
|
63
|
+
repoRelativePosix.startsWith(".harness/"));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* True only for the baseline harness guard patterns (`.harness`, `.harness/**`).
|
|
67
|
+
* Deeper `.harness/<sub>/...` forbidden patterns are treated as explicit
|
|
68
|
+
* operator choices and stay authoritative over the test-artifact exception.
|
|
69
|
+
*/
|
|
70
|
+
export function isHarnessBaselineForbiddenPattern(pattern) {
|
|
71
|
+
const normalized = pattern.trim().replace(/\\/g, "/");
|
|
72
|
+
return normalized === ".harness" || normalized === ".harness/**";
|
|
73
|
+
}
|
|
@@ -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, OUTPUT_LIMIT_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, CONTEXT_OVERFLOW_RETRY_CATEGORY, } from "./retry-policy.js";
|
|
7
7
|
import { isBackendTestCompletenessRetryCandidate } from "./backend-test-writer-completeness.js";
|
|
8
|
+
import { isAnyTestArtifactPath, isHarnessBaselineForbiddenPattern, } from "./test-artifact-namespace.js";
|
|
8
9
|
import { defaultFrontendTestLayout, frontendTestLayoutFromSpec, } from "./frontend-test-layout.js";
|
|
9
10
|
const GOVERNANCE_WARNING_TYPES = new Set([
|
|
10
11
|
"read-only-missing-artifacts-forbidden",
|
|
@@ -58,9 +59,27 @@ function writeSetEntryWithinAllowed(entry, allowedPaths) {
|
|
|
58
59
|
const probes = probePathsForWriteSetEntry(entry);
|
|
59
60
|
return probes.some((probe) => allowedPaths.some((allowed) => pathMatchesPattern(probe, allowed)));
|
|
60
61
|
}
|
|
61
|
-
function writeSetEntryHitsForbidden(entry, forbiddenPaths) {
|
|
62
|
+
function writeSetEntryHitsForbidden(entry, forbiddenPaths, spec) {
|
|
62
63
|
const probes = probePathsForWriteSetEntry(entry);
|
|
63
|
-
|
|
64
|
+
// D3 test-artifact exception: entries covered by a frozen dedicated-test
|
|
65
|
+
// layout root under `.harness/testcase/` are the only `.harness` writes a
|
|
66
|
+
// test DAG may declare. The exception is exact-namespace (canonical roots
|
|
67
|
+
// only), so `.harness/foo` or `.harness/dag-runs/x` stays forbidden.
|
|
68
|
+
const frozenTestRoots = [
|
|
69
|
+
spec?.backendTestLayout?.testRoot,
|
|
70
|
+
spec?.frontendTestLayout?.testRoot,
|
|
71
|
+
].filter((root) => typeof root === "string" && isAnyTestArtifactPath(root));
|
|
72
|
+
return probes.some((probe) => forbiddenPaths.some((forbidden) => {
|
|
73
|
+
if (frozenTestRoots.some((root) => probe === root || probe.startsWith(`${root}/`)) &&
|
|
74
|
+
pathMatchesPattern(probe, forbidden)) {
|
|
75
|
+
// probe is inside a canonical frozen test root: the BASELINE
|
|
76
|
+
// harness guard patterns (`.harness` / `.harness/**`) are
|
|
77
|
+
// exempted; any explicit deeper or custom matching pattern
|
|
78
|
+
// still wins.
|
|
79
|
+
return !isHarnessBaselineForbiddenPattern(forbidden);
|
|
80
|
+
}
|
|
81
|
+
return pathMatchesPattern(probe, forbidden);
|
|
82
|
+
}));
|
|
64
83
|
}
|
|
65
84
|
function forbiddenPathsExcludeRootArtifacts(forbiddenPaths) {
|
|
66
85
|
return ROOT_ARTIFACT_PATH_PROBES.some((probe) => forbiddenPaths.some((forbidden) => pathMatchesPattern(probe, forbidden)));
|
|
@@ -381,7 +400,7 @@ function validateTaskWritePolicy(task, spec, issues) {
|
|
|
381
400
|
continue;
|
|
382
401
|
}
|
|
383
402
|
if (forbiddenPaths.length > 0 &&
|
|
384
|
-
writeSetEntryHitsForbidden(entry, forbiddenPaths)) {
|
|
403
|
+
writeSetEntryHitsForbidden(entry, forbiddenPaths, spec)) {
|
|
385
404
|
issues.push({
|
|
386
405
|
type: "write-set-forbidden",
|
|
387
406
|
message: `task ${task.id} writeSet entry "${entry}" matches forbiddenPaths`,
|
package/docs/templates/README.md
CHANGED
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
- `frontend-test-dag.json` — frontend-test DAG 模板;其 deterministic checklist 会拒绝 alternative executable instructions,仅允许 allowlisted `playwright-cli` command。Pi generator/reviser 的 runtime writeSet 仅授权下列目标项目生成路径(不是本仓库文档索引目标):
|
|
43
43
|
|
|
44
44
|
~~~text
|
|
45
|
-
testcase/frontend/cases/FE-*.md
|
|
46
|
-
testcase/frontend/cases/index.md
|
|
47
|
-
testcase/frontend/cases/manifest.draft.json
|
|
45
|
+
.harness/testcase/frontend/cases/FE-*.md
|
|
46
|
+
.harness/testcase/frontend/cases/index.md
|
|
47
|
+
.harness/testcase/frontend/cases/manifest.draft.json
|
|
48
48
|
~~~
|
|
49
49
|
|
|
50
50
|
Pi writer writeSet 不包含 final manifest(文件名 manifest.json)。依赖 checklist 的 exclusive shell materializer 是 final manifest 的唯一写入者,以 temp+rename 原子生成 final,后续 map 只消费该 materializer 输出。
|
|
51
51
|
- `frontend-test-dag.retrieve-context.prompt.md`、`frontend-test-dag.generate-cases.prompt.md`、`frontend-test-dag.review-cases.prompt.md`、`frontend-test-dag.review-execution.prompt.md`、`frontend-test-dag.retrospect.prompt.md` — 上下文、用例、审查、执行审查和复盘提示。
|
|
52
52
|
- `frontend-test-case-checklist.md` — 前端测试用例检查清单。
|
|
53
|
-
- `frontend-test-standard-scenarios.v1.json` — frontend-test 标准场景规范(generate 覆盖 must 场景;运行时可物化到目标项目 testcase/frontend/rag 下的 standard-scenarios.v1.json)。
|
|
53
|
+
- `frontend-test-standard-scenarios.v1.json` — frontend-test 标准场景规范(generate 覆盖 must 场景;运行时可物化到目标项目 .harness/testcase/frontend/rag 下的 standard-scenarios.v1.json)。
|
|
54
54
|
|
|
55
55
|
## 知识同步与配置
|
|
56
56
|
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
## 当前合同
|
|
8
8
|
|
|
9
|
-
- 输入:最终
|
|
9
|
+
- 输入:最终 `.harness/testcase/md/**`、run-owned 环境报告与 Markdown 校验报告,以及有界的 pytest config / `conftest.py`。
|
|
10
10
|
- 禁止重新读取 `source/**`、新增测试场景、重新分配 AC、执行 pytest、修改生产代码/配置或输出业务 JSON。
|
|
11
|
-
-
|
|
11
|
+
- 写入范围:`.harness/testcase/**/test_*.py`、`.harness/testcase/**/helpers/**`、`.harness/testcase/**/factories/**`。
|
|
12
12
|
- pytest 可以使用模块级 `test_*` 函数,也可以使用 pytest 测试类中的 `test_*` 方法;不得为了追溯强迫目标项目放弃现有 class-based 风格。
|
|
13
13
|
- 每个最终 Markdown Case ID 必须出现在至少一个真实测试函数/方法区域中,优先同时出现在函数/方法名和 docstring:
|
|
14
14
|
|
|
@@ -26,7 +26,7 @@ class TestOrderApi:
|
|
|
26
26
|
- 禁止 `skip` / `xfail`、吞断言、宽异常静默通过、mock 替代真实目标、删除用例或弱化断言。
|
|
27
27
|
- best-effort 清理只能捕获所选 HTTP client 实际抛出的窄 transport exception,例如 `requests.RequestException` 或 `urllib.error.URLError`;禁止 `except:`、`except Exception`、`except BaseException` 后 `pass`。
|
|
28
28
|
- 同一 Case ID 可以由多个 pytest 函数覆盖;额外映射会进入 traceability 报告,但不能伪造未在 Markdown 中定义的业务场景。
|
|
29
|
-
- 生成 pytest 文件名必须与 Markdown
|
|
29
|
+
- 生成 pytest 文件名必须与 Markdown 模块一一对应:`.harness/testcase/md/<module>.md` → `.harness/testcase/test_<module>.py`(`<module>` 为文件名去 `.md` 后小写、非字母数字转 `_`)。即使 Markdown `自动化映射` 写了别的路径,也必须写模块 stem 路径,不得额外发明 `test_be_*` 前缀。
|
|
30
30
|
- 测试失败必须诚实保留,后续节点只执行一次 pytest,并从同一 pytest-html 报告生成 HTML/facts。
|
|
31
31
|
|
|
32
32
|
## 推荐输出
|