@tea-agent/loop-agent 0.16.17 → 0.16.18

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 CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ### 改进
6
6
 
7
+ - 前端测试 DAG 收紧体验:execution preflight 仅硬校验绝对非生产 `baseUrl`;用例 map 缩短为优先用 `playwright-cli` 执行;复盘合并执行证据审查且不依赖 outcome=pass,失败也能出报告。
8
+ - 前端测试用例生成现在会解析明确 `baseUrl`:优先读取任务源 `config.md` 中的前端 URL,缺失时默认 `http://localhost:5173`,并写入 RAG `context.md`;生成命令不得再保留 `<base-url>` 占位符。
7
9
  - 前端浏览器测试 DAG 的复盘报告现写入 `testcase/frontend/reports/**`,不再要求 `docs/test-reports/**` 权限;任务执行约束可以安全禁止整个 `docs/**`,同时仍保留可审计的测试资产。
8
10
  - `frontend-test` 结果链新增 run-owned `frontend-test-result-v1`、单次用例修订与终审门禁;只有所有浏览器用例和 AC 覆盖通过且结果合同明确为 real/pass 时,Worker 才会投影真实集成。
9
11
 
@@ -22,6 +24,12 @@
22
24
  - Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
23
25
  - 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为 initial/final Result、repair eligibility、traceability 与 Observe 投影保留结构化运行证据。
24
26
 
27
+ ## [0.16.18] - 2026-07-20
28
+
29
+ ### 修复
30
+
31
+ - frontend-test 执行预检改为硬校验绝对非生产 `baseUrl`(优先 config.md,缺省 `http://localhost:5173`);fixture/reset 不再作为 preflight 硬门,避免中文隔离叙述因缺少英文 `reset` 被误拦。
32
+
25
33
  ## [0.16.17] - 2026-07-20
26
34
 
27
35
  ### 修复
@@ -562,10 +562,7 @@ export async function materializeBackendTestExecutionContract(input) {
562
562
  catch (error) {
563
563
  throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
564
564
  }
565
- const candidates = [
566
- sanitizeBackendTestExecutionInput(parsed),
567
- coerceBackendTestExecutionInput(parsed),
568
- ];
565
+ const candidates = [coerceBackendTestExecutionInput(parsed)];
569
566
  let accepted = null;
570
567
  let lastSchemaError = "invalid execution contract";
571
568
  let lastSecretError = "";
@@ -13,6 +13,7 @@ const caseStatusSchema = z.enum(["passed", "failed", "blocked"]);
13
13
  export const frontendTestResultContractSchema = z.object({
14
14
  schemaVersion: z.literal(1),
15
15
  sourceBinding: z.object({
16
+ schemaVersion: z.literal(1).optional(),
16
17
  taskId: z.string().min(1),
17
18
  sources: z.array(z.object({
18
19
  kind: z.enum(["requirement", "constraint", "reference"]),
@@ -74,14 +75,28 @@ async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
74
75
  if (!safeRelativePathSchema.safeParse(relative).success) {
75
76
  throw new Error(`unsafe evidence path: ${relative}`);
76
77
  }
77
- const absolute = path.resolve(evidenceRoot, relative);
78
+ // Accept either evidence-root-relative paths or repo-relative paths under the case evidence root.
79
+ const normalized = relative.replaceAll("\\", "/");
80
+ const evidenceRootRel = path.relative(repoRoot, evidenceRoot).replaceAll("\\", "/");
81
+ let candidate = normalized;
82
+ if (normalized === evidenceRootRel || normalized.startsWith(`${evidenceRootRel}/`)) {
83
+ candidate = path.relative(evidenceRoot, path.resolve(repoRoot, normalized)).replaceAll("\\", "/");
84
+ }
85
+ else if (normalized.startsWith("testcase/frontend/evidence/")) {
86
+ candidate = path.relative(evidenceRoot, path.resolve(repoRoot, normalized)).replaceAll("\\", "/");
87
+ }
88
+ if (!candidate || candidate.startsWith("..")) {
89
+ throw new Error(`evidence path escapes evidence root: ${relative}`);
90
+ }
91
+ const absolute = path.resolve(evidenceRoot, candidate);
78
92
  assertInside(evidenceRoot, absolute, "evidence path");
79
93
  const resolved = await realpath(absolute);
80
94
  assertInside(repoRoot, resolved, "evidence realpath");
81
95
  const info = await lstat(resolved);
82
96
  if (!info.isFile())
83
97
  throw new Error(`evidence is not a file: ${relative}`);
84
- return { path: relative, sha256: sha256(await readFile(resolved)) };
98
+ const storedPath = path.relative(repoRoot, resolved).replaceAll("\\", "/");
99
+ return { path: storedPath, sha256: sha256(await readFile(resolved)) };
85
100
  }
86
101
  export async function materializeFrontendTestResult(input) {
87
102
  if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(input.artifactName) || !/^[a-z0-9][a-z0-9._-]*$/.test(input.outputDir)) {
@@ -105,7 +120,8 @@ export async function materializeFrontendTestResult(input) {
105
120
  if (!item || typeof item.caseId !== "string" || !Array.isArray(item.acIds) || typeof item.evidenceDir !== "string") {
106
121
  throw new Error("invalid frontend manifest case");
107
122
  }
108
- if (!item.evidenceDir.startsWith(`testcase/frontend/evidence/${item.caseId}/`)) {
123
+ const evidencePrefix = `testcase/frontend/evidence/${item.caseId}`;
124
+ if (!(item.evidenceDir === evidencePrefix || item.evidenceDir.startsWith(`${evidencePrefix}/`))) {
109
125
  throw new Error(`unsafe evidenceDir for ${item.caseId}`);
110
126
  }
111
127
  for (const acId of item.acIds) {
@@ -127,7 +143,7 @@ export async function materializeFrontendTestResult(input) {
127
143
  continue;
128
144
  evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath));
129
145
  }
130
- if (status === "passed" && !evidence.some((entry) => /\.(png|jpg|jpeg|webp|zip|har|webm|mp4|ya?ml)$/i.test(entry.path))) {
146
+ if (status === "passed" && !evidence.some((entry) => /\.(png|jpg|jpeg|webp|zip|har|webm|mp4|ya?ml|md)$/i.test(entry.path))) {
131
147
  throw new Error(`passed case requires browser evidence: ${item.caseId}`);
132
148
  }
133
149
  cases.push({ caseId: item.caseId, acIds: item.acIds, status, evidence, ...(status === "blocked" ? { blockedReason: resultRaw.blockedReason } : {}) });
@@ -3672,8 +3672,8 @@ function buildFrontendTestHybridDag(sources) {
3672
3672
  "node -e",
3673
3673
  JSON.stringify([
3674
3674
  "const fs=require('fs'),path=require('path');",
3675
- "const file='testcase/frontend/cases/manifest.json'; if(!fs.existsSync(file)) throw new Error('missing '+file);",
3676
- "const manifest=JSON.parse(fs.readFileSync(file,'utf8')); if(manifest.schemaVersion!==1||!Array.isArray(manifest.cases)||manifest.cases.length===0) throw new Error('invalid or empty frontend case manifest');",
3675
+ "const draft='testcase/frontend/cases/manifest.draft.json',file='testcase/frontend/cases/manifest.json'; if(!fs.existsSync(draft)) throw new Error('missing '+draft);",
3676
+ "const manifest=JSON.parse(fs.readFileSync(draft,'utf8')); if(manifest.schemaVersion!==1||!Array.isArray(manifest.cases)||manifest.cases.length===0) throw new Error('invalid or empty frontend case manifest');",
3677
3677
  "const dims=new Set(['core','boundary','flow','backend']);",
3678
3678
  "const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
3679
3679
  "for(const c of manifest.cases){",
@@ -3689,6 +3689,7 @@ function buildFrontendTestHybridDag(sources) {
3689
3689
  " if(seenCasePath.has(c.casePath)) throw new Error('duplicate casePath'); seenCasePath.add(c.casePath);",
3690
3690
  " if(seenEvidenceDir.has(c.evidenceDir)) throw new Error('duplicate evidenceDir'); seenEvidenceDir.add(c.evidenceDir);",
3691
3691
  "}",
3692
+ "fs.writeFileSync(file,JSON.stringify(manifest,null,2)+'\\n'); fs.unlinkSync(draft);",
3692
3693
  "process.stdout.write(JSON.stringify({cases:manifest.cases}));",
3693
3694
  ].join("")),
3694
3695
  ].join(" ");
@@ -3699,7 +3700,7 @@ function buildFrontendTestHybridDag(sources) {
3699
3700
  "const manifestPath='testcase/frontend/cases/manifest.json';if(!fs.existsSync(manifestPath))throw new Error('missing '+manifestPath);",
3700
3701
  "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));if(!Array.isArray(manifest.cases))throw new Error('invalid frontend case manifest');",
3701
3702
  "const statuses=new Set(['passed','failed','blocked']);let failed=false;",
3702
- "for(const c of manifest.cases){const dir=c&&c.evidenceDir;if(!c||typeof c.caseId!=='string'||typeof dir!=='string'||path.isAbsolute(dir)||dir.includes('..')||!dir.startsWith('testcase/frontend/evidence/'+c.caseId+'/')){console.error('invalid case evidence target');failed=true;continue;}const execution=path.join(dir,'execution.md'),resultPath=path.join(dir,'case-result.json');if(!fs.existsSync(execution)){console.error(c.caseId+': missing '+execution);failed=true;}if(!fs.existsSync(resultPath)){console.error(c.caseId+': missing '+resultPath);failed=true;continue;}let result;try{result=JSON.parse(fs.readFileSync(resultPath,'utf8'));}catch{console.error(c.caseId+': invalid JSON '+resultPath);failed=true;continue;}if(!result||result.caseId!==c.caseId||!statuses.has(result.status)||!Array.isArray(result.evidencePaths)||result.evidencePaths.some(p=>typeof p!=='string'||path.isAbsolute(p)||p.includes('..'))){console.error(c.caseId+': result must have matching caseId, passed|failed|blocked status, and safe evidencePaths array');failed=true;continue;}if(result.status==='passed'&&(result.evidencePaths.length<2||!result.evidencePaths.some(p=>/\\.(png|jpg|jpeg|webp|zip|har|webm|mp4)$/i.test(p)))){console.error(c.caseId+': passed result requires browser evidence');failed=true;}if(result.status==='blocked'&&(typeof result.blockedReason!=='string'||!result.blockedReason.trim())){console.error(c.caseId+': blocked result requires blockedReason');failed=true;}}",
3703
+ "for(const c of manifest.cases){const dir=c&&c.evidenceDir;if(!c||typeof c.caseId!=='string'||typeof dir!=='string'||path.isAbsolute(dir)||dir.includes('..')||!(dir==='testcase/frontend/evidence/'+c.caseId||dir.startsWith('testcase/frontend/evidence/'+c.caseId+'/'))){console.error('invalid case evidence target');failed=true;continue;}const execution=path.join(dir,'execution.md'),resultPath=path.join(dir,'case-result.json');if(!fs.existsSync(execution)){console.error(c.caseId+': missing '+execution);failed=true;}if(!fs.existsSync(resultPath)){console.error(c.caseId+': missing '+resultPath);failed=true;continue;}let result;try{result=JSON.parse(fs.readFileSync(resultPath,'utf8'));}catch{console.error(c.caseId+': invalid JSON '+resultPath);failed=true;continue;}if(!result||result.caseId!==c.caseId||!statuses.has(result.status)||!Array.isArray(result.evidencePaths)||result.evidencePaths.some(p=>typeof p!=='string'||path.isAbsolute(p)||p.includes('..'))){console.error(c.caseId+': result must have matching caseId, passed|failed|blocked status, and safe evidencePaths array');failed=true;continue;}if(result.status==='passed'&&(result.evidencePaths.length<1||!result.evidencePaths.some(p=>/\\.(png|jpg|jpeg|webp|zip|har|webm|mp4|md)$/i.test(p)))){console.error(c.caseId+': passed result requires browser evidence');failed=true;}if(result.status==='blocked'&&(typeof result.blockedReason!=='string'||!result.blockedReason.trim())){console.error(c.caseId+': blocked result requires blockedReason');failed=true;}}",
3703
3704
  "if(failed)process.exit(1);console.log('frontend case evidence validation ok cases='+manifest.cases.length);",
3704
3705
  ].join("")),
3705
3706
  ].join(" ");
@@ -3717,26 +3718,24 @@ function buildFrontendTestHybridDag(sources) {
3717
3718
  "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
3718
3719
  "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
3719
3720
  "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
3720
- "Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <base-url>; generated operations stay in the default browser session and must not use unverified named-session flags.",
3721
+ "Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <resolved-base-url>; resolve baseUrl from task source config.md when present, otherwise default http://localhost:5173; generated operations stay in the default browser session and must not use unverified named-session flags.",
3721
3722
  "A frontend case review must emit VERDICT: pass before manifest materialization; request-revision blocks browser execution.",
3722
3723
  "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
3723
3724
  ],
3724
3725
  defaults: {
3725
3726
  ...HYBRID_DEFAULTS,
3727
+ skills: [],
3726
3728
  writePolicy: "read-only",
3727
- contextProfile: sources.taskConfig.contextProfile,
3729
+ // slim: large skill bodies + full source context cause Windows spawn ENAMETOOLONG
3730
+ contextProfile: "slim",
3728
3731
  },
3729
3732
  skillsByRole: {
3730
- planner: ["loop-agent"],
3733
+ planner: [],
3731
3734
  scout: ["playwright-cli"],
3732
- implementer: [
3733
- "playwright-cli-case-generator",
3734
- "playwright-cli",
3735
- "webapp-testing",
3736
- ],
3737
- reviewer: ["requesting-code-review"],
3738
- verifier: ["playwright-cli", "webapp-testing"],
3739
- closeout: ["loop-agent", "verification-before-completion"],
3735
+ implementer: ["playwright-cli"],
3736
+ reviewer: [],
3737
+ verifier: ["playwright-cli"],
3738
+ closeout: ["verification-before-completion"],
3740
3739
  },
3741
3740
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
3742
3741
  tasks: [
@@ -3756,6 +3755,7 @@ function buildFrontendTestHybridDag(sources) {
3756
3755
  "Build the frontend test RAG package.",
3757
3756
  "Read task source, relevant routes/components/API or Mock facts, existing tests, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
3758
3757
  "Record AC IDs, source paths, routes, states, roles, fixture/data prerequisites, API mapping status, risks, and isolated execution contract. Do not guess unavailable facts.",
3758
+ "Base URL resolution (required): (1) Prefer an absolute http(s) frontend URL from task source config.md (source/references/**/config.md or any attached config.md), including keys baseUrl/base_url/frontendBaseUrl/FRONTEND_BASE_URL/url or labeled frontend base URL text. (2) If config.md has no usable absolute URL, default to http://localhost:5173. (3) Never use production hosts. (4) Write both a human-readable base URL line and machine-readable lines `baseUrl: <url>` and `baseUrlSource: config.md|<path>` or `baseUrlSource: default-localhost-5173`. (5) Include the exact browser start prefix with the resolved URL: playwright-cli open --browser=chrome --headed <resolved-base-url>.",
3759
3759
  buildSourceContextBlock(sources),
3760
3760
  ].join("\n\n"),
3761
3761
  },
@@ -3768,9 +3768,9 @@ function buildFrontendTestHybridDag(sources) {
3768
3768
  writePolicy: "read-only",
3769
3769
  allowedPaths: [...ragWriteSet],
3770
3770
  forbiddenPaths: forbidden,
3771
- outputContract: "Fail-closed preflight for an isolated non-production frontend test execution contract.",
3772
- subtask_prompt: "Validate that RAG context declares a non-production base URL/env name, fixture/reset isolation, browser startup, and no credential values.",
3773
- shell: { commands: [["node -e", JSON.stringify("const fs=require('fs');const p='testcase/frontend/rag/context.md';if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const required=[['base URL',/base[- ]url/i],['fixture',/fixture/i],['reset',/reset|重置/i],['playwright-cli open --browser=chrome --headed',/playwright-cli open --browser=chrome --headed/i]];for(const [label,pattern] of required)if(!pattern.test(s))throw new Error('frontend-test execution contract missing '+label);if(/https?:\\/\\/(?:www\\.)?[^\\s]*(?:prod|production)/i.test(s))throw new Error('production URL forbidden');console.log('frontend-test-execution-v1 validated')")].join(" ")], cwd: ".", timeoutMs: 60000 },
3771
+ outputContract: "Fail-closed preflight: absolute non-production baseUrl required; fixture/reset not hard-gated.",
3772
+ subtask_prompt: "Hard-validate only an absolute non-production baseUrl in RAG context (from config.md or default http://localhost:5173). Fixture/reset and other isolation details are soft guidance for later nodes, not preflight failures.",
3773
+ shell: { commands: [["node -e", JSON.stringify("const fs=require('fs'); const p='testcase/frontend/rag/context.md'; if(!fs.existsSync(p))throw new Error('missing '+p); const s=fs.readFileSync(p,'utf8'); const patterns=[ /baseUrl\\s*[:=]\\s*(https?:\\/\\/\\S+)/i, /base[- ]url\\s*[:=]\\s*(https?:\\/\\/\\S+)/i, /playwright-cli open --browser=chrome --headed\\s+(https?:\\/\\/\\S+)/i, /(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\]},\"']*)/i ]; let baseUrl=null; for(const re of patterns){const m=s.match(re); if(m){baseUrl=m[1]; break;}} if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl (prefer config.md; default http://localhost:5173)'); baseUrl=baseUrl.replace(/[)\\]},.\"']+$/,''); if(!/^https?:\\/\\//i.test(baseUrl))throw new Error('baseUrl must be absolute http(s): '+baseUrl); if(/(?:^|\\/\\/)(?:www\\.)?[^\\s/]*(?:prod|production)/i.test(baseUrl))throw new Error('production URL forbidden: '+baseUrl); console.log('frontend-test-execution-v1 validated baseUrl='+baseUrl);")].join(" ")], cwd: ".", timeoutMs: 60000 },
3774
3774
  },
3775
3775
  {
3776
3776
  id: "generate-frontend-functional-cases-pi",
@@ -3783,12 +3783,12 @@ function buildFrontendTestHybridDag(sources) {
3783
3783
  writeSet: casesWriteSet,
3784
3784
  allowedPaths: [...ragWriteSet, ...casesWriteSet],
3785
3785
  forbiddenPaths: forbidden,
3786
- outputContract: "Write executable Markdown frontend cases, index.md, and manifest.json schemaVersion 1; no test source code.",
3786
+ outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1; no test source code.",
3787
3787
  subtask_prompt: [
3788
3788
  "Use skill playwright-cli-case-generator.",
3789
3789
  "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.",
3790
- "Generate Markdown cases, index.md and manifest.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir). IDs use FE-<FEATURE>-<NNN>-<dimension>; dimensions core|boundary|flow|backend.",
3791
- "Never infer API fields, constraints, SLA, credentials, or unrecorded test data. Do not create pytest or Playwright source. Every browser start command is: playwright-cli open --browser=chrome --headed <base-url>. Use the same default browser session for every subsequent command; never write -s=<case-id> or assume named-session binding.",
3790
+ "Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir). IDs use FE-<FEATURE>-<NNN>-<dimension>; dimensions core|boundary|flow|backend.",
3791
+ "Never infer API fields, constraints, SLA, credentials, or unrecorded test data. Do not create pytest or Playwright source. Copy the resolved absolute baseUrl from context.md (baseUrl field; resolved from config.md or default http://localhost:5173). Every browser start command must be: playwright-cli open --browser=chrome --headed <resolved-base-url-from-context.md> with that concrete URL — never leave a <base-url> placeholder. Use the same default browser session for every subsequent command; never write -s=<case-id> or assume named-session binding.",
3792
3792
  "Each case must be independently reproducible: for every executable sub-scenario state fixture/reset, UI reset, a fresh snapshot before references are used, exact evidence write point, preconditions/data cleanup, UI assertions, and evidence paths under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
3793
3793
  ].join("\n\n"),
3794
3794
  },
@@ -3802,7 +3802,7 @@ function buildFrontendTestHybridDag(sources) {
3802
3802
  allowedPaths: [...ragWriteSet, ...casesWriteSet],
3803
3803
  forbiddenPaths: forbidden,
3804
3804
  outputContract: "First line VERDICT: pass or VERDICT: request-revision, followed by AC-to-case coverage and execution risk findings; no writes. request-revision blocks manifest materialization.",
3805
- subtask_prompt: "Review only the RAG package and frontend Markdown cases. Verify traceability, independent execution, safe data/environment handling, manifest correctness, session consistency, fixture/UI reset and fresh snapshot steps, and evidence requirements. Any Important or Critical finding requires VERDICT: request-revision. Browser execution is blocked unless this review passes.",
3805
+ subtask_prompt: "Review only the RAG package, frontend Markdown cases, and manifest.draft.json. Verify traceability, independent execution, safe data/environment handling, manifest correctness, session consistency, fixture/UI reset and fresh snapshot steps, and evidence requirements. Any Important or Critical finding requires VERDICT: request-revision. Browser execution is blocked unless this review passes.",
3806
3806
  },
3807
3807
  {
3808
3808
  id: "revise-frontend-cases-pi",
@@ -3817,7 +3817,7 @@ function buildFrontendTestHybridDag(sources) {
3817
3817
  allowedPaths: [...ragWriteSet, ...casesWriteSet],
3818
3818
  forbiddenPaths: forbidden,
3819
3819
  outputContract: "Apply the one permitted frontend case revision under testcase/frontend/cases/** only; no browser execution or evidence writes.",
3820
- subtask_prompt: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases/**, preserve traceable AC mappings, and do not execute a browser or write evidence.",
3820
+ subtask_prompt: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases/**, preserve traceable AC mappings, and do not 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.",
3821
3821
  },
3822
3822
  {
3823
3823
  id: "review-frontend-cases-final-pi",
@@ -3863,11 +3863,12 @@ function buildFrontendTestHybridDag(sources) {
3863
3863
  role: "verifier",
3864
3864
  executor: "shell",
3865
3865
  complexity: "LOW",
3866
- writePolicy: "read-only",
3866
+ writePolicy: "exclusive",
3867
+ writeSet: casesWriteSet,
3867
3868
  allowedPaths: casesWriteSet,
3868
3869
  forbiddenPaths: forbidden,
3869
- outputContract: "Validated frontend manifest payload { cases: [...] }; shell command echo is permitted only as the prefix before exactly one final JSON line.",
3870
- subtask_prompt: "Validate and materialize the generated frontend case manifest.",
3870
+ outputContract: "Validated frontend manifest payload { cases: [...] }; after the review gate, atomically materialize testcase/frontend/cases/manifest.json from manifest.draft.json; shell output may echo only the prefix before exactly one final JSON line.",
3871
+ subtask_prompt: "Validate manifest.draft.json and materialize manifest.json only after the effective frontend case review has passed.",
3871
3872
  shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
3872
3873
  },
3873
3874
  {
@@ -3897,8 +3898,8 @@ function buildFrontendTestHybridDag(sources) {
3897
3898
  },
3898
3899
  childTask: {
3899
3900
  executor: "pi",
3900
- role: "verifier",
3901
- skills: ["playwright-cli", "webapp-testing"],
3901
+ role: "implementer",
3902
+ skills: ["playwright-cli"],
3902
3903
  toolProfile: "write",
3903
3904
  complexity: "MED",
3904
3905
  writePolicy: "exclusive",
@@ -3912,13 +3913,10 @@ function buildFrontendTestHybridDag(sources) {
3912
3913
  writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
3913
3914
  outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.",
3914
3915
  subtaskPromptTemplate: [
3915
- "Execute exactly case {{case.caseId}} from {{case.casePath}} using playwright-cli and webapp-testing. This is a fresh Pi session; do not use /new.",
3916
- "Use only the declared isolated test environment. If CLI/browser/base URL/credentials/fixture isolation is missing, record blocked rather than installing tools or guessing.",
3917
- "Use exactly this browser start command prefix: playwright-cli open --browser=chrome --headed <base-url>. Do not put session flags before open. Every later playwright-cli command must use that same default browser session; must not use -s=<case-id>, -s=, or any named-session flag because no session binding is verified.",
3918
- "For every sub-scenario, record fixture/reset, UI reset, and a fresh snapshot before using element references. If the isolated environment is missing, write blocked evidence before any browser command; do not open or connect to a browser.",
3919
- "Before returning, always persist {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json. case-result.json must be JSON with matching caseId, status as passed, failed, or blocked, and evidencePaths array; a blocked result must include non-empty blockedReason and must never imply pass.",
3920
- "Run a local deterministic validation before returning: node -e \"const fs=require('fs');const p='{{case.evidenceDir}}';const r=JSON.parse(fs.readFileSync(p+'case-result.json','utf8'));if(!fs.existsSync(p+'execution.md')||r.caseId!=='{{case.caseId}}'||!['passed','failed','blocked'].includes(r.status)||!Array.isArray(r.evidencePaths)||(r.status==='blocked'&&!(typeof r.blockedReason==='string'&&r.blockedReason.trim())))process.exit(1)\". Persist screenshots/trace/video/logs when actually available. execution.md must record the executed or blocked steps, base URL safety decision, fixture/reset and request-observation availability, and evidence file list.",
3921
- "A business failed or blocked case is a recorded result, not a node failure. Close the session and return only compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
3916
+ "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). Prefer playwright-cli over prose review.",
3917
+ "1) Read baseUrl from testcase/frontend/rag/context.md (config.md preferred, else http://localhost:5173). 2) Start browser: playwright-cli open --browser=chrome --headed <resolved-base-url> (default session only; no -s=). 3) Follow the case steps with snapshot before element refs. 4) If env/CLI/baseUrl is unavailable, write blocked evidence and do not open a browser.",
3918
+ "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json (caseId, status passed|failed|blocked, evidencePaths; blocked needs blockedReason). Then validate: node -e \"const fs=require('fs');const p='{{case.evidenceDir}}';const r=JSON.parse(fs.readFileSync(p+'case-result.json','utf8'));if(!fs.existsSync(p+'execution.md')||r.caseId!=='{{case.caseId}}'||!['passed','failed','blocked'].includes(r.status)||!Array.isArray(r.evidencePaths)||(r.status==='blocked'&&!(typeof r.blockedReason==='string'&&r.blockedReason.trim())))process.exit(1)\".",
3919
+ "Business failed/blocked is a recorded result, not a node failure. Close browser. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
3922
3920
  ].join("\n\n"),
3923
3921
  },
3924
3922
  },
@@ -3968,25 +3966,13 @@ function buildFrontendTestHybridDag(sources) {
3968
3966
  writePolicy: "read-only",
3969
3967
  allowedPaths: [],
3970
3968
  forbiddenPaths: forbidden,
3971
- outputContract: "Pass only when the run-owned frontend-test-result-v1 records outcome=passed and integrationMode=real.",
3972
- subtask_prompt: "Gate the authoritative frontend-test result before Pi review and retrospective.",
3969
+ outputContract: "Pass only when the run-owned frontend-test-result-v1 records outcome=passed and integrationMode=real. Does not gate retrospective closeout.",
3970
+ subtask_prompt: "Delivery/Worker gate for authoritative frontend-test result. Retrospective does not depend on this node so failed runs can still write reports.",
3973
3971
  shell: { commands: [frontendTestOutcomeGate], cwd: ".", timeoutMs: 60000 },
3974
3972
  },
3975
- {
3976
- id: "review-frontend-execution-pi",
3977
- depends_on: ["frontend-test-result-outcome-gate-shell"],
3978
- role: "reviewer",
3979
- executor: "pi",
3980
- complexity: "HIGH",
3981
- writePolicy: "read-only",
3982
- allowedPaths: ["testcase/frontend/**"],
3983
- forbiddenPaths: forbidden,
3984
- outputContract: "Read-only AC-to-case-to-browser-evidence review, including failed, blocked and token-budget-exhausted cases.",
3985
- subtask_prompt: "Review the frontend case aggregate and on-disk case/evidence artifacts. A passed case requires assertion plus screenshot or equivalent browser evidence; failed/blocked cases require reasons. Do not replace browser evidence with model conclusions.",
3986
- },
3987
3973
  {
3988
3974
  id: "frontend-test-retrospect-pi",
3989
- depends_on: ["review-frontend-execution-pi"],
3975
+ depends_on: ["materialize-frontend-test-result-shell"],
3990
3976
  role: "closeout",
3991
3977
  executor: "pi",
3992
3978
  toolProfile: "write",
@@ -3995,8 +3981,8 @@ function buildFrontendTestHybridDag(sources) {
3995
3981
  writeSet: ["testcase/frontend/reports/**"],
3996
3982
  allowedPaths: ["testcase/frontend/**"],
3997
3983
  forbiddenPaths: forbidden,
3998
- outputContract: "Write frontend-test retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, risks, findings, and A/B/C/D rating.",
3999
- subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/. Summarize coverage, passed/failed/blocked cases (including token-budget-exhausted), review findings, browser anomalies, residual risks, and A/B/C/D rating. Blocked cases never count as passed. Do not write docs/**.",
3984
+ outputContract: "Write frontend-test retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, execution evidence review, risks, findings, and A/B/C/D rating — even when outcome is failed/incomplete.",
3985
+ subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report: coverage, passed/failed/blocked (including token-budget-exhausted), evidence gaps, browser anomalies, residual risks, and A/B/C/D rating. Passed cases need assertion plus screenshot or equivalent evidence when available; failed/blocked need explicit reasons. Blocked cases never count as passed. Do not replace browser evidence with model conclusions. Do not write docs/**.",
4000
3986
  },
4001
3987
  ],
4002
3988
  };
@@ -4,10 +4,12 @@ Use `playwright-cli-case-generator`. Read only `testcase/frontend/rag/context.md
4
4
 
5
5
  Each case is independently executable and includes AC mapping, preconditions, cleanup, UI assertions, and its isolated evidence path. Use dimensions `core`, `boundary`, `flow`, or `backend`. Never guess API fields, constraints, SLA, credentials, or unrecorded data.
6
6
 
7
- Every case must use this exact browser-start command prefix:
7
+ Every case must use the **resolved** absolute base URL from `testcase/frontend/rag/context.md` (field `baseUrl` / base URL line). Do not leave a `<base-url>` placeholder. Resolution policy (already applied by retrieve-context): prefer `config.md` frontend URL; else default `http://localhost:5173`.
8
+
9
+ Every case must use this exact browser-start command prefix with that concrete URL:
8
10
 
9
11
  ```text
10
- playwright-cli open --browser=chrome --headed <base-url>
12
+ playwright-cli open --browser=chrome --headed <resolved-base-url-from-context.md>
11
13
  ```
12
14
 
13
15
  Do not put a session flag before `open`. Every later Playwright CLI command must stay in that same default browser session: do **not** emit `-s=<case-id>`, `-s=...`, or assume an undocumented named-session binding.
@@ -11,7 +11,7 @@
11
11
  "globalConstraints": [
12
12
  "Do not generate pytest or Playwright source code.",
13
13
  "Only use declared isolated test environments; production URLs and real credentials are blocked.",
14
- "Every generated browser start command is playwright-cli open --browser=chrome --headed <base-url>; subsequent commands stay in that default session and must not use unverified named-session flags.",
14
+ "Every generated browser start command is playwright-cli open --browser=chrome --headed <resolved-base-url> (from task source config.md when present, else http://localhost:5173); subsequent commands stay in that default session and must not use unverified named-session flags.",
15
15
  "A frontend case review must emit VERDICT: pass before manifest materialization; request-revision blocks browser execution.",
16
16
  "Case children execute serially. Persist each case result, logs and browser evidence before the next child starts.",
17
17
  "A token threshold is a post-case stop check, not a model hard token cap; unstarted cases must be recorded as blocked: token-budget-exhausted."
@@ -259,14 +259,13 @@
259
259
  },
260
260
  "childTask": {
261
261
  "executor": "pi",
262
- "role": "verifier",
262
+ "role": "implementer",
263
263
  "skills": [
264
- "playwright-cli",
265
- "webapp-testing"
264
+ "playwright-cli"
266
265
  ],
267
266
  "toolProfile": "write",
268
267
  "complexity": "MED",
269
- "subtaskPromptTemplate": "Execute {{case.caseId}} from {{case.casePath}} in a fresh Pi session. Use exactly this browser start command prefix: playwright-cli open --browser=chrome --headed <base-url>. Do not put session flags before open. Every later playwright-cli command must use that same default browser session; must not use -s=<case-id>, -s=, or any named-session flag because no session binding is verified. For every sub-scenario, record fixture/reset, UI reset, and a fresh snapshot before using element references. If the isolated environment is missing, write blocked evidence before any browser command; do not open or connect to a browser. Before returning, always persist {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json. case-result.json must be JSON with matching caseId, status as passed, failed, or blocked, and evidencePaths array; a blocked result must include non-empty blockedReason and must never imply pass. Run a local deterministic validation before returning: node -e \"const fs=require('fs');const p='{{case.evidenceDir}}';const r=JSON.parse(fs.readFileSync(p+'case-result.json','utf8'));if(!fs.existsSync(p+'execution.md')||r.caseId!=='{{case.caseId}}'||!['passed','failed','blocked'].includes(r.status)||(r.status==='blocked'&&!(typeof r.blockedReason==='string'&&r.blockedReason.trim())))process.exit(1)\". Persist screenshots/trace/video/logs when actually available. execution.md must record the executed or blocked steps, base URL safety decision, fixture/reset and request-observation availability, and evidence file list. A business failed or blocked case is a recorded result, not a node failure. Close the session and return only compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
268
+ "subtaskPromptTemplate": "Primary job: EXECUTE {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). 1) Read baseUrl from testcase/frontend/rag/context.md (config.md preferred, else http://localhost:5173). 2) playwright-cli open --browser=chrome --headed <resolved-base-url> (default session; no -s=). 3) Follow case steps with snapshot before element refs. 4) If env/CLI/baseUrl missing, write blocked evidence and do not open a browser. Always persist {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json (caseId, status passed|failed|blocked, evidencePaths; blocked needs blockedReason). Validate with node -e before return. Business failed/blocked is not a node failure. Close browser. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
270
269
  "outputContract": "Compact JSON <=1200 chars.",
271
270
  "writePolicy": "exclusive",
272
271
  "allowedPaths": [
@@ -358,7 +357,7 @@
358
357
  "artifacts/**"
359
358
  ],
360
359
  "outputContract": "Pass only for a real passed frontend-test-result-v1.",
361
- "subtask_prompt": "Fail closed unless the final frontend test result records a real pass.",
360
+ "subtask_prompt": "Delivery/Worker gate for authoritative frontend-test result. Retrospective does not depend on this node so failed runs can still write reports.",
362
361
  "shell": {
363
362
  "commands": [
364
363
  "test -n \"${HARNESS_DAG_RUN_DIR:-}\" || { echo \"missing HARNESS_DAG_RUN_DIR for frontend-test outcome gate\" >&2; exit 2; }; RESULT=\"${HARNESS_DAG_RUN_DIR}/contracts/frontend-test-result.json\"; test -f \"${RESULT}\" || { echo \"missing frontend-test result: ${RESULT}\" >&2; exit 2; }; node -e 'const fs=require(\"fs\");const r=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\"));const ok=r.outcome===\"passed\"&&r.integrationMode===\"real\"&&Number(r.totals?.failed||0)===0&&Number(r.totals?.blocked||0)===0&&Array.isArray(r.acceptanceCoverage?.missing)&&r.acceptanceCoverage.missing.length===0;if(!ok)process.exit(1);' \"${RESULT}\""
@@ -367,29 +366,10 @@
367
366
  "timeoutMs": 60000
368
367
  }
369
368
  },
370
- {
371
- "id": "review-frontend-execution-pi",
372
- "depends_on": [
373
- "frontend-outcome-gate-shell"
374
- ],
375
- "executor": "pi",
376
- "role": "reviewer",
377
- "complexity": "HIGH",
378
- "writePolicy": "read-only",
379
- "allowedPaths": [
380
- "testcase/frontend/**"
381
- ],
382
- "forbiddenPaths": [
383
- ".harness/**",
384
- "artifacts/**"
385
- ],
386
- "outputContract": "AC-to-case-to-browser-evidence review.",
387
- "subtask_prompt_markdown": "./frontend-test-dag.review-execution.prompt.md"
388
- },
389
369
  {
390
370
  "id": "frontend-test-retrospect-pi",
391
371
  "depends_on": [
392
- "review-frontend-execution-pi"
372
+ "finalize-frontend-test-result-shell"
393
373
  ],
394
374
  "executor": "pi",
395
375
  "role": "closeout",
@@ -406,8 +386,8 @@
406
386
  ".harness/**",
407
387
  "artifacts/**"
408
388
  ],
409
- "outputContract": "Write testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, risks, findings, and A/B/C/D rating.",
410
- "subtask_prompt": "Write the frontend test retrospective under testcase/frontend/reports/. Summarize coverage, passed/failed/blocked cases (including token-budget-exhausted), review findings, browser anomalies, residual risks, and A/B/C/D rating. Blocked cases never count as passed. Do not write docs/**."
389
+ "outputContract": "Write testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, execution evidence review, risks, findings, and A/B/C/D rating — even when outcome is failed/incomplete.",
390
+ "subtask_prompt": "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report. Blocked cases never count as passed. Do not write docs/**."
411
391
  }
412
392
  ]
413
393
  }
@@ -1,3 +1,14 @@
1
1
  # Retrieve frontend test context
2
2
 
3
3
  Write only `testcase/frontend/rag/context.md` and `coverage-map.md`. Record traceable facts from the task source, routes, components, API/Mock contracts, existing tests and execution contract. Do not invent fields, credentials, limits or test data.
4
+
5
+ ## Base URL resolution (required)
6
+
7
+ Resolve a single absolute browser base URL and record it explicitly in `context.md` (both a human-readable `base URL` line and a machine-readable `baseUrl: <url>` line):
8
+
9
+ 1. Prefer frontend URL facts from task source `config.md` (including `source/references/**/config.md` or any attached reference named `config.md`): keys such as `baseUrl`, `base_url`, `frontendBaseUrl`, `FRONTEND_BASE_URL`, `url`, or labeled frontend base URL text.
10
+ 2. If no usable absolute `http://` / `https://` URL is found in `config.md` (or equivalent source facts), default to `http://localhost:5173`.
11
+ 3. Never use production hosts. Prefer local / isolated non-production URLs.
12
+ 4. Also record `baseUrlSource: config.md|<path>` or `baseUrlSource: default-localhost-5173` so later nodes can audit the choice.
13
+ 5. Include the exact browser start prefix that generators must copy:
14
+ `playwright-cli open --browser=chrome --headed <resolved-base-url>`.
@@ -1,3 +1,5 @@
1
1
  # Frontend test retrospective
2
2
 
3
- Write a dated report under `testcase/frontend/reports/` covering case coverage, passed/failed/blocked results, review findings, browser anomalies, residual risks, and an A/B/C/D maturity rating. Cite the deterministic case-evidence validation outcome. Blocked cases never count as passed; a missing or malformed `execution.md` / `case-result.json` is a verification gap, not a pass. Do not write under `docs/**`.
3
+ Write a dated report under `testcase/frontend/reports/` (for example `frontend-test-retrospect-<date>.md`) after `frontend-test-result-v1` materialization. Do **not** require outcome=pass; failed, incomplete, and blocked runs still need a report.
4
+
5
+ Combine a short AC → case → browser-evidence review with the closeout: case coverage, passed/failed/blocked results (including `token-budget-exhausted`), evidence gaps, browser anomalies, residual risks, and an A/B/C/D maturity rating. Cite the deterministic case-evidence validation outcome. Passed cases should reference assertion plus screenshot or equivalent browser evidence when available; failed/blocked cases need explicit reasons. Blocked cases never count as passed; a missing or malformed `execution.md` / `case-result.json` is a verification gap, not a pass. Do not write under `docs/**`.
@@ -2,4 +2,4 @@
2
2
 
3
3
  Read the RAG files and Markdown cases only. First line must be `VERDICT: pass` or `VERDICT: request-revision`. Report AC coverage, case independence, evidence completeness, unsafe environment/data dependencies, and manifest issues. This verdict is a deterministic safety gate: `request-revision` blocks manifest materialization and browser execution.
4
4
 
5
- Every case must retain the exact browser-start command prefix `playwright-cli open --browser=chrome --headed <base-url>`; session flags must not precede `open`, and subsequent commands must remain in its default session without `-s=` or assumed named-session binding. Verify every executable sub-scenario specifies fixture/reset, UI reset, fresh snapshot before element refs, and an evidence write point. Verify each case requires both `execution.md` and `case-result.json` under its own evidence directory. The JSON result must contain matching `caseId`, `status` (`passed`, `failed`, or `blocked`) and `evidencePaths`; blocked cases must name a non-empty `blockedReason` and cannot count as passed.
5
+ Every case must retain the exact browser-start command prefix with the resolved absolute baseUrl from `testcase/frontend/rag/context.md` (prefer task source `config.md`, else `http://localhost:5173`): `playwright-cli open --browser=chrome --headed <resolved-base-url>`; session flags must not precede `open`, and subsequent commands must remain in its default session without `-s=` or assumed named-session binding. Verify every executable sub-scenario specifies fixture/reset, UI reset, fresh snapshot before element refs, and an evidence write point. Verify each case requires both `execution.md` and `case-result.json` under its own evidence directory. The JSON result must contain matching `caseId`, `status` (`passed`, `failed`, or `blocked`) and `evidencePaths`; blocked cases must name a non-empty `blockedReason` and cannot count as passed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.16.17",
3
+ "version": "0.16.18",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -32,7 +32,8 @@ Playwright/Pytest 源码,也不修改被测应用。
32
32
 
33
33
  1. 元信息:功能、CRUD 分类、维度、关联 AC、RAG 来源、API 映射状态与数据策略。
34
34
  2. 前置条件:独立 `-s=<case-id>` session、登录状态、fixture/存量数据、清理责任。
35
- 3. 可独立执行的命令序列:`open --browser=chrome --headed <base-url>`、按需登录/数据准备、
35
+ 3. 可独立执行的命令序列:使用 RAG `context.md` 中已解析的绝对 `baseUrl`(优先来自任务源 `config.md`;缺失时默认 `http://localhost:5173`),写成
36
+ `open --browser=chrome --headed <resolved-base-url>`,禁止保留 `<base-url>` 占位符;再按需登录/数据准备、
36
37
  `snapshot` 后优先使用元素引用、操作、UI 断言、可选 API 断言、cleanup、`close`。
37
38
  4. 明确的 UI/API 预期与数据清理结果;无法满足的环境或数据依赖必须写为 `blocked`。
38
39