@tea-agent/loop-agent 0.38.2 → 0.38.4

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.
Files changed (192) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +171 -96
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +4 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +428 -27
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/commands/task-advance.js +31 -0
  17. package/dist/executors/dag-pi-executor.js +76 -10
  18. package/dist/executors/pi-event-serializer.js +42 -1
  19. package/dist/executors/pi-executor.js +51 -0
  20. package/dist/executors/pi-extension-resolver.js +233 -0
  21. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  22. package/dist/executors/pi-sdk-executor.js +187 -59
  23. package/dist/executors/shell-executor.js +265 -15
  24. package/dist/executors/shell-write-guard.js +7 -0
  25. package/dist/shared/dag-prompt-override.js +28 -0
  26. package/dist/shared/operator/capabilities.js +32 -0
  27. package/dist/shared/playwright-cli-command-policy.js +15 -0
  28. package/dist/shared/update/console-notifier.js +100 -0
  29. package/dist/{cli → shared}/update/npm-client.js +41 -8
  30. package/dist/{cli → shared}/update/state.js +41 -13
  31. package/dist/task/contract/constants.js +1 -0
  32. package/dist/task/contract/diff.js +26 -0
  33. package/dist/task/contract/project.js +5 -0
  34. package/dist/task/contract/schema.js +6 -1
  35. package/dist/task/source-prepare/build-draft.js +15 -0
  36. package/dist/task/source-prepare/parse-intent.js +24 -4
  37. package/dist/worker/cli.js +31 -1
  38. package/dist/worker/console/chat/routes.js +1 -0
  39. package/dist/worker/console/dag-execution-receipt.js +33 -0
  40. package/dist/worker/console/operator-actions.js +116 -0
  41. package/dist/worker/console/operator-user-error.js +2 -2
  42. package/dist/worker/console/pi-readiness.js +4 -4
  43. package/dist/worker/console/routes.js +43 -2
  44. package/dist/worker/console/security.js +63 -9
  45. package/dist/worker/console/server.js +36 -5
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  78. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  79. package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  105. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  106. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  107. package/dist/worker/observe/node-input.js +74 -3
  108. package/dist/worker/observe/node-process.js +377 -0
  109. package/dist/worker/observe/routes.js +36 -3
  110. package/dist/worker/observe/shell-handler-keys.js +34 -0
  111. package/dist/worker/observe/static/api.js +66 -0
  112. package/dist/worker/observe/static/constants.js +8 -1
  113. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  114. package/dist/worker/observe/static/format.js +23 -0
  115. package/dist/worker/observe/static/markdown-render.js +20 -1
  116. package/dist/worker/observe/static/operator-chrome.js +4 -0
  117. package/dist/worker/observe/static/state.js +13 -2
  118. package/dist/worker/observe/static/styles.css +73 -0
  119. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  120. package/dist/worker/observe/static/views/dag.js +8 -1
  121. package/dist/worker/observe/static/views/failures.js +5 -2
  122. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  123. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +76 -6
  124. package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
  125. package/dist/workflows/dag/contract-output-registry.js +15 -0
  126. package/dist/workflows/dag/failure-category.js +12 -0
  127. package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
  128. package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
  129. package/dist/workflows/dag/frontend-repair.js +14 -0
  130. package/dist/workflows/dag/frontend-risk.js +15 -2
  131. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  132. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  133. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  134. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  135. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  136. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  137. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  138. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  139. package/dist/workflows/dag/init-hybrid.js +196 -117
  140. package/dist/workflows/dag/node-execution.js +139 -10
  141. package/dist/workflows/dag/prompt.js +15 -1
  142. package/dist/workflows/dag/rerun-plan.js +134 -3
  143. package/dist/workflows/dag/rerun-run.js +58 -6
  144. package/dist/workflows/dag/retry-policy.js +91 -0
  145. package/dist/workflows/dag/structured-output-repair.js +712 -0
  146. package/dist/workflows/dag/types.js +32 -1
  147. package/dist/workflows/dag/validate.js +41 -1
  148. package/docs/init-surface.manifest.json +5 -0
  149. package/docs/operations/README.md +1 -1
  150. package/docs/operations/local-development-environment.md +1 -5
  151. package/docs/skills/vetted-skill-registry.md +14 -0
  152. package/docs/templates/README.md +1 -1
  153. package/docs/templates/agent-dag.schema.json +44 -0
  154. package/docs/templates/backend-test-dag.json +2 -2
  155. package/docs/templates/frontend-test-dag.json +8 -10
  156. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  157. package/docs/templates/init-managed-agents.md +13 -3
  158. package/harness.json +2 -2
  159. package/package.json +12 -4
  160. package/scripts/next-info.mjs +356 -0
  161. package/scripts/next-publish-gate.mjs +238 -0
  162. package/scripts/release-source-binding.mjs +251 -0
  163. package/skills/analyze-product-requirements/SKILL.md +8 -5
  164. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  165. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  166. package/skills/analyze-product-requirements/references/example.md +50 -0
  167. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  168. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  169. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  170. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  171. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  172. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  173. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  174. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  175. package/skills/codebase-scout/SKILL.md +1 -1
  176. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  178. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  179. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  180. package/skills/improve-codebase-architecture/deepening.md +37 -0
  181. package/skills/improve-codebase-architecture/html-report.md +123 -0
  182. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  183. package/skills/improve-codebase-architecture/language.md +53 -0
  184. package/skills/loop-agent/references/command-reference.md +11 -4
  185. package/skills/playwright-cli/SKILL.md +22 -0
  186. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  187. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
  188. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
  189. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
  190. package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
  191. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
  192. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
@@ -1,16 +1,19 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { isPlaywrightCliCommand } from "../../shared/playwright-cli-command-policy.js";
4
+ import { isTruncatedFrontendAcId, isTruncatedWhenUndeclaredFrontendAcId, } from "./frontend-test-result-contract.js";
4
5
  const COMMAND_FENCE_LANGUAGE = /^(?:|bash|sh|shell|zsh|fish|console|terminal|command|cmd|powershell|pwsh|javascript|js|typescript|ts|python|py)$/i;
5
6
  const LIST_ITEM = /^\s*(?:[-+*]|\d+[.)])\s+(.+)$/;
6
7
  const INDENTED_CODE = /^(?: {4,}|\t+)(\S.*)$/;
7
8
  const EXPLICIT_COMMAND = /^(?:(?:shell|terminal)(?:\s+command)?|command|run command|execute command)\s*:\s*(.+)$/i;
8
9
  const BLOCKED_REASON_PREFIX = /^(?:(?:blocked|forbidden|reject(?:ed)?|disallow(?:ed)?|prohibited)(?:\s+reason)?\s*(?::|\bbecause\b)|(?:do not|must not|never)\s+(?:run|execute|use)\b)/i;
9
10
  /** Standalone blockedReason / environment tokens (not executable commands). */
10
- const BLOCKED_REASON_TOKEN = /^(?:playwright-cli-unavailable|frontend-base-url-unreachable|curl-unavailable|browser-command-capability-unavailable|browser-command-evidence-missing|token-budget-exhausted|current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable|invalid-evidence-shape)(?:\s|$|[.,;:)`'"\]])/i;
11
+ const BLOCKED_REASON_TOKEN = /^(?:playwright-cli-unavailable|frontend-base-url-unreachable|curl-unavailable|browser-command-capability-unavailable|browser-command-evidence-missing|token-budget-exhausted|current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable|unique-control-unavailable|invalid-evidence-shape)(?:\s|$|[.,;:)`'"\]])/i;
11
12
  const BLOCKED_PROSE_HINT = /(?:blockedReason|blocked\s*reason|environmentProbe|evidenceDir|\bunavailable\b|不可用|写\s*blocked|写入\s*blocked|伪命令|自然语言|元数据|fail-closed|探测失败)/i;
12
13
  const IMPERATIVE_COMMAND = /^(?:(?:run|execute|use)(?:\s+(?:the\s+)?(?:(?:shell|terminal)\s+)?command)?|in\s+(?:the\s+)?(?:shell|terminal|console)\s*,?\s*(?:run|execute|use))\s*:?\s+(.+)$/i;
13
14
  const INLINE_CODE_STEP = /^`([^`\r\n]+)`[.!?]?$/;
15
+ /** Assertion-count annotations appended to a playwright-cli literal (e.g. `find "锚点">=1`, `>=N`, `≥N`, `(≥1)`) document an expected match count; they are documentation, not shell arguments. */
16
+ const ASSERTION_COUNT_SUFFIX = /\s*[((]?\s*(?:>=|≥|>|超过|至少)\s*\d+\s*[))]?\s*$/;
14
17
  const AUTOMATION_EXECUTABLE = /^(?:playwright-cli\b|playwright\b|@playwright\/test\b|npx\b|npm\b|pnpm\b|yarn\b|bunx?\b|node(?:js)?\b|python(?:3)?\b|bash\b|sh\b|zsh\b|fish\b|powershell\b|pwsh\b|cmd(?:\.exe)?\b|cypress\b|selenium\b|webdriverio\b|chromedriver\b|google-chrome\b|chrome\b|firefox\b|curl\b|wget\b)/i;
15
18
  const SHELL_WRAPPED_EXECUTABLE = /^(?:sudo\s+|env(?:\s+[A-Za-z_][A-Za-z0-9_]*=[^\s]+)*\s+|(?:\.{1,2}[\\/]|~[\\/]|[A-Za-z]:[\\/]|\/)[^\s]+)/i;
16
19
  const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)/;
@@ -44,6 +47,11 @@ function isBlockedReason(value) {
44
47
  return true;
45
48
  if (/^blockedReason\s*[:=]/i.test(trimmed))
46
49
  return true;
50
+ // Expected-outcome field lines ("status=blocked" / "status: failed") document
51
+ // the intended terminal state of a designed-negative case; they are
52
+ // case-result metadata, never executable commands.
53
+ if (/^["'`]?status["'`]?\s*[:=]\s*["'`]?(passed|failed|blocked)["'`]?\s*[.。]?$/i.test(trimmed))
54
+ return true;
47
55
  return false;
48
56
  }
49
57
  /**
@@ -238,6 +246,12 @@ function commandGateIssue(input) {
238
246
  if (input.instruction.fromFence && isBlockedReason(input.instruction.command)) {
239
247
  return null;
240
248
  }
249
+ // An expected-match-count annotation (`find "锚点">=1`, with or without a
250
+ // playwright-cli prefix) documents the assertion; it is not part of the
251
+ // command and never executable input.
252
+ if (/^(?:playwright-cli\s+)?find\s+"[^"]*"/i.test(input.instruction.command) && ASSERTION_COUNT_SUFFIX.test(input.instruction.command)) {
253
+ return null;
254
+ }
241
255
  const commandMatch = input.instruction.command.match(/^playwright-cli\s+([^\s`]+)/i);
242
256
  if (commandMatch) {
243
257
  const command = commandMatch[1].toLowerCase().replace(/^[`'"]+|[`'":,,。→]+$/g, "");
@@ -282,7 +296,13 @@ export async function validateFrontendCaseChecklist(input) {
282
296
  const manifestPath = await exists(draft) ? draft : await exists(final) ? final : "";
283
297
  if (!manifestPath)
284
298
  throw new Error("checklist: missing manifest.draft.json or manifest.json");
285
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
299
+ let manifest;
300
+ try {
301
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
302
+ }
303
+ catch {
304
+ throw new Error("checklist: manifest is malformed JSON");
305
+ }
286
306
  if (!Array.isArray(manifest.cases) || manifest.cases.length === 0)
287
307
  throw new Error("checklist: empty cases");
288
308
  const declaredAc = new Set(input.declaredAcIds ?? []);
@@ -334,6 +354,10 @@ export async function validateFrontendCaseChecklist(input) {
334
354
  for (const ac of item.acIds) {
335
355
  if (typeof ac !== "string" || !acIdRe.test(ac))
336
356
  issues.push({ ruleId: "ac-id-shape", caseId: id, casePath, detail: String(ac) });
357
+ else if (declaredAc.size > 0 && isTruncatedFrontendAcId(ac))
358
+ issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
359
+ else if (declaredAc.size === 0 && isTruncatedWhenUndeclaredFrontendAcId(ac))
360
+ issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
337
361
  else if (declaredAc.size > 0 && !declaredAc.has(ac))
338
362
  issues.push({ ruleId: "unknown-ac", caseId: id, casePath, detail: `${ac} not in sourceBinding` });
339
363
  }
@@ -1,5 +1,6 @@
1
1
  import { readFile, rename, unlink, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { isTruncatedFrontendAcId, isTruncatedWhenUndeclaredFrontendAcId, } from "./frontend-test-result-contract.js";
3
4
  const DIMENSIONS = new Set(["core", "boundary", "flow", "backend"]);
4
5
  function fail(ruleId, detail) {
5
6
  const msg = JSON.stringify({ ruleId, detail: String(detail ?? "") });
@@ -62,6 +63,10 @@ export async function materializeFrontendTestCaseManifest(input) {
62
63
  for (const ac of c.acIds) {
63
64
  if (typeof ac !== "string" || !acIdRe.test(ac))
64
65
  fail("ac-id-shape", `acIds entry must be AC-* acceptance id, not caseId: ${ac}`);
66
+ if ((declaredAc.size > 0
67
+ ? isTruncatedFrontendAcId(ac)
68
+ : isTruncatedWhenUndeclaredFrontendAcId(ac)))
69
+ fail("ac-id-truncated", `acIds entry is a truncated acceptance id (missing feature prefix or sequence number): ${ac}`);
65
70
  if (declaredAc.size > 0 && !declaredAc.has(ac))
66
71
  fail("unknown-ac", `${ac} not in sourceBinding; repair generator input or AC list`);
67
72
  acIds.push(ac);
@@ -1,25 +1,17 @@
1
1
  import { readFile, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { markdownSection } from "./frontend-test-markdown.js";
3
4
  const CASE_ID = /^FE-[A-Za-z0-9]+-\d{3}-(?:core|boundary|flow|backend)$/i;
4
5
  const AC_ID = /^AC(?:-[A-Z0-9]+)+$/i;
5
6
  const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|结果正确|页面正常|按实际情况处理|验证成功/i;
6
7
  const SECTION_ALIASES = {
7
8
  purpose: ["Test Purpose", "测试目的", "测试场景"],
8
9
  source: ["Source References", "需求依据"],
9
- preconditions: ["Preconditions", "前置条件"],
10
- steps: ["Steps", "操作步骤"],
10
+ preconditions: ["Preconditions", "前置条件", "前置条件与重置"],
11
+ steps: ["Test Steps", "测试步骤", "Steps", "操作步骤"],
11
12
  expected: ["Expected Results", "预期结果"],
12
13
  automation: ["Automation Notes", "自动化映射", "自动化说明"],
13
14
  };
14
- function section(body, names) {
15
- const marker = new RegExp(`^###\\s+(?:${names.map((v) => v.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")).join("|")})\\s*$`, "mi");
16
- const hit = marker.exec(body);
17
- if (!hit)
18
- return "";
19
- const rest = body.slice(hit.index + hit[0].length);
20
- const next = /^###\s+/m.exec(rest);
21
- return rest.slice(0, next?.index ?? rest.length).trim();
22
- }
23
15
  function hasList(value) { return /^\s*(?:\d+[.)]|[-*+])\s+\S+/m.test(value); }
24
16
  function hasAssertion(value) { return /(?:status|状态|包含|显示|等于|为|可见|不可见|跳转|错误|成功|失败|should|expect|assert|must)/i.test(value); }
25
17
  export async function validateFrontendCaseContent(input) {
@@ -69,9 +61,9 @@ export async function validateFrontendCaseContent(input) {
69
61
  if (PLACEHOLDER.test(body))
70
62
  findings.push({ ruleId: "placeholder-wording", caseId: id, detail: "case contains placeholder or non-assertable wording" });
71
63
  for (const [key, names] of Object.entries(SECTION_ALIASES))
72
- if (!section(body, names))
64
+ if (!markdownSection(body, names))
73
65
  findings.push({ ruleId: `missing-${key}`, caseId: id, detail: `missing section: ${names.join(" or ")}` });
74
- const steps = section(body, SECTION_ALIASES.steps), expected = section(body, SECTION_ALIASES.expected);
66
+ const steps = markdownSection(body, SECTION_ALIASES.steps), expected = markdownSection(body, SECTION_ALIASES.expected);
75
67
  if (!hasList(steps))
76
68
  findings.push({ ruleId: "unstructured-steps", caseId: id, detail: "steps should contain numbered or bulleted executable actions" });
77
69
  if (!hasList(expected) || !hasAssertion(expected))
@@ -0,0 +1,227 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ export const FRONTEND_TEST_CONTEXT_REL = "testcase/frontend/rag/context.md";
5
+ export const FRONTEND_TEST_PROBE_REL = "testcase/frontend/rag/environment-probe.json";
6
+ const CONNECTION_REFUSED_HINT = "被测服务未在 baseUrl 监听(connection refused)。请先启动本地前端(例如 scripts/serve.sh 或项目约定的 npm start),确认可访问后再从 materialize-frontend-test-execution-shell 续跑。";
7
+ export function classifyFrontendBaseUrlProbeFailure(input) {
8
+ if (input.spawnError)
9
+ return { errorClass: "spawn-error" };
10
+ const exit = input.curlExit ?? 0;
11
+ if (exit === 7) {
12
+ return { errorClass: "connection-refused", hint: CONNECTION_REFUSED_HINT };
13
+ }
14
+ if (exit === 6) {
15
+ return {
16
+ errorClass: "dns-unresolved",
17
+ hint: "无法解析 baseUrl 主机名。确认 context.md 中的地址可在本机解析,或改用 127.0.0.1。",
18
+ };
19
+ }
20
+ if (exit === 28) {
21
+ return {
22
+ errorClass: "connect-timeout",
23
+ hint: "连接 baseUrl 超时。确认被测服务已启动且端口可访问。",
24
+ };
25
+ }
26
+ if (exit !== 0)
27
+ return { errorClass: `curl-exit-${exit}` };
28
+ if (input.httpStatus && Number.isFinite(input.httpStatus)) {
29
+ return { errorClass: `http-${input.httpStatus}` };
30
+ }
31
+ return { errorClass: "unreachable" };
32
+ }
33
+ function defaultCurlRunner(args) {
34
+ const result = spawnSync("curl", [...args], { encoding: "utf8" });
35
+ return {
36
+ status: result.status,
37
+ stdout: result.stdout ?? "",
38
+ stderr: result.stderr ?? "",
39
+ error: result.error,
40
+ };
41
+ }
42
+ function redactUrl(value) {
43
+ try {
44
+ const parsed = new URL(value);
45
+ parsed.username = "";
46
+ parsed.password = "";
47
+ parsed.search = "";
48
+ return parsed.toString();
49
+ }
50
+ catch {
51
+ return value.replace(/\/\/[^@\s]+@/g, "//");
52
+ }
53
+ }
54
+ function resolveBaseUrl(context) {
55
+ const patterns = [
56
+ /baseUrl\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
57
+ /base[-_ ]url\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
58
+ /playwright-cli open --browser=chrome\s+((?:https?):\/\/[^\s"'`<>]+)/i,
59
+ /(https?:\/\/(?:localhost|127\.0\.0\.1)[^\s)}\],"']*)/i,
60
+ ];
61
+ for (const pattern of patterns) {
62
+ const match = context.match(pattern);
63
+ if (match?.[1])
64
+ return match[1].replace(/[)}\],"'.`]+$/g, "");
65
+ }
66
+ return null;
67
+ }
68
+ async function writeProbe(workspaceRoot, probe) {
69
+ const probePath = path.join(workspaceRoot, FRONTEND_TEST_PROBE_REL);
70
+ const contextPath = path.join(workspaceRoot, FRONTEND_TEST_CONTEXT_REL);
71
+ await mkdir(path.dirname(probePath), { recursive: true });
72
+ await writeFile(probePath, `${JSON.stringify(probe, null, 2)}\n`, "utf8");
73
+ let context = "";
74
+ try {
75
+ context = await readFile(contextPath, "utf8");
76
+ }
77
+ catch {
78
+ context = "";
79
+ }
80
+ const line = `environmentProbe: ${probe.status}${probe.blockedReason ? ` (${probe.blockedReason})` : ""}`;
81
+ const next = /environmentProbe\s*[:=]/i.test(context)
82
+ ? context.replace(/environmentProbe\s*[:=]\s*.*/i, line)
83
+ : `${context.trimEnd()}\n\n${line}\n`;
84
+ await writeFile(contextPath, next, "utf8");
85
+ }
86
+ export async function probeFrontendTestEnvironment(input) {
87
+ const curl = input.curl ?? defaultCurlRunner;
88
+ const contextPath = path.join(input.workspaceRoot, FRONTEND_TEST_CONTEXT_REL);
89
+ let context;
90
+ try {
91
+ context = await readFile(contextPath, "utf8");
92
+ }
93
+ catch {
94
+ return { ok: false, stderr: `missing ${FRONTEND_TEST_CONTEXT_REL}` };
95
+ }
96
+ const rawUrl = resolveBaseUrl(context);
97
+ if (!rawUrl) {
98
+ return {
99
+ ok: false,
100
+ stderr: "frontend-test preflight missing absolute baseUrl from context.md",
101
+ };
102
+ }
103
+ let parsed;
104
+ try {
105
+ parsed = new URL(rawUrl);
106
+ }
107
+ catch {
108
+ return { ok: false, stderr: `baseUrl must be absolute http(s): ${rawUrl}` };
109
+ }
110
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
111
+ parsed.username ||
112
+ parsed.password ||
113
+ parsed.search ||
114
+ parsed.hash) {
115
+ return {
116
+ ok: false,
117
+ stderr: `unsafe baseUrl from context.md: ${redactUrl(rawUrl)}`,
118
+ };
119
+ }
120
+ if (/(?:^|\.)(?:www\.)?[^.]*?(?:prod|production)/i.test(parsed.hostname)) {
121
+ return {
122
+ ok: false,
123
+ stderr: `production URL forbidden: ${redactUrl(rawUrl)}`,
124
+ };
125
+ }
126
+ const baseUrl = parsed.toString();
127
+ const safe = redactUrl(baseUrl);
128
+ const sourceMatch = context.match(/baseUrlSource\s*[:=]\s*([^\r\n]+)/i);
129
+ const baseUrlSource = sourceMatch ? sourceMatch[1].trim() : "context.md";
130
+ const failBlocked = async (reason, extra) => {
131
+ const probe = {
132
+ status: "unreachable",
133
+ blockedReason: reason,
134
+ baseUrlRedacted: extra.baseUrlRedacted ?? safe,
135
+ httpStatus: extra.httpStatus ?? null,
136
+ method: extra.method ?? null,
137
+ curlExit: extra.curlExit ?? null,
138
+ errorClass: extra.errorClass ?? null,
139
+ ...(extra.hint ? { hint: extra.hint } : {}),
140
+ ...(extra.baseUrlSource ? { baseUrlSource: extra.baseUrlSource } : {}),
141
+ };
142
+ await writeProbe(input.workspaceRoot, probe);
143
+ const payload = {
144
+ blockedReason: reason,
145
+ baseUrl: probe.baseUrlRedacted,
146
+ httpStatus: probe.httpStatus,
147
+ errorClass: probe.errorClass,
148
+ ...(probe.hint ? { hint: probe.hint } : {}),
149
+ };
150
+ return {
151
+ ok: false,
152
+ stderr: `frontend-test preflight blocked: ${JSON.stringify(payload)}`,
153
+ probe,
154
+ };
155
+ };
156
+ const curlCheck = curl(["--version"]);
157
+ if (curlCheck.error || curlCheck.status !== 0) {
158
+ return failBlocked("curl-unavailable", {
159
+ baseUrlRedacted: safe,
160
+ errorClass: "curl-missing",
161
+ });
162
+ }
163
+ const probeMethod = (method) => curl([
164
+ "-sS",
165
+ "-o",
166
+ "/dev/null",
167
+ "-w",
168
+ "%{http_code}",
169
+ "--connect-timeout",
170
+ "3",
171
+ "--max-time",
172
+ "8",
173
+ "-X",
174
+ method,
175
+ "-L",
176
+ "--max-redirs",
177
+ "3",
178
+ "--http1.1",
179
+ "--proto-redir",
180
+ "=http,https",
181
+ safe,
182
+ ]);
183
+ let used = "HEAD";
184
+ let result = probeMethod("HEAD");
185
+ let code = String(result.stdout || "").trim();
186
+ let statusNum = Number.parseInt(code, 10);
187
+ const headRejected = result.status !== 0 || !statusNum || statusNum === 405 || statusNum === 501;
188
+ if (headRejected) {
189
+ used = "GET";
190
+ result = probeMethod("GET");
191
+ code = String(result.stdout || "").trim();
192
+ statusNum = Number.parseInt(code, 10);
193
+ }
194
+ const ok = statusNum >= 200 && statusNum < 400;
195
+ if (!ok) {
196
+ const classified = classifyFrontendBaseUrlProbeFailure({
197
+ spawnError: Boolean(result.error),
198
+ curlExit: result.status,
199
+ httpStatus: Number.isFinite(statusNum) ? statusNum : null,
200
+ });
201
+ return failBlocked("frontend-base-url-unreachable", {
202
+ baseUrlRedacted: safe,
203
+ httpStatus: Number.isFinite(statusNum) ? statusNum : null,
204
+ method: used,
205
+ curlExit: result.status,
206
+ errorClass: classified.errorClass,
207
+ hint: classified.hint,
208
+ });
209
+ }
210
+ const probe = {
211
+ status: "reachable",
212
+ blockedReason: null,
213
+ baseUrl: safe,
214
+ baseUrlRedacted: safe,
215
+ baseUrlSource,
216
+ httpStatus: statusNum,
217
+ method: used,
218
+ curlExit: result.status,
219
+ errorClass: null,
220
+ };
221
+ await writeProbe(input.workspaceRoot, probe);
222
+ return {
223
+ ok: true,
224
+ stdout: `frontend-test-execution-v1 validated contextBaseUrl=${safe} source=${baseUrlSource} probe=reachable method=${used} httpStatus=${statusNum}`,
225
+ probe,
226
+ };
227
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Bounded ATX section/list parsing for frontend-test case Markdown.
3
+ * Generator cases use `##` headings; older fixtures use `###`.
4
+ */
5
+ const ATX_HEADING = /^(#{1,3})\s+(\S.*)$/;
6
+ function headingMatches(title, names) {
7
+ const normalized = title.trim();
8
+ return names.some((name) => {
9
+ if (normalized === name)
10
+ return true;
11
+ return (normalized.startsWith(name) &&
12
+ (normalized.length === name.length ||
13
+ /[\s::与((-]/.test(normalized.charAt(name.length))));
14
+ });
15
+ }
16
+ export function markdownSection(body, names) {
17
+ const lines = body.replace(/\r\n/g, "\n").split("\n");
18
+ let start = -1;
19
+ let startLevel = 0;
20
+ for (let index = 0; index < lines.length; index += 1) {
21
+ const match = ATX_HEADING.exec(lines[index]);
22
+ if (!match)
23
+ continue;
24
+ if (!headingMatches(match[2], names))
25
+ continue;
26
+ start = index;
27
+ startLevel = match[1].length;
28
+ break;
29
+ }
30
+ if (start < 0)
31
+ return "";
32
+ const collected = [];
33
+ for (let index = start + 1; index < lines.length; index += 1) {
34
+ const match = ATX_HEADING.exec(lines[index]);
35
+ if (match && match[1].length <= startLevel)
36
+ break;
37
+ collected.push(lines[index]);
38
+ }
39
+ return collected.join("\n").trim();
40
+ }
41
+ export function markdownList(value) {
42
+ const items = [];
43
+ for (const raw of value.replace(/\r\n/g, "\n").split("\n")) {
44
+ const listed = raw.match(/^\s*(?:\d+[.)]|[-*+])\s+(.*)$/);
45
+ if (listed) {
46
+ const text = listed[1].trim();
47
+ if (text)
48
+ items.push(text);
49
+ continue;
50
+ }
51
+ const continuation = raw.match(/^\s{2,}(\S.*)$/);
52
+ if (continuation && items.length > 0) {
53
+ items[items.length - 1] = `${items[items.length - 1]} ${continuation[1].trim()}`;
54
+ }
55
+ }
56
+ return items;
57
+ }
58
+ export function markdownH1Title(body) {
59
+ const match = /^#\s+(\S.*)$/m.exec(body.replace(/\r\n/g, "\n"));
60
+ return match?.[1]?.trim() ?? "";
61
+ }