@tea-agent/loop-agent 0.39.0-next.9 → 0.39.1
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/AGENTS.md +4 -1
- package/CHANGELOG.md +269 -69
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +16 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +124 -18
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +74 -28
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +555 -34
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-failure-category.js +138 -0
- package/dist/shared/dag-prompt-override.js +27 -0
- package/dist/shared/operator/capabilities.js +181 -27
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +70 -15
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +28 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +3 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +20 -1
- package/dist/task/contract/schema.js +8 -1
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +474 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +19 -3
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +18 -2
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +480 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +10 -10
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +320 -4
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/pi-readiness.js +5 -4
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/routes.js +46 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +111 -7
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
- package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-B4i9cVxX.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +406 -0
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-aGmcjgTn.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-Biyo-tqA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +40 -1
- package/dist/worker/console/static-src/app/useOperatorActions.js +6 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +160 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +224 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +67 -1
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +60 -0
- package/dist/worker/observe/static/index.html +5 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +85 -0
- package/dist/worker/observe/static/views/dag-inspector.js +912 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/worker/observe/static/views/task.js +55 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +856 -54
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +343 -33
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +454 -36
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +110 -61
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +7 -116
- package/dist/workflows/dag/frontend-implementation-contract.js +465 -6
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +293 -84
- package/dist/workflows/dag/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
- package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
- package/dist/workflows/dag/frontend-verification-trace.js +29 -4
- package/dist/workflows/dag/init-hybrid.js +1019 -209
- package/dist/workflows/dag/node-execution.js +228 -15
- package/dist/workflows/dag/prompt.js +60 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +224 -27
- package/dist/workflows/dag/rerun-run.js +61 -6
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +109 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +185 -12
- package/dist/workflows/dag/validate.js +64 -20
- package/docs/README.md +1 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +6 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +2 -2
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-implementation-contract.schema.json +681 -24
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +6 -4
- package/package.json +12 -4
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
- package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
- package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Deterministic frontend-implementation risk classifier (M4).
|
|
3
3
|
* Pure function: no I/O, no model, no side effects.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Dependency-installation language in task text. Mentioning package.json or
|
|
7
|
+
* allowing its path is NOT dependency introduction by itself; the manifest
|
|
8
|
+
* path check in classifyFrontendRisk requires this language to escalate.
|
|
9
|
+
*/
|
|
10
|
+
const DEPENDENCY_INSTALL_LANGUAGE = /new\s+dependency|add\s+dependencies?|install\s+dependencies?|npm\s+(?:install|i)\b|yarn\s+(?:add|install)\b|pnpm\s+(?:add|install)\b|bun\s+(?:add|install)\b|新增依赖|添加依赖|安装依赖|引入依赖/i;
|
|
5
11
|
const HIGH_RISK_PATTERNS = [
|
|
6
12
|
{
|
|
7
13
|
id: "new-route",
|
|
@@ -25,7 +31,7 @@ const HIGH_RISK_PATTERNS = [
|
|
|
25
31
|
},
|
|
26
32
|
{
|
|
27
33
|
id: "new-dependency",
|
|
28
|
-
re:
|
|
34
|
+
re: DEPENDENCY_INSTALL_LANGUAGE,
|
|
29
35
|
},
|
|
30
36
|
{
|
|
31
37
|
id: "schema-api-change",
|
|
@@ -88,7 +94,14 @@ export function classifyFrontendRisk(input) {
|
|
|
88
94
|
const paths = input.allowedPaths ?? [];
|
|
89
95
|
const spread = pathSpreadScore(paths);
|
|
90
96
|
if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
|
|
91
|
-
|
|
97
|
+
if (DEPENDENCY_INSTALL_LANGUAGE.test(blob)) {
|
|
98
|
+
signals.push("new-dependency");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Editing manifest scripts/config is not dependency introduction;
|
|
102
|
+
// record the rejected escalation instead of forcing full gates.
|
|
103
|
+
rejectedSignals.push("new-dependency:manifest-path-without-install-language");
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
106
|
// Explicit multi-domain language only — path count alone is not high-risk
|
|
94
107
|
if (spread >= 3 &&
|
|
@@ -1,16 +1,20 @@
|
|
|
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";
|
|
5
|
+
import { defaultFrontendTestLayout, frontendTestCasePath, } from "./frontend-test-layout.js";
|
|
4
6
|
const COMMAND_FENCE_LANGUAGE = /^(?:|bash|sh|shell|zsh|fish|console|terminal|command|cmd|powershell|pwsh|javascript|js|typescript|ts|python|py)$/i;
|
|
5
7
|
const LIST_ITEM = /^\s*(?:[-+*]|\d+[.)])\s+(.+)$/;
|
|
6
8
|
const INDENTED_CODE = /^(?: {4,}|\t+)(\S.*)$/;
|
|
7
9
|
const EXPLICIT_COMMAND = /^(?:(?:shell|terminal)(?:\s+command)?|command|run command|execute command)\s*:\s*(.+)$/i;
|
|
8
10
|
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
11
|
/** 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;
|
|
12
|
+
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
13
|
const BLOCKED_PROSE_HINT = /(?:blockedReason|blocked\s*reason|environmentProbe|evidenceDir|\bunavailable\b|不可用|写\s*blocked|写入\s*blocked|伪命令|自然语言|元数据|fail-closed|探测失败)/i;
|
|
12
14
|
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
15
|
const INLINE_CODE_STEP = /^`([^`\r\n]+)`[.!?]?$/;
|
|
16
|
+
/** 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. */
|
|
17
|
+
const ASSERTION_COUNT_SUFFIX = /\s*[((]?\s*(?:>=|≥|>|超过|至少)\s*\d+\s*[))]?\s*$/;
|
|
14
18
|
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
19
|
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
20
|
const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)/;
|
|
@@ -44,6 +48,11 @@ function isBlockedReason(value) {
|
|
|
44
48
|
return true;
|
|
45
49
|
if (/^blockedReason\s*[:=]/i.test(trimmed))
|
|
46
50
|
return true;
|
|
51
|
+
// Expected-outcome field lines ("status=blocked" / "status: failed") document
|
|
52
|
+
// the intended terminal state of a designed-negative case; they are
|
|
53
|
+
// case-result metadata, never executable commands.
|
|
54
|
+
if (/^["'`]?status["'`]?\s*[:=]\s*["'`]?(passed|failed|blocked)["'`]?\s*[.。]?$/i.test(trimmed))
|
|
55
|
+
return true;
|
|
47
56
|
return false;
|
|
48
57
|
}
|
|
49
58
|
/**
|
|
@@ -238,6 +247,12 @@ function commandGateIssue(input) {
|
|
|
238
247
|
if (input.instruction.fromFence && isBlockedReason(input.instruction.command)) {
|
|
239
248
|
return null;
|
|
240
249
|
}
|
|
250
|
+
// An expected-match-count annotation (`find "锚点">=1`, with or without a
|
|
251
|
+
// playwright-cli prefix) documents the assertion; it is not part of the
|
|
252
|
+
// command and never executable input.
|
|
253
|
+
if (/^(?:playwright-cli\s+)?find\s+"[^"]*"/i.test(input.instruction.command) && ASSERTION_COUNT_SUFFIX.test(input.instruction.command)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
241
256
|
const commandMatch = input.instruction.command.match(/^playwright-cli\s+([^\s`]+)/i);
|
|
242
257
|
if (commandMatch) {
|
|
243
258
|
const command = commandMatch[1].toLowerCase().replace(/^[`'"]+|[`'":,,。→]+$/g, "");
|
|
@@ -276,13 +291,20 @@ function commandGateIssue(input) {
|
|
|
276
291
|
};
|
|
277
292
|
}
|
|
278
293
|
export async function validateFrontendCaseChecklist(input) {
|
|
279
|
-
const
|
|
294
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
295
|
+
const root = path.join(input.workspaceRoot, layout.casesDir);
|
|
280
296
|
const draft = path.join(root, "manifest.draft.json");
|
|
281
297
|
const final = path.join(root, "manifest.json");
|
|
282
298
|
const manifestPath = await exists(draft) ? draft : await exists(final) ? final : "";
|
|
283
299
|
if (!manifestPath)
|
|
284
300
|
throw new Error("checklist: missing manifest.draft.json or manifest.json");
|
|
285
|
-
|
|
301
|
+
let manifest;
|
|
302
|
+
try {
|
|
303
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
throw new Error("checklist: manifest is malformed JSON");
|
|
307
|
+
}
|
|
286
308
|
if (!Array.isArray(manifest.cases) || manifest.cases.length === 0)
|
|
287
309
|
throw new Error("checklist: empty cases");
|
|
288
310
|
const declaredAc = new Set(input.declaredAcIds ?? []);
|
|
@@ -299,7 +321,7 @@ export async function validateFrontendCaseChecklist(input) {
|
|
|
299
321
|
if (typeof item?.caseId === "string" && /^AC-/i.test(item.caseId))
|
|
300
322
|
issues.push({ ruleId: "case-id-is-ac", caseId: id, detail: "caseId must not be an acceptance id" });
|
|
301
323
|
const casePath = typeof item?.casePath === "string" ? item.casePath : "";
|
|
302
|
-
const expectedPath = typeof item?.caseId === "string" ?
|
|
324
|
+
const expectedPath = typeof item?.caseId === "string" ? frontendTestCasePath(layout, item.caseId) : "";
|
|
303
325
|
const absolute = path.resolve(input.workspaceRoot, casePath);
|
|
304
326
|
const relative = path.relative(input.workspaceRoot, absolute);
|
|
305
327
|
if (!casePath || relative.startsWith("..") || path.isAbsolute(relative) || !(await exists(absolute))) {
|
|
@@ -334,6 +356,10 @@ export async function validateFrontendCaseChecklist(input) {
|
|
|
334
356
|
for (const ac of item.acIds) {
|
|
335
357
|
if (typeof ac !== "string" || !acIdRe.test(ac))
|
|
336
358
|
issues.push({ ruleId: "ac-id-shape", caseId: id, casePath, detail: String(ac) });
|
|
359
|
+
else if (declaredAc.size > 0 && isTruncatedFrontendAcId(ac))
|
|
360
|
+
issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
|
|
361
|
+
else if (declaredAc.size === 0 && isTruncatedWhenUndeclaredFrontendAcId(ac))
|
|
362
|
+
issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
|
|
337
363
|
else if (declaredAc.size > 0 && !declaredAc.has(ac))
|
|
338
364
|
issues.push({ ruleId: "unknown-ac", caseId: id, casePath, detail: `${ac} not in sourceBinding` });
|
|
339
365
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
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";
|
|
4
|
+
import { defaultFrontendTestLayout, frontendTestCasePath, frontendTestEvidenceDir, frontendTestManifestDraftPath, frontendTestManifestPath, } from "./frontend-test-layout.js";
|
|
3
5
|
const DIMENSIONS = new Set(["core", "boundary", "flow", "backend"]);
|
|
4
6
|
function fail(ruleId, detail) {
|
|
5
7
|
const msg = JSON.stringify({ ruleId, detail: String(detail ?? "") });
|
|
@@ -18,8 +20,9 @@ function fail(ruleId, detail) {
|
|
|
18
20
|
* present). When empty, ac-id-shape still applies.
|
|
19
21
|
*/
|
|
20
22
|
export async function materializeFrontendTestCaseManifest(input) {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
24
|
+
const draft = path.join(input.workspaceRoot, frontendTestManifestDraftPath(layout));
|
|
25
|
+
const file = path.join(input.workspaceRoot, frontendTestManifestPath(layout));
|
|
23
26
|
const tmp = `${file}.tmp`;
|
|
24
27
|
const maxCases = input.maxCases ?? 32;
|
|
25
28
|
const declaredAc = new Set(input.declaredAcIds ?? []);
|
|
@@ -62,12 +65,16 @@ export async function materializeFrontendTestCaseManifest(input) {
|
|
|
62
65
|
for (const ac of c.acIds) {
|
|
63
66
|
if (typeof ac !== "string" || !acIdRe.test(ac))
|
|
64
67
|
fail("ac-id-shape", `acIds entry must be AC-* acceptance id, not caseId: ${ac}`);
|
|
68
|
+
if ((declaredAc.size > 0
|
|
69
|
+
? isTruncatedFrontendAcId(ac)
|
|
70
|
+
: isTruncatedWhenUndeclaredFrontendAcId(ac)))
|
|
71
|
+
fail("ac-id-truncated", `acIds entry is a truncated acceptance id (missing feature prefix or sequence number): ${ac}`);
|
|
65
72
|
if (declaredAc.size > 0 && !declaredAc.has(ac))
|
|
66
73
|
fail("unknown-ac", `${ac} not in sourceBinding; repair generator input or AC list`);
|
|
67
74
|
acIds.push(ac);
|
|
68
75
|
}
|
|
69
|
-
const casePath =
|
|
70
|
-
const evidenceDir =
|
|
76
|
+
const casePath = frontendTestCasePath(layout, c.caseId);
|
|
77
|
+
const evidenceDir = frontendTestEvidenceDir(layout, c.caseId);
|
|
71
78
|
for (const [k, v] of [
|
|
72
79
|
["casePath", casePath],
|
|
73
80
|
["evidenceDir", evidenceDir],
|
|
@@ -1,29 +1,23 @@
|
|
|
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";
|
|
4
|
+
import { defaultFrontendTestLayout, frontendTestCasePath, } from "./frontend-test-layout.js";
|
|
3
5
|
const CASE_ID = /^FE-[A-Za-z0-9]+-\d{3}-(?:core|boundary|flow|backend)$/i;
|
|
4
6
|
const AC_ID = /^AC(?:-[A-Z0-9]+)+$/i;
|
|
5
7
|
const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|结果正确|页面正常|按实际情况处理|验证成功/i;
|
|
6
8
|
const SECTION_ALIASES = {
|
|
7
9
|
purpose: ["Test Purpose", "测试目的", "测试场景"],
|
|
8
10
|
source: ["Source References", "需求依据"],
|
|
9
|
-
preconditions: ["Preconditions", "前置条件"],
|
|
10
|
-
steps: ["Steps", "操作步骤"],
|
|
11
|
+
preconditions: ["Preconditions", "前置条件", "前置条件与重置"],
|
|
12
|
+
steps: ["Test Steps", "测试步骤", "Steps", "操作步骤"],
|
|
11
13
|
expected: ["Expected Results", "预期结果"],
|
|
12
14
|
automation: ["Automation Notes", "自动化映射", "自动化说明"],
|
|
13
15
|
};
|
|
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
16
|
function hasList(value) { return /^\s*(?:\d+[.)]|[-*+])\s+\S+/m.test(value); }
|
|
24
17
|
function hasAssertion(value) { return /(?:status|状态|包含|显示|等于|为|可见|不可见|跳转|错误|成功|失败|should|expect|assert|must)/i.test(value); }
|
|
25
18
|
export async function validateFrontendCaseContent(input) {
|
|
26
|
-
const
|
|
19
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
20
|
+
const root = path.join(input.workspaceRoot, layout.casesDir);
|
|
27
21
|
const manifestPath = path.join(root, "manifest.json");
|
|
28
22
|
const findings = [];
|
|
29
23
|
let manifest;
|
|
@@ -57,7 +51,7 @@ export async function validateFrontendCaseContent(input) {
|
|
|
57
51
|
findings.push({ ruleId: "unknown-ac", caseId: id, detail: `${ac} is not declared by the task` });
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
|
-
const file = path.join(input.workspaceRoot, item?.casePath ||
|
|
54
|
+
const file = path.join(input.workspaceRoot, item?.casePath || frontendTestCasePath(layout, id));
|
|
61
55
|
let body = "";
|
|
62
56
|
try {
|
|
63
57
|
body = await readFile(file, "utf8");
|
|
@@ -69,9 +63,9 @@ export async function validateFrontendCaseContent(input) {
|
|
|
69
63
|
if (PLACEHOLDER.test(body))
|
|
70
64
|
findings.push({ ruleId: "placeholder-wording", caseId: id, detail: "case contains placeholder or non-assertable wording" });
|
|
71
65
|
for (const [key, names] of Object.entries(SECTION_ALIASES))
|
|
72
|
-
if (!
|
|
66
|
+
if (!markdownSection(body, names))
|
|
73
67
|
findings.push({ ruleId: `missing-${key}`, caseId: id, detail: `missing section: ${names.join(" or ")}` });
|
|
74
|
-
const steps =
|
|
68
|
+
const steps = markdownSection(body, SECTION_ALIASES.steps), expected = markdownSection(body, SECTION_ALIASES.expected);
|
|
75
69
|
if (!hasList(steps))
|
|
76
70
|
findings.push({ ruleId: "unstructured-steps", caseId: id, detail: "steps should contain numbered or bulleted executable actions" });
|
|
77
71
|
if (!hasList(expected) || !hasAssertion(expected))
|
|
@@ -97,7 +91,8 @@ function render(findings) {
|
|
|
97
91
|
}
|
|
98
92
|
export async function writeFrontendCaseQualityReports(input) {
|
|
99
93
|
const result = await validateFrontendCaseContent(input);
|
|
100
|
-
const
|
|
94
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
95
|
+
const dir = path.join(input.workspaceRoot, input.outputDir ?? layout.reportsDir);
|
|
101
96
|
await mkdir(dir, { recursive: true });
|
|
102
97
|
await writeFile(path.join(dir, "frontend-test-case-quality-advisory.md"), result.markdown, "utf8");
|
|
103
98
|
await writeFile(path.join(dir, "frontend-test-case-quality-advisory.html"), result.html, "utf8");
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { defaultFrontendTestLayout, frontendTestContextRel, frontendTestProbeRel, } from "./frontend-test-layout.js";
|
|
5
|
+
export const FRONTEND_TEST_CONTEXT_REL = "testcase/frontend/rag/context.md";
|
|
6
|
+
export const FRONTEND_TEST_PROBE_REL = "testcase/frontend/rag/environment-probe.json";
|
|
7
|
+
const CONNECTION_REFUSED_HINT = "被测服务未在 baseUrl 监听(connection refused)。请先启动本地前端(例如 scripts/serve.sh 或项目约定的 npm start),确认可访问后再从 materialize-frontend-test-execution-shell 续跑。";
|
|
8
|
+
export function classifyFrontendBaseUrlProbeFailure(input) {
|
|
9
|
+
if (input.spawnError)
|
|
10
|
+
return { errorClass: "spawn-error" };
|
|
11
|
+
const exit = input.curlExit ?? 0;
|
|
12
|
+
if (exit === 7) {
|
|
13
|
+
return { errorClass: "connection-refused", hint: CONNECTION_REFUSED_HINT };
|
|
14
|
+
}
|
|
15
|
+
if (exit === 6) {
|
|
16
|
+
return {
|
|
17
|
+
errorClass: "dns-unresolved",
|
|
18
|
+
hint: "无法解析 baseUrl 主机名。确认 context.md 中的地址可在本机解析,或改用 127.0.0.1。",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (exit === 28) {
|
|
22
|
+
return {
|
|
23
|
+
errorClass: "connect-timeout",
|
|
24
|
+
hint: "连接 baseUrl 超时。确认被测服务已启动且端口可访问。",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (exit !== 0)
|
|
28
|
+
return { errorClass: `curl-exit-${exit}` };
|
|
29
|
+
if (input.httpStatus && Number.isFinite(input.httpStatus)) {
|
|
30
|
+
return { errorClass: `http-${input.httpStatus}` };
|
|
31
|
+
}
|
|
32
|
+
return { errorClass: "unreachable" };
|
|
33
|
+
}
|
|
34
|
+
function defaultCurlRunner(args) {
|
|
35
|
+
const result = spawnSync("curl", [...args], { encoding: "utf8" });
|
|
36
|
+
return {
|
|
37
|
+
status: result.status,
|
|
38
|
+
stdout: result.stdout ?? "",
|
|
39
|
+
stderr: result.stderr ?? "",
|
|
40
|
+
error: result.error,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function redactUrl(value) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(value);
|
|
46
|
+
parsed.username = "";
|
|
47
|
+
parsed.password = "";
|
|
48
|
+
parsed.search = "";
|
|
49
|
+
return parsed.toString();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return value.replace(/\/\/[^@\s]+@/g, "//");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function resolveBaseUrl(context) {
|
|
56
|
+
const patterns = [
|
|
57
|
+
/baseUrl\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
58
|
+
/base[-_ ]url\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
59
|
+
/playwright-cli open --browser=chrome\s+((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
60
|
+
/(https?:\/\/(?:localhost|127\.0\.0\.1)[^\s)}\],"']*)/i,
|
|
61
|
+
];
|
|
62
|
+
for (const pattern of patterns) {
|
|
63
|
+
const match = context.match(pattern);
|
|
64
|
+
if (match?.[1])
|
|
65
|
+
return match[1].replace(/[)}\],"'.`]+$/g, "");
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
async function writeProbe(workspaceRoot, probe, layout) {
|
|
70
|
+
const probePath = path.join(workspaceRoot, frontendTestProbeRel(layout));
|
|
71
|
+
const contextPath = path.join(workspaceRoot, frontendTestContextRel(layout));
|
|
72
|
+
await mkdir(path.dirname(probePath), { recursive: true });
|
|
73
|
+
await writeFile(probePath, `${JSON.stringify(probe, null, 2)}\n`, "utf8");
|
|
74
|
+
let context = "";
|
|
75
|
+
try {
|
|
76
|
+
context = await readFile(contextPath, "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
context = "";
|
|
80
|
+
}
|
|
81
|
+
const line = `environmentProbe: ${probe.status}${probe.blockedReason ? ` (${probe.blockedReason})` : ""}`;
|
|
82
|
+
const next = /environmentProbe\s*[:=]/i.test(context)
|
|
83
|
+
? context.replace(/environmentProbe\s*[:=]\s*.*/i, line)
|
|
84
|
+
: `${context.trimEnd()}\n\n${line}\n`;
|
|
85
|
+
await writeFile(contextPath, next, "utf8");
|
|
86
|
+
}
|
|
87
|
+
export async function probeFrontendTestEnvironment(input) {
|
|
88
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
89
|
+
const curl = input.curl ?? defaultCurlRunner;
|
|
90
|
+
const contextRel = frontendTestContextRel(layout);
|
|
91
|
+
const contextPath = path.join(input.workspaceRoot, contextRel);
|
|
92
|
+
let context;
|
|
93
|
+
try {
|
|
94
|
+
context = await readFile(contextPath, "utf8");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return { ok: false, stderr: `missing ${contextRel}` };
|
|
98
|
+
}
|
|
99
|
+
const rawUrl = resolveBaseUrl(context);
|
|
100
|
+
if (!rawUrl) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
stderr: "frontend-test preflight missing absolute baseUrl from context.md",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = new URL(rawUrl);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { ok: false, stderr: `baseUrl must be absolute http(s): ${rawUrl}` };
|
|
112
|
+
}
|
|
113
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
114
|
+
parsed.username ||
|
|
115
|
+
parsed.password ||
|
|
116
|
+
parsed.search ||
|
|
117
|
+
parsed.hash) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
stderr: `unsafe baseUrl from context.md: ${redactUrl(rawUrl)}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (/(?:^|\.)(?:www\.)?[^.]*?(?:prod|production)/i.test(parsed.hostname)) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
stderr: `production URL forbidden: ${redactUrl(rawUrl)}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const baseUrl = parsed.toString();
|
|
130
|
+
const safe = redactUrl(baseUrl);
|
|
131
|
+
const sourceMatch = context.match(/baseUrlSource\s*[:=]\s*([^\r\n]+)/i);
|
|
132
|
+
const baseUrlSource = sourceMatch ? sourceMatch[1].trim() : "context.md";
|
|
133
|
+
const failBlocked = async (reason, extra) => {
|
|
134
|
+
const probe = {
|
|
135
|
+
status: "unreachable",
|
|
136
|
+
blockedReason: reason,
|
|
137
|
+
baseUrlRedacted: extra.baseUrlRedacted ?? safe,
|
|
138
|
+
httpStatus: extra.httpStatus ?? null,
|
|
139
|
+
method: extra.method ?? null,
|
|
140
|
+
curlExit: extra.curlExit ?? null,
|
|
141
|
+
errorClass: extra.errorClass ?? null,
|
|
142
|
+
...(extra.hint ? { hint: extra.hint } : {}),
|
|
143
|
+
...(extra.baseUrlSource ? { baseUrlSource: extra.baseUrlSource } : {}),
|
|
144
|
+
};
|
|
145
|
+
await writeProbe(input.workspaceRoot, probe, layout);
|
|
146
|
+
const payload = {
|
|
147
|
+
blockedReason: reason,
|
|
148
|
+
baseUrl: probe.baseUrlRedacted,
|
|
149
|
+
httpStatus: probe.httpStatus,
|
|
150
|
+
errorClass: probe.errorClass,
|
|
151
|
+
...(probe.hint ? { hint: probe.hint } : {}),
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
stderr: `frontend-test preflight blocked: ${JSON.stringify(payload)}`,
|
|
156
|
+
probe,
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
const curlCheck = curl(["--version"]);
|
|
160
|
+
if (curlCheck.error || curlCheck.status !== 0) {
|
|
161
|
+
return failBlocked("curl-unavailable", {
|
|
162
|
+
baseUrlRedacted: safe,
|
|
163
|
+
errorClass: "curl-missing",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const probeMethod = (method) => curl([
|
|
167
|
+
"-sS",
|
|
168
|
+
"-o",
|
|
169
|
+
"/dev/null",
|
|
170
|
+
"-w",
|
|
171
|
+
"%{http_code}",
|
|
172
|
+
"--connect-timeout",
|
|
173
|
+
"3",
|
|
174
|
+
"--max-time",
|
|
175
|
+
"8",
|
|
176
|
+
"-X",
|
|
177
|
+
method,
|
|
178
|
+
"-L",
|
|
179
|
+
"--max-redirs",
|
|
180
|
+
"3",
|
|
181
|
+
"--http1.1",
|
|
182
|
+
"--proto-redir",
|
|
183
|
+
"=http,https",
|
|
184
|
+
safe,
|
|
185
|
+
]);
|
|
186
|
+
let used = "HEAD";
|
|
187
|
+
let result = probeMethod("HEAD");
|
|
188
|
+
let code = String(result.stdout || "").trim();
|
|
189
|
+
let statusNum = Number.parseInt(code, 10);
|
|
190
|
+
const headRejected = result.status !== 0 || !statusNum || statusNum === 405 || statusNum === 501;
|
|
191
|
+
if (headRejected) {
|
|
192
|
+
used = "GET";
|
|
193
|
+
result = probeMethod("GET");
|
|
194
|
+
code = String(result.stdout || "").trim();
|
|
195
|
+
statusNum = Number.parseInt(code, 10);
|
|
196
|
+
}
|
|
197
|
+
const ok = statusNum >= 200 && statusNum < 400;
|
|
198
|
+
if (!ok) {
|
|
199
|
+
const classified = classifyFrontendBaseUrlProbeFailure({
|
|
200
|
+
spawnError: Boolean(result.error),
|
|
201
|
+
curlExit: result.status,
|
|
202
|
+
httpStatus: Number.isFinite(statusNum) ? statusNum : null,
|
|
203
|
+
});
|
|
204
|
+
return failBlocked("frontend-base-url-unreachable", {
|
|
205
|
+
baseUrlRedacted: safe,
|
|
206
|
+
httpStatus: Number.isFinite(statusNum) ? statusNum : null,
|
|
207
|
+
method: used,
|
|
208
|
+
curlExit: result.status,
|
|
209
|
+
errorClass: classified.errorClass,
|
|
210
|
+
hint: classified.hint,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const probe = {
|
|
214
|
+
status: "reachable",
|
|
215
|
+
blockedReason: null,
|
|
216
|
+
baseUrl: safe,
|
|
217
|
+
baseUrlRedacted: safe,
|
|
218
|
+
baseUrlSource,
|
|
219
|
+
httpStatus: statusNum,
|
|
220
|
+
method: used,
|
|
221
|
+
curlExit: result.status,
|
|
222
|
+
errorClass: null,
|
|
223
|
+
};
|
|
224
|
+
await writeProbe(input.workspaceRoot, probe, layout);
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
stdout: `frontend-test-execution-v1 validated contextBaseUrl=${safe} source=${baseUrlSource} probe=reachable method=${used} httpStatus=${statusNum}`,
|
|
228
|
+
probe,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { frontendTestResultContractSchema } from "./frontend-test-result-contract.js";
|
|
4
|
+
import { defaultFrontendTestLayout, } from "./frontend-test-layout.js";
|
|
4
5
|
function escapeHtml(value) {
|
|
5
6
|
return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6
7
|
}
|
|
@@ -80,7 +81,8 @@ async function writePairAtomic(markdownPath, markdown, htmlPath, html) {
|
|
|
80
81
|
export async function renderFrontendTestHtmlReport(input) {
|
|
81
82
|
const resultPath = path.join(input.runDir, "contracts", "frontend-test-result.json");
|
|
82
83
|
const result = frontendTestResultContractSchema.parse(JSON.parse(await readFile(resultPath, "utf8")));
|
|
83
|
-
const
|
|
84
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
85
|
+
const outputDir = path.join(input.workspaceRoot, layout.reportsDir);
|
|
84
86
|
const markdownPath = path.join(outputDir, "frontend-test-report.md");
|
|
85
87
|
const htmlPath = path.join(outputDir, "frontend-test-report.html");
|
|
86
88
|
const outcomeLabel = result.outcome === "passed" ? "测试通过" : result.outcome === "failed" ? "测试失败" : "测试未完成";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { frontendTestResultContractSchema, } from "./frontend-test-result-contract.js";
|
|
4
|
+
import { defaultFrontendTestLayout, } from "./frontend-test-layout.js";
|
|
4
5
|
function ratioMetric(numerator, denominator, threshold, label) {
|
|
5
6
|
if (denominator === 0) {
|
|
6
7
|
return { numerator, denominator, ratio: null, threshold, status: "unavailable", reason: `${label}-denominator-is-zero` };
|
|
@@ -169,7 +170,8 @@ export async function renderFrontendTestL5Report(input) {
|
|
|
169
170
|
const resultPath = path.join(input.runDir, "contracts", "frontend-test-result.json");
|
|
170
171
|
const result = frontendTestResultContractSchema.parse(JSON.parse(await readFile(resultPath, "utf8")));
|
|
171
172
|
const metrics = computeFrontendL5ReportMetrics(result);
|
|
172
|
-
const
|
|
173
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
174
|
+
const outputDir = path.join(input.workspaceRoot, layout.reportsDir);
|
|
173
175
|
const markdownPath = path.join(outputDir, "frontend-test-l5-dashboard.md");
|
|
174
176
|
const htmlPath = path.join(outputDir, "frontend-test-l5-dashboard.html");
|
|
175
177
|
await writePairAtomic(markdownPath, renderMarkdown(result, metrics), htmlPath, renderHtml(result, metrics));
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Frontend-test artifact layout (configurable testRoot).
|
|
4
|
+
*
|
|
5
|
+
* Resolved once from `task.frontendTest.testRoot` at DAG compile time and
|
|
6
|
+
* frozen on the spec for the whole run. Writers, gates, Playwright fences
|
|
7
|
+
* and TaskSpec allowedPaths all derive from this single object.
|
|
8
|
+
*
|
|
9
|
+
* Default layout keeps the historical frozen contract:
|
|
10
|
+
* testcase/frontend/rag/**
|
|
11
|
+
* testcase/frontend/cases/**
|
|
12
|
+
* testcase/frontend/evidence/**
|
|
13
|
+
* testcase/frontend/reports/**
|
|
14
|
+
* testcase/frontend/fixtures/**
|
|
15
|
+
*/
|
|
16
|
+
export const FRONTEND_TEST_DEFAULT_TEST_ROOT = "testcase/frontend";
|
|
17
|
+
const FORBIDDEN_TEST_ROOT_PREFIXES = [
|
|
18
|
+
"src",
|
|
19
|
+
"app",
|
|
20
|
+
"lib",
|
|
21
|
+
"apps",
|
|
22
|
+
"packages",
|
|
23
|
+
".harness",
|
|
24
|
+
".git",
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
"docs",
|
|
29
|
+
"website",
|
|
30
|
+
"scripts",
|
|
31
|
+
"test",
|
|
32
|
+
"smoke",
|
|
33
|
+
"knowledge",
|
|
34
|
+
"features",
|
|
35
|
+
"dogfood",
|
|
36
|
+
"examples",
|
|
37
|
+
"bin",
|
|
38
|
+
];
|
|
39
|
+
function normalizeRelPosix(value, label) {
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
throw new Error(`frontendTest.${label} must not be empty`);
|
|
43
|
+
if (trimmed.includes("\\")) {
|
|
44
|
+
throw new Error(`frontendTest.${label} must use forward slashes: ${value}`);
|
|
45
|
+
}
|
|
46
|
+
if (path.isAbsolute(trimmed) || /^[A-Za-z]:/.test(trimmed)) {
|
|
47
|
+
throw new Error(`frontendTest.${label} must be a repository-relative path: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
const segments = trimmed.split("/").filter((segment) => segment.length > 0);
|
|
50
|
+
if (segments.some((segment) => segment === ".." || segment === ".")) {
|
|
51
|
+
throw new Error(`frontendTest.${label} must not contain "." or ".." segments: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
if (segments.some((segment) => segment.startsWith("."))) {
|
|
54
|
+
throw new Error(`frontendTest.${label} must not target hidden directories: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
if (segments.some((segment) => !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(segment))) {
|
|
57
|
+
throw new Error(`frontendTest.${label} contains unsafe path segments: ${value}`);
|
|
58
|
+
}
|
|
59
|
+
return segments.join("/");
|
|
60
|
+
}
|
|
61
|
+
function assertTestRootAllowed(testRoot) {
|
|
62
|
+
const topSegment = testRoot.split("/")[0];
|
|
63
|
+
if (FORBIDDEN_TEST_ROOT_PREFIXES.some((forbidden) => topSegment === forbidden || topSegment.startsWith(`${forbidden}.`))) {
|
|
64
|
+
throw new Error(`frontendTest.testRoot must not target production/governance trees: ${testRoot}`);
|
|
65
|
+
}
|
|
66
|
+
if (topSegment.startsWith(".")) {
|
|
67
|
+
throw new Error(`frontendTest.testRoot must not target hidden directories: ${testRoot}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the frozen frontend-test artifact layout from task config.
|
|
72
|
+
* Throws on unsafe or ambiguous configuration (fail-closed).
|
|
73
|
+
*/
|
|
74
|
+
export function resolveFrontendTestLayout(config) {
|
|
75
|
+
const testRoot = config?.testRoot === undefined || config?.testRoot === null
|
|
76
|
+
? FRONTEND_TEST_DEFAULT_TEST_ROOT
|
|
77
|
+
: normalizeRelPosix(config.testRoot, "testRoot");
|
|
78
|
+
assertTestRootAllowed(testRoot);
|
|
79
|
+
const isDefault = testRoot === FRONTEND_TEST_DEFAULT_TEST_ROOT;
|
|
80
|
+
return {
|
|
81
|
+
testRoot,
|
|
82
|
+
ragDir: `${testRoot}/rag`,
|
|
83
|
+
casesDir: `${testRoot}/cases`,
|
|
84
|
+
evidenceDir: `${testRoot}/evidence`,
|
|
85
|
+
reportsDir: `${testRoot}/reports`,
|
|
86
|
+
fixturesDir: `${testRoot}/fixtures`,
|
|
87
|
+
isDefault,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Historical default layout. Old DAGs without frontendTestLayout use this. */
|
|
91
|
+
export function defaultFrontendTestLayout() {
|
|
92
|
+
return resolveFrontendTestLayout(undefined);
|
|
93
|
+
}
|
|
94
|
+
export function frontendTestLayoutFromSpec(spec) {
|
|
95
|
+
const frozen = spec?.frontendTestLayout;
|
|
96
|
+
if (!frozen)
|
|
97
|
+
return defaultFrontendTestLayout();
|
|
98
|
+
return resolveFrontendTestLayout({ testRoot: frozen.testRoot });
|
|
99
|
+
}
|
|
100
|
+
const DEFAULT_PREFIX = `${FRONTEND_TEST_DEFAULT_TEST_ROOT}/`;
|
|
101
|
+
/**
|
|
102
|
+
* Longest-prefix rewrite of historical `testcase/frontend/` paths.
|
|
103
|
+
* No-op when layout.isDefault so default DAGs stay byte-identical.
|
|
104
|
+
* Never replaces the short token `testcase/` (would collide with backend-test).
|
|
105
|
+
*/
|
|
106
|
+
export function applyFrontendTestLayoutToText(text, layout) {
|
|
107
|
+
if (layout.isDefault)
|
|
108
|
+
return text;
|
|
109
|
+
const replacement = `${layout.testRoot}/`;
|
|
110
|
+
let output = "";
|
|
111
|
+
for (let i = 0; i < text.length;) {
|
|
112
|
+
if (text.startsWith(replacement, i)) {
|
|
113
|
+
output += replacement;
|
|
114
|
+
i += replacement.length;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (text.startsWith(DEFAULT_PREFIX, i)) {
|
|
118
|
+
output += replacement;
|
|
119
|
+
i += DEFAULT_PREFIX.length;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
output += text[i];
|
|
123
|
+
i += 1;
|
|
124
|
+
}
|
|
125
|
+
return output;
|
|
126
|
+
}
|
|
127
|
+
export function frontendTestCasePath(layout, caseId) {
|
|
128
|
+
return `${layout.casesDir}/${caseId}.md`;
|
|
129
|
+
}
|
|
130
|
+
export function frontendTestEvidenceDir(layout, caseId) {
|
|
131
|
+
return `${layout.evidenceDir}/${caseId}/`;
|
|
132
|
+
}
|
|
133
|
+
export function frontendTestEvidencePrefix(layout, caseId) {
|
|
134
|
+
return `${layout.evidenceDir}/${caseId}`;
|
|
135
|
+
}
|
|
136
|
+
export function frontendTestManifestPath(layout) {
|
|
137
|
+
return `${layout.casesDir}/manifest.json`;
|
|
138
|
+
}
|
|
139
|
+
export function frontendTestManifestDraftPath(layout) {
|
|
140
|
+
return `${layout.casesDir}/manifest.draft.json`;
|
|
141
|
+
}
|
|
142
|
+
export function frontendTestContextRel(layout) {
|
|
143
|
+
return `${layout.ragDir}/context.md`;
|
|
144
|
+
}
|
|
145
|
+
export function frontendTestCoverageMapRel(layout) {
|
|
146
|
+
return `${layout.ragDir}/coverage-map.md`;
|
|
147
|
+
}
|
|
148
|
+
export function frontendTestProbeRel(layout) {
|
|
149
|
+
return `${layout.ragDir}/environment-probe.json`;
|
|
150
|
+
}
|
|
151
|
+
export function frontendTestStandardScenariosRel(layout) {
|
|
152
|
+
return `${layout.ragDir}/standard-scenarios.v1.json`;
|
|
153
|
+
}
|
|
154
|
+
export function frontendTestUiAnchorsRel(layout) {
|
|
155
|
+
return `${layout.ragDir}/ui-anchors.md`;
|
|
156
|
+
}
|
|
157
|
+
export function escapeRegexLiteral(value) {
|
|
158
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
}
|