@tea-agent/loop-agent 0.39.0-next.9 → 0.39.0
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 +262 -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 +4 -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.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 +554 -33
- 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 +28 -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 +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +28 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/worker/cli.js +31 -1
- 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/dag-execution-receipt.js +33 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operator-actions.js +195 -4
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/pi-readiness.js +5 -4
- 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 +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +42 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-Br92EGb7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-oulImtQq.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-tyHc6aqO.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-CY86BSpT.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-BKKsDmNs.js} +1 -1
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-CXZdjl3B.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-B-aIFDQ5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-BDgAW_B2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-DABa4UPR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-aGbysVOx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-B8MSZDVs.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-CpelV73g.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-BE04ty3z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-BUxTkaaR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-CYYBVKfX.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-CEQF_xmQ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-CqpxN5IT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-B3PzanVi.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-BSUGCNx1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-CdLevU5T.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BjpF_Kok.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C4I6NJny.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-C8hecCr5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-Daod6tOS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-BvvmazGv.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-DeN_vfV4.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-D6dq1t9z.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-DziIdNY4.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js} +1 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +1 -0
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +406 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-Czv5eYx2.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-DxGmNj5K.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-CYCFqkO_.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-B0YzRwrd.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-t3EZtIGh.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-BTXpTv0T.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-DU8UkKqA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-DP4y8ohp.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-B07vNaYR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-xDWktb2i.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-BLNMKqeS.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-n0adalxH.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-BbnS-2DL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-B4uPpCLu.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA--MMbB0fX.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-BXUgp2QY.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-CsaMxki7.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-C2IuG9iH.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-CmEWOXCW.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-D_9LE0MG.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -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/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/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- 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 +23 -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/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -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/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
- 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 +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- 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 +100 -56
- 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 +377 -4
- package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
- package/dist/workflows/dag/frontend-repair.js +14 -0
- 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 +894 -205
- package/dist/workflows/dag/node-execution.js +183 -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 +143 -1
- package/dist/workflows/dag/validate.js +64 -20
- package/docs/init-surface.manifest.json +5 -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 +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- 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/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
|
@@ -15,6 +15,8 @@ import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend
|
|
|
15
15
|
import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
|
|
16
16
|
import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
|
|
17
17
|
import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
18
|
+
import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
|
|
19
|
+
import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
|
|
18
20
|
import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
|
|
19
21
|
import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
|
|
20
22
|
import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
|
|
@@ -27,11 +29,14 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
|
|
|
27
29
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
28
30
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
29
31
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
32
|
+
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
33
|
+
import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
|
|
34
|
+
import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
|
|
30
35
|
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
31
36
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
32
|
-
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
37
|
+
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, markdownFiles, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
33
38
|
import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
|
|
34
|
-
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
39
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
35
40
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
36
41
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
37
42
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -48,6 +53,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
48
53
|
phase: "initial",
|
|
49
54
|
repairAttempt: 0,
|
|
50
55
|
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
56
|
+
layout: input.layout,
|
|
51
57
|
});
|
|
52
58
|
const initialArtifacts = await writeBackendScenarioParamArtifacts({
|
|
53
59
|
runDir: input.runDir,
|
|
@@ -73,6 +79,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
73
79
|
phase: "final",
|
|
74
80
|
repairAttempt: 1,
|
|
75
81
|
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
82
|
+
layout: input.layout,
|
|
76
83
|
});
|
|
77
84
|
finalFacts = reassessed.facts;
|
|
78
85
|
finalMarkdown = reassessed.markdown;
|
|
@@ -117,6 +124,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
|
|
|
117
124
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
118
125
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
119
126
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
127
|
+
const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
|
|
128
|
+
const TEST_TIMEOUT_MARKERS = [
|
|
129
|
+
/Test timed out in \d+ms/i,
|
|
130
|
+
/Timeout of \d+ms exceeded/i,
|
|
131
|
+
];
|
|
132
|
+
const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
|
|
133
|
+
/AssertionError/i,
|
|
134
|
+
/expected:?\s/i,
|
|
135
|
+
/received:?\s/i,
|
|
136
|
+
/Snapshot /i,
|
|
137
|
+
/toMatchSnapshot/i,
|
|
138
|
+
/SyntaxError/i,
|
|
139
|
+
/TS\d{4}/,
|
|
140
|
+
/error TS/i,
|
|
141
|
+
/eslint/i,
|
|
142
|
+
/compile error/i,
|
|
143
|
+
/Failed to compile/i,
|
|
144
|
+
];
|
|
145
|
+
function isIdentifiedTestCommand(command) {
|
|
146
|
+
return /(?:^|[\s"'`])(?:npx\s+)?vitest(?:\s|$)|(?:^|[\s"'`])npm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])pnpm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])yarn(?:\.cmd)?\s+test(?:\s|$)/i.test(command);
|
|
147
|
+
}
|
|
148
|
+
function classifyShellCommandFailure(input) {
|
|
149
|
+
if (input.processCategory !== "nonzero-exit")
|
|
150
|
+
return input.processCategory;
|
|
151
|
+
if (!isIdentifiedTestCommand(input.command))
|
|
152
|
+
return "nonzero-exit";
|
|
153
|
+
const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
|
|
154
|
+
const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
|
|
155
|
+
if (!hasTimeoutMarker)
|
|
156
|
+
return "nonzero-exit";
|
|
157
|
+
const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
|
|
158
|
+
return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
|
|
159
|
+
}
|
|
120
160
|
const NODE_STDOUT_MAX = 12_000;
|
|
121
161
|
const NODE_STDERR_MAX = 8_000;
|
|
122
162
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
@@ -323,16 +363,21 @@ export async function executeShellCommand(input) {
|
|
|
323
363
|
});
|
|
324
364
|
});
|
|
325
365
|
child.on("close", (code) => {
|
|
326
|
-
let
|
|
366
|
+
let processCategory = "nonzero-exit";
|
|
327
367
|
if (timedOut) {
|
|
328
|
-
|
|
368
|
+
processCategory = "timeout";
|
|
329
369
|
}
|
|
330
370
|
else if (code === 0) {
|
|
331
|
-
|
|
371
|
+
processCategory = "success";
|
|
332
372
|
}
|
|
333
373
|
finish({
|
|
334
374
|
exitCode: code,
|
|
335
|
-
failureCategory
|
|
375
|
+
failureCategory: classifyShellCommandFailure({
|
|
376
|
+
command: input.command,
|
|
377
|
+
processCategory,
|
|
378
|
+
stdout: formatBoundedOutput(stdout),
|
|
379
|
+
stderr: formatBoundedOutput(stderr),
|
|
380
|
+
}),
|
|
336
381
|
ok: code === 0 && !timedOut,
|
|
337
382
|
});
|
|
338
383
|
});
|
|
@@ -492,11 +537,114 @@ async function readAdvisoryRunReport(reportsDir, filename, title) {
|
|
|
492
537
|
async function executeBackendTestPipeline(input, meta) {
|
|
493
538
|
const pipeline = input.task.shell?.backendTestPipeline;
|
|
494
539
|
const started = Date.now();
|
|
540
|
+
// Plan A: prefer the frozen layout carried by the compiled spec; fall back
|
|
541
|
+
// to the historical default for hand-written legacy DAGs.
|
|
542
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
543
|
+
? {
|
|
544
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
545
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
546
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
547
|
+
}
|
|
548
|
+
: undefined);
|
|
495
549
|
try {
|
|
496
550
|
if (!pipeline)
|
|
497
551
|
throw new Error("missing backend-test pipeline id");
|
|
498
552
|
const outputs = [];
|
|
499
|
-
if (pipeline === "
|
|
553
|
+
if (pipeline === "ingest-backend-test-gap") {
|
|
554
|
+
// Plan D: deterministic gap ingest. Identity (taskId + requirement
|
|
555
|
+
// sha256) is fail-closed; a mismatched plan blocks every writer.
|
|
556
|
+
const gapDocRel = meta.spec.globalConstraints
|
|
557
|
+
?.map((item) => /^backendTestGapDoc=(.+)$/i.exec(item.trim())?.[1])
|
|
558
|
+
.find(Boolean);
|
|
559
|
+
const binding = meta.spec.sourceBinding;
|
|
560
|
+
const requirementSha256 = binding?.sources.find((s) => s.kind === "requirement")?.sha256 ?? "";
|
|
561
|
+
if (!gapDocRel || !binding || !requirementSha256) {
|
|
562
|
+
return {
|
|
563
|
+
ok: false,
|
|
564
|
+
stdout: outputs.join("\n"),
|
|
565
|
+
stderr: "backend-test gap ingest requires the bound gap document path and the task source binding with a requirement sha256",
|
|
566
|
+
failureCategory: "invalid-output",
|
|
567
|
+
durationMs: Date.now() - started,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const gapDocAbs = path.join(input.cwd, ...gapDocRel.split("/"));
|
|
571
|
+
let parsed;
|
|
572
|
+
try {
|
|
573
|
+
parsed = JSON.parse(await readFile(gapDocAbs, "utf8"));
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
return {
|
|
577
|
+
ok: false,
|
|
578
|
+
stdout: outputs.join("\n"),
|
|
579
|
+
stderr: `backend-test gap document is unreadable JSON at ${gapDocRel}: ${error instanceof Error ? error.message : String(error)}`,
|
|
580
|
+
failureCategory: "invalid-output",
|
|
581
|
+
durationMs: Date.now() - started,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const doc = backendTestGapDocSchema.safeParse(parsed);
|
|
585
|
+
if (!doc.success) {
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
stdout: outputs.join("\n"),
|
|
589
|
+
stderr: `invalid backend-test-gap-v1 document: ${doc.error.issues
|
|
590
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
591
|
+
.join("; ")}`,
|
|
592
|
+
failureCategory: "invalid-output",
|
|
593
|
+
durationMs: Date.now() - started,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// previous N6 facts (may be absent on a fresh checkout)
|
|
597
|
+
const factsPath = path.join(input.cwd, "contracts", "backend-test-case-coverage-facts.json");
|
|
598
|
+
let previousMissingSlots;
|
|
599
|
+
let previousCoveredSlots;
|
|
600
|
+
try {
|
|
601
|
+
const facts = JSON.parse(await readFile(factsPath, "utf8"));
|
|
602
|
+
previousMissingSlots = facts.scenarioPartitions?.missingSlots;
|
|
603
|
+
previousCoveredSlots = (facts.rules ?? []).flatMap((rule) => rule.coveredTestPoints ?? []);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// no previous facts in the workspace: the gap doc alone drives the plan
|
|
607
|
+
}
|
|
608
|
+
// module index comes from the README on disk
|
|
609
|
+
let existingModuleStems = [];
|
|
610
|
+
try {
|
|
611
|
+
const files = await markdownFiles(input.cwd, backendLayout);
|
|
612
|
+
existingModuleStems = files
|
|
613
|
+
.map((file) => path.basename(file, ".md"))
|
|
614
|
+
.filter((stem) => stem.toLowerCase() !== "readme");
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// no markdown tree: every slot becomes a conflict
|
|
618
|
+
}
|
|
619
|
+
const plan = ingestBackendTestGap({
|
|
620
|
+
gapDoc: doc.data,
|
|
621
|
+
previousMissingSlots,
|
|
622
|
+
previousCoveredSlots,
|
|
623
|
+
existingModuleStems,
|
|
624
|
+
sharedSetupPath: meta.spec.backendTestSharedSetup?.path ?? null,
|
|
625
|
+
layout: backendLayout,
|
|
626
|
+
taskId: binding.taskId,
|
|
627
|
+
requirementSha256,
|
|
628
|
+
});
|
|
629
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
630
|
+
await mkdir(contractsDir, { recursive: true });
|
|
631
|
+
const planPath = path.join(contractsDir, "backend-test-gap-plan-v1.json");
|
|
632
|
+
await writeFile(planPath, JSON.stringify(plan, null, 2), "utf8");
|
|
633
|
+
outputs.push(`gapPlan=${planPath}`, `gapPlanStatus=${plan.status}`);
|
|
634
|
+
if (plan.status === "error") {
|
|
635
|
+
return {
|
|
636
|
+
ok: false,
|
|
637
|
+
stdout: outputs.join("\n"),
|
|
638
|
+
stderr: [
|
|
639
|
+
"backend-test gap ingest failed closed (identity/conflicts):",
|
|
640
|
+
...plan.conflicts,
|
|
641
|
+
].join("\n"),
|
|
642
|
+
failureCategory: "invalid-output",
|
|
643
|
+
durationMs: Date.now() - started,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
else if (pipeline === "markdown-environment") {
|
|
500
648
|
const results = await executePipelineCommands(input, meta);
|
|
501
649
|
if (!results.every((result) => result.ok)) {
|
|
502
650
|
const failure = results.find((result) => !result.ok);
|
|
@@ -530,6 +678,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
530
678
|
environmentMarkdown,
|
|
531
679
|
requiredRequirementIds,
|
|
532
680
|
sourceBinding: meta.spec.sourceBinding,
|
|
681
|
+
layout: backendLayout,
|
|
682
|
+
sharedSetup: meta.spec.backendTestSharedSetup ?? null,
|
|
533
683
|
});
|
|
534
684
|
}
|
|
535
685
|
catch (error) {
|
|
@@ -583,6 +733,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
583
733
|
const coverage = await analyzeBackendTestCaseCoverage({
|
|
584
734
|
workspaceRoot: input.cwd,
|
|
585
735
|
sourceBinding: normalizedBinding,
|
|
736
|
+
layout: backendLayout,
|
|
586
737
|
});
|
|
587
738
|
const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
|
|
588
739
|
const contractsDir = path.join(meta.runDir, "contracts");
|
|
@@ -590,6 +741,23 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
590
741
|
const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
|
|
591
742
|
await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
|
|
592
743
|
outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
|
|
744
|
+
// Plan C: scenario-partition slot coverage is fail-closed. General
|
|
745
|
+
// coverage findings stay advisory, but a declared filter axis missing
|
|
746
|
+
// an each-value/omitted slot or its complement slot must block pytest
|
|
747
|
+
// generation until the Markdown declares the slot.
|
|
748
|
+
const partitionBlocking = coverage.facts.findings.filter((finding) => /^(?:MISSING_PARTITION|MISSING_PARTITION_SLOT|MISSING_COMPLEMENT_SLOT|MISSING_PARTITION_TABLE|INVALID_PARTITION_DOMAIN|NO_FINITE_DOMAIN|PARTITION_SOURCE_MISMATCH)/.test(finding));
|
|
749
|
+
if (partitionBlocking.length > 0) {
|
|
750
|
+
return {
|
|
751
|
+
ok: false,
|
|
752
|
+
stdout: outputs.join("\n\n"),
|
|
753
|
+
stderr: [
|
|
754
|
+
"backend-test scenario partitions are incomplete (fail-closed):",
|
|
755
|
+
...partitionBlocking,
|
|
756
|
+
].join("\n"),
|
|
757
|
+
failureCategory: "invalid-output",
|
|
758
|
+
durationMs: Date.now() - started,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
593
761
|
}
|
|
594
762
|
catch (error) {
|
|
595
763
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -606,9 +774,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
606
774
|
}
|
|
607
775
|
}
|
|
608
776
|
else if (pipeline === "markdown-collection-assess") {
|
|
609
|
-
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
|
|
777
|
+
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
610
778
|
let facts;
|
|
611
|
-
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
779
|
+
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
|
|
612
780
|
const priorityOnlyFacts = assessPriorityOnlyBackendPytestModules({
|
|
613
781
|
phase: "initial",
|
|
614
782
|
mappedScripts: resolution.expectedScripts,
|
|
@@ -618,7 +786,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
618
786
|
facts = priorityOnlyFacts;
|
|
619
787
|
}
|
|
620
788
|
else if (resolution.missingScripts.length > 0) {
|
|
621
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
789
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
|
|
622
790
|
facts = assessMissingBackendPytestScripts({
|
|
623
791
|
mappedScripts: resolution.expectedScripts,
|
|
624
792
|
existingMappedScripts: resolution.existingScripts,
|
|
@@ -632,10 +800,11 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
632
800
|
const scenario = await runBackendScenarioParamPreflight({
|
|
633
801
|
workspaceRoot: input.cwd,
|
|
634
802
|
runDir: meta.runDir,
|
|
803
|
+
layout: backendLayout,
|
|
635
804
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
636
805
|
});
|
|
637
806
|
outputs.push(...scenario.outputs);
|
|
638
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
807
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
|
|
639
808
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
640
809
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
641
810
|
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
@@ -669,6 +838,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
669
838
|
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
670
839
|
}
|
|
671
840
|
facts = assessBackendPytestCollection({
|
|
841
|
+
layout: backendLayout,
|
|
672
842
|
phase: "initial",
|
|
673
843
|
mappedScripts,
|
|
674
844
|
inventory,
|
|
@@ -678,6 +848,43 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
678
848
|
fixtureResolution,
|
|
679
849
|
});
|
|
680
850
|
}
|
|
851
|
+
try {
|
|
852
|
+
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
853
|
+
workspaceRoot: input.cwd,
|
|
854
|
+
layout: backendLayout,
|
|
855
|
+
taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
|
|
856
|
+
});
|
|
857
|
+
const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
|
|
858
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
859
|
+
await mkdir(contractsDir, { recursive: true });
|
|
860
|
+
const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
|
|
861
|
+
await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
|
|
862
|
+
outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
|
|
863
|
+
if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
|
|
864
|
+
const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
|
|
865
|
+
.filter((entry) => entry.status !== "EXACT_1_TO_1")
|
|
866
|
+
.flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
|
|
867
|
+
.filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
|
|
868
|
+
if (correspondenceRepairPaths.length > 0) {
|
|
869
|
+
facts.status = "REPAIRABLE";
|
|
870
|
+
facts.repairEligible = true;
|
|
871
|
+
facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
|
|
872
|
+
facts.findings.push({
|
|
873
|
+
kind: "markdown-pytest-correspondence",
|
|
874
|
+
classification: "test-asset-defect",
|
|
875
|
+
repairability: "repairable",
|
|
876
|
+
detail: correspondence.facts.entries
|
|
877
|
+
.filter((entry) => entry.status !== "EXACT_1_TO_1")
|
|
878
|
+
.flatMap((entry) => entry.findings)
|
|
879
|
+
.join("; ")
|
|
880
|
+
.slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
|
|
887
|
+
}
|
|
681
888
|
const artifacts = await writeBackendPytestCollectionArtifacts({
|
|
682
889
|
runDir: meta.runDir,
|
|
683
890
|
stem: "initial",
|
|
@@ -711,14 +918,15 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
711
918
|
});
|
|
712
919
|
}
|
|
713
920
|
else if (initial.status === "REPAIRABLE") {
|
|
714
|
-
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
921
|
+
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
715
922
|
const scenario = await runBackendScenarioParamPreflight({
|
|
716
923
|
workspaceRoot: input.cwd,
|
|
717
924
|
runDir: meta.runDir,
|
|
925
|
+
layout: backendLayout,
|
|
718
926
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
719
927
|
});
|
|
720
928
|
outputs.push(...scenario.outputs);
|
|
721
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
929
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
|
|
722
930
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
723
931
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
724
932
|
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
@@ -753,6 +961,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
753
961
|
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
754
962
|
}
|
|
755
963
|
const finalFacts = assessBackendPytestCollection({
|
|
964
|
+
layout: backendLayout,
|
|
756
965
|
phase: "final",
|
|
757
966
|
mappedScripts,
|
|
758
967
|
inventory,
|
|
@@ -776,12 +985,57 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
776
985
|
facts: effective,
|
|
777
986
|
});
|
|
778
987
|
const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
|
|
988
|
+
const correspondence = meta.spec.sourceBinding
|
|
989
|
+
? await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
990
|
+
workspaceRoot: input.cwd,
|
|
991
|
+
layout: backendLayout,
|
|
992
|
+
taskId: meta.spec.sourceBinding.taskId,
|
|
993
|
+
})
|
|
994
|
+
: undefined;
|
|
995
|
+
const eligibility = correspondence
|
|
996
|
+
? buildBackendTestItemEligibility(effective.collectedItemIds, {
|
|
997
|
+
correspondenceEntries: correspondence.facts.entries,
|
|
998
|
+
scenarioEntries: scenarioFacts.entries,
|
|
999
|
+
})
|
|
1000
|
+
: { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
|
|
1001
|
+
const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
|
|
1002
|
+
const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
|
|
1003
|
+
await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
|
|
1004
|
+
await writeFile(eligibilityFactsPath, `${JSON.stringify({
|
|
1005
|
+
schemaId: "backend-test-execution-eligibility-v1",
|
|
1006
|
+
status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
|
|
1007
|
+
collectedItemCount: effective.collectedItemIds.length,
|
|
1008
|
+
eligibleItemCount: eligibility.eligibleItemIds.length,
|
|
1009
|
+
excludedItemCount: eligibility.excludedItems.length,
|
|
1010
|
+
eligibleItemIds: eligibility.eligibleItemIds,
|
|
1011
|
+
excludedItems: eligibility.excludedItems,
|
|
1012
|
+
inputHashes: eligibilityInputHashes,
|
|
1013
|
+
}, null, 2)}\n`, "utf8");
|
|
1014
|
+
const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
|
|
1015
|
+
"# Backend Test Execution Eligibility",
|
|
1016
|
+
"",
|
|
1017
|
+
"## Status",
|
|
1018
|
+
"",
|
|
1019
|
+
eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
|
|
1020
|
+
"",
|
|
1021
|
+
`- Collected items: ${effective.collectedItemIds.length}`,
|
|
1022
|
+
`- Eligible items: ${eligibility.eligibleItemIds.length}`,
|
|
1023
|
+
`- Excluded items: ${eligibility.excludedItems.length}`,
|
|
1024
|
+
"",
|
|
1025
|
+
"## Excluded Items",
|
|
1026
|
+
"",
|
|
1027
|
+
...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
|
|
1028
|
+
"",
|
|
1029
|
+
].join("\n"));
|
|
1030
|
+
outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
|
|
779
1031
|
const readiness = await materializeBackendTestExecutionReadiness({
|
|
780
1032
|
runDir: meta.runDir,
|
|
781
1033
|
workspaceRoot: input.cwd,
|
|
782
1034
|
effective,
|
|
783
1035
|
scenarioParamStatus: scenarioFacts.overallStatus,
|
|
784
1036
|
scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
|
|
1037
|
+
eligibility,
|
|
1038
|
+
eligibilityInputHashes,
|
|
785
1039
|
});
|
|
786
1040
|
return {
|
|
787
1041
|
ok: true,
|
|
@@ -792,6 +1046,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
792
1046
|
collectedItemCount: effective.collectedItemCount,
|
|
793
1047
|
fixtureResolutionStatus: effective.fixtureResolutionStatus,
|
|
794
1048
|
executionReadinessStatus: readiness.status,
|
|
1049
|
+
eligibleItemCount: readiness.eligibleItemIds.length,
|
|
1050
|
+
excludedItemCount: readiness.excludedItems.length,
|
|
795
1051
|
factsPath: "contracts/backend-test-pytest-collection-effective.json",
|
|
796
1052
|
readinessPath: "contracts/backend-test-execution-readiness.json",
|
|
797
1053
|
reportPath: artifacts.reportPath,
|
|
@@ -826,6 +1082,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
826
1082
|
try {
|
|
827
1083
|
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
828
1084
|
workspaceRoot: input.cwd,
|
|
1085
|
+
layout: backendLayout,
|
|
829
1086
|
taskId: sourceBinding.taskId,
|
|
830
1087
|
});
|
|
831
1088
|
const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
|
|
@@ -888,9 +1145,32 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
888
1145
|
else if (pipeline === "markdown-execute-html") {
|
|
889
1146
|
const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
|
|
890
1147
|
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
|
|
891
|
-
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
892
|
-
|
|
893
|
-
|
|
1148
|
+
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
1149
|
+
let eligibleItemIds = executionReadiness.eligibleItemIds;
|
|
1150
|
+
// Plan D gap-fill executeScope=affected: when a gap plan exists in
|
|
1151
|
+
// this run, prefer the newly planned items (pytest node ids contain
|
|
1152
|
+
// the TP slot id) plus every collected item of the patched modules;
|
|
1153
|
+
// scope=all keeps the full eligible set.
|
|
1154
|
+
const affectedScope = meta.spec.globalConstraints?.some((item) => /^backendTestExecuteScope=affected$/i.test(item.trim())) ?? false;
|
|
1155
|
+
if (affectedScope) {
|
|
1156
|
+
try {
|
|
1157
|
+
const plan = JSON.parse(await readFile(path.join(meta.runDir, "contracts", "backend-test-gap-plan-v1.json"), "utf8"));
|
|
1158
|
+
const newPoints = new Set(plan.newTestPoints ?? []);
|
|
1159
|
+
const modules = new Set((plan.targetModules ?? []).map((stem) => `test_${stem}.py`));
|
|
1160
|
+
const affected = eligibleItemIds.filter((id) => modules.size === 0 ||
|
|
1161
|
+
[...modules].some((script) => id.includes(script)) ||
|
|
1162
|
+
[...newPoints].some((point) => id.includes(point)));
|
|
1163
|
+
if (affected.length > 0)
|
|
1164
|
+
eligibleItemIds = affected;
|
|
1165
|
+
}
|
|
1166
|
+
catch {
|
|
1167
|
+
// no gap plan in this run: execute the full eligible set
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
|
|
1171
|
+
const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
|
|
1172
|
+
await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
|
|
1173
|
+
const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
|
|
894
1174
|
// Split into short bash -c commands (aligned with markdown-environment).
|
|
895
1175
|
// A single ultra-long compound command has been observed on Windows to
|
|
896
1176
|
// exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
|
|
@@ -967,8 +1247,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
967
1247
|
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
968
1248
|
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
969
1249
|
'echo "STEP=pytest-run"',
|
|
970
|
-
`echo "- pytestCommand: python -m pytest
|
|
971
|
-
`
|
|
1250
|
+
`echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
|
|
1251
|
+
`echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
|
|
1252
|
+
`echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
|
|
1253
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargetsArg} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
|
|
972
1254
|
"STATUS=$?",
|
|
973
1255
|
'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
974
1256
|
'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
|
|
@@ -1053,11 +1335,13 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1053
1335
|
htmlContent: pytestHtmlContent,
|
|
1054
1336
|
pytestExitCode,
|
|
1055
1337
|
});
|
|
1056
|
-
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
1338
|
+
const cases = await collectBackendTestHumanCaseCatalog(input.cwd, backendLayout);
|
|
1057
1339
|
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
1058
1340
|
const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
|
|
1059
1341
|
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
1060
|
-
const
|
|
1342
|
+
const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
|
|
1343
|
+
const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
|
|
1344
|
+
const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
|
|
1061
1345
|
const htmlContent = renderBackendTestHtml({
|
|
1062
1346
|
title: meta.spec.title,
|
|
1063
1347
|
parsed,
|
|
@@ -1245,6 +1529,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1245
1529
|
fromNodeId: "backend-test-execution-envelope",
|
|
1246
1530
|
artifactName: "backend-test-execution.json",
|
|
1247
1531
|
outputDir: "contracts",
|
|
1532
|
+
allowedTestRoot: backendLayout.testRoot,
|
|
1248
1533
|
});
|
|
1249
1534
|
outputs.push(`analysis=${analysis.path}`, `execution=${execution.path}`);
|
|
1250
1535
|
}
|
|
@@ -1360,6 +1645,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1360
1645
|
artifactName: "backend-test-result-initial.json",
|
|
1361
1646
|
outputDir: "contracts",
|
|
1362
1647
|
junitRelativePath: "reports/backend-test-initial-junit.xml",
|
|
1648
|
+
testRoot: backendLayout.testRoot,
|
|
1363
1649
|
});
|
|
1364
1650
|
outputs.push(...results.map((result) => result.stdout), `result=${artifact.path}`);
|
|
1365
1651
|
}
|
|
@@ -1734,10 +2020,12 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
1734
2020
|
async function executeFrontendTestCaseChecklist(input, meta) {
|
|
1735
2021
|
const started = Date.now();
|
|
1736
2022
|
try {
|
|
2023
|
+
const layout = frontendTestLayoutFromSpec(meta.spec);
|
|
1737
2024
|
const declaredAcIds = (meta.spec.sourceBinding?.requirementIds ?? []).filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
|
|
1738
2025
|
const result = await validateFrontendCaseChecklist({
|
|
1739
2026
|
workspaceRoot: input.cwd,
|
|
1740
2027
|
declaredAcIds,
|
|
2028
|
+
layout,
|
|
1741
2029
|
});
|
|
1742
2030
|
if (result.issues.length) {
|
|
1743
2031
|
return {
|
|
@@ -1772,6 +2060,7 @@ async function executeFrontendTestL5Report(input, meta) {
|
|
|
1772
2060
|
const output = await renderFrontendTestL5Report({
|
|
1773
2061
|
workspaceRoot: input.cwd,
|
|
1774
2062
|
runDir: meta.runDir,
|
|
2063
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1775
2064
|
});
|
|
1776
2065
|
return {
|
|
1777
2066
|
ok: true,
|
|
@@ -1797,6 +2086,7 @@ async function executeFrontendTestHtmlReport(input, meta) {
|
|
|
1797
2086
|
const output = await renderFrontendTestHtmlReport({
|
|
1798
2087
|
workspaceRoot: input.cwd,
|
|
1799
2088
|
runDir: meta.runDir,
|
|
2089
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1800
2090
|
});
|
|
1801
2091
|
return {
|
|
1802
2092
|
ok: true,
|
|
@@ -1816,11 +2106,60 @@ async function executeFrontendTestHtmlReport(input, meta) {
|
|
|
1816
2106
|
};
|
|
1817
2107
|
}
|
|
1818
2108
|
}
|
|
1819
|
-
async function
|
|
2109
|
+
async function executeFrontendTestStandardScenarios(input, meta) {
|
|
2110
|
+
const started = Date.now();
|
|
2111
|
+
try {
|
|
2112
|
+
const result = await copyFrontendTestStandardScenarios({
|
|
2113
|
+
workspaceRoot: input.cwd,
|
|
2114
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2115
|
+
});
|
|
2116
|
+
return {
|
|
2117
|
+
ok: true,
|
|
2118
|
+
stdout: JSON.stringify(result),
|
|
2119
|
+
stderr: "",
|
|
2120
|
+
failureCategory: "success",
|
|
2121
|
+
durationMs: Date.now() - started,
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
catch (error) {
|
|
2125
|
+
return {
|
|
2126
|
+
ok: false,
|
|
2127
|
+
stdout: "",
|
|
2128
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2129
|
+
failureCategory: "invalid-output",
|
|
2130
|
+
durationMs: Date.now() - started,
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
async function executeFrontendTestEnvironmentProbe(input, meta) {
|
|
2135
|
+
const started = Date.now();
|
|
2136
|
+
const result = await probeFrontendTestEnvironment({
|
|
2137
|
+
workspaceRoot: input.cwd,
|
|
2138
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2139
|
+
});
|
|
2140
|
+
if (result.ok) {
|
|
2141
|
+
return {
|
|
2142
|
+
ok: true,
|
|
2143
|
+
stdout: result.stdout,
|
|
2144
|
+
stderr: "",
|
|
2145
|
+
failureCategory: "success",
|
|
2146
|
+
durationMs: Date.now() - started,
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
return {
|
|
2150
|
+
ok: false,
|
|
2151
|
+
stdout: "",
|
|
2152
|
+
stderr: result.stderr,
|
|
2153
|
+
failureCategory: "nonzero-exit",
|
|
2154
|
+
durationMs: Date.now() - started,
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
async function executeFrontendTestEvidenceValidation(input, meta) {
|
|
1820
2158
|
const started = Date.now();
|
|
1821
2159
|
try {
|
|
1822
2160
|
const result = await validateFrontendCaseEvidence({
|
|
1823
2161
|
workspaceRoot: input.cwd,
|
|
2162
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1824
2163
|
});
|
|
1825
2164
|
const output = `frontend case evidence validation cases=${result.cases} findings=${result.issues.length}${result.issues.length ? ` issues=${JSON.stringify(result.issues)}` : ""}`;
|
|
1826
2165
|
if (result.hardFail) {
|
|
@@ -1859,6 +2198,7 @@ async function executeFrontendTestCaseManifest(input, meta) {
|
|
|
1859
2198
|
const checklist = await validateFrontendCaseChecklist({
|
|
1860
2199
|
workspaceRoot: input.cwd,
|
|
1861
2200
|
declaredAcIds,
|
|
2201
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1862
2202
|
});
|
|
1863
2203
|
if (checklist.issues.length) {
|
|
1864
2204
|
return {
|
|
@@ -1873,6 +2213,7 @@ async function executeFrontendTestCaseManifest(input, meta) {
|
|
|
1873
2213
|
workspaceRoot: input.cwd,
|
|
1874
2214
|
maxCases: gate.maxCases,
|
|
1875
2215
|
declaredAcIds,
|
|
2216
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1876
2217
|
});
|
|
1877
2218
|
return {
|
|
1878
2219
|
ok: true,
|
|
@@ -1911,6 +2252,7 @@ async function executeFrontendTestResultFinalize(input, meta) {
|
|
|
1911
2252
|
try {
|
|
1912
2253
|
const evidence = await validateFrontendCaseEvidence({
|
|
1913
2254
|
workspaceRoot: input.cwd,
|
|
2255
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1914
2256
|
});
|
|
1915
2257
|
advisoryStdout = `frontend case evidence validation cases=${evidence.cases} findings=${evidence.issues.length}${evidence.issues.length ? ` issues=${JSON.stringify(evidence.issues)}` : ""}`;
|
|
1916
2258
|
// Advisory findings must not block materialize; only hardFail (path escape) does.
|
|
@@ -1929,6 +2271,7 @@ async function executeFrontendTestResultFinalize(input, meta) {
|
|
|
1929
2271
|
artifactName: "frontend-test-result.json",
|
|
1930
2272
|
outputDir: "contracts",
|
|
1931
2273
|
sourceBinding: meta.spec.sourceBinding,
|
|
2274
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1932
2275
|
});
|
|
1933
2276
|
return {
|
|
1934
2277
|
ok: true,
|
|
@@ -1967,12 +2310,14 @@ async function executeFrontendTestReports(input, meta) {
|
|
|
1967
2310
|
const l5 = await renderFrontendTestL5Report({
|
|
1968
2311
|
workspaceRoot: input.cwd,
|
|
1969
2312
|
runDir: meta.runDir,
|
|
2313
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1970
2314
|
});
|
|
1971
2315
|
lines.push(`Frontend L-5 report: ${l5.htmlPath}\nMarkdown: ${l5.markdownPath}\nStatus: ${l5.metrics.status}`);
|
|
1972
2316
|
}
|
|
1973
2317
|
const report = await renderFrontendTestHtmlReport({
|
|
1974
2318
|
workspaceRoot: input.cwd,
|
|
1975
2319
|
runDir: meta.runDir,
|
|
2320
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
1976
2321
|
});
|
|
1977
2322
|
lines.push(`Frontend test report: ${report.htmlPath}\nMarkdown: ${report.markdownPath}\nOutcome: ${report.outcome}\nCases: ${report.caseCount}`);
|
|
1978
2323
|
return {
|
|
@@ -2055,7 +2400,76 @@ async function executeFrontendLintBaseline(input, meta) {
|
|
|
2055
2400
|
};
|
|
2056
2401
|
}
|
|
2057
2402
|
}
|
|
2058
|
-
|
|
2403
|
+
async function writeShellPresetProcessReceipt(input) {
|
|
2404
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
|
|
2405
|
+
schemaVersion: 1,
|
|
2406
|
+
kind: "shell-preset",
|
|
2407
|
+
preset: input.preset,
|
|
2408
|
+
ok: input.ok,
|
|
2409
|
+
durationMs: input.durationMs,
|
|
2410
|
+
steps: input.steps,
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
async function ensureShellPresetProcessReceipt(input) {
|
|
2414
|
+
const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
|
|
2415
|
+
if (existsSync(receiptPath))
|
|
2416
|
+
return;
|
|
2417
|
+
if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
const detail = input.result.ok ? input.result.stdout : input.result.stderr;
|
|
2421
|
+
await writeShellPresetProcessReceipt({
|
|
2422
|
+
runDir: input.runDir,
|
|
2423
|
+
nodeId: input.nodeId,
|
|
2424
|
+
preset: input.preset,
|
|
2425
|
+
ok: input.result.ok,
|
|
2426
|
+
durationMs: input.result.durationMs,
|
|
2427
|
+
steps: [
|
|
2428
|
+
{
|
|
2429
|
+
id: "run",
|
|
2430
|
+
label: input.preset,
|
|
2431
|
+
ok: input.result.ok,
|
|
2432
|
+
detail: detail?.slice(0, 8 * 1024),
|
|
2433
|
+
durationMs: input.result.durationMs,
|
|
2434
|
+
},
|
|
2435
|
+
],
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
function namedShellHandler(shell) {
|
|
2439
|
+
if (!shell)
|
|
2440
|
+
return null;
|
|
2441
|
+
const record = shell;
|
|
2442
|
+
for (const key of [
|
|
2443
|
+
"finalWriteSetApprovalGate",
|
|
2444
|
+
"frontendTestStandardScenarios",
|
|
2445
|
+
"frontendTestEnvironmentProbe",
|
|
2446
|
+
"frontendBrowserToolPreflight",
|
|
2447
|
+
"frontendPrewriteGate",
|
|
2448
|
+
"frontendLintBaseline",
|
|
2449
|
+
"frontendVerificationBundle",
|
|
2450
|
+
"frontendReviewContext",
|
|
2451
|
+
"frontendTestCaseChecklist",
|
|
2452
|
+
"frontendTestCaseManifest",
|
|
2453
|
+
"frontendTestEvidenceValidation",
|
|
2454
|
+
"frontendTestResultFinalize",
|
|
2455
|
+
"frontendTestL5Report",
|
|
2456
|
+
"frontendTestHtmlReport",
|
|
2457
|
+
"frontendTestReports",
|
|
2458
|
+
"backendTestPipeline",
|
|
2459
|
+
"jsonArtifactGate",
|
|
2460
|
+
"projectGovernanceGate",
|
|
2461
|
+
"requirementCoverageGate",
|
|
2462
|
+
"verdictGate",
|
|
2463
|
+
"verifyEvidence",
|
|
2464
|
+
"repairArtifactGate",
|
|
2465
|
+
"jacocoCoverage",
|
|
2466
|
+
]) {
|
|
2467
|
+
if (record[key] != null)
|
|
2468
|
+
return key;
|
|
2469
|
+
}
|
|
2470
|
+
return null;
|
|
2471
|
+
}
|
|
2472
|
+
async function executeDagShellNodeInner(input, meta) {
|
|
2059
2473
|
const shell = input.task.shell;
|
|
2060
2474
|
if (shell?.finalWriteSetApprovalGate) {
|
|
2061
2475
|
const started = Date.now();
|
|
@@ -2094,30 +2508,91 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2094
2508
|
};
|
|
2095
2509
|
}
|
|
2096
2510
|
}
|
|
2511
|
+
if (shell?.frontendTestStandardScenarios) {
|
|
2512
|
+
return executeFrontendTestStandardScenarios(input, meta);
|
|
2513
|
+
}
|
|
2514
|
+
if (shell?.frontendTestEnvironmentProbe) {
|
|
2515
|
+
return executeFrontendTestEnvironmentProbe(input, meta);
|
|
2516
|
+
}
|
|
2097
2517
|
if (shell?.frontendBrowserToolPreflight) {
|
|
2098
2518
|
const started = Date.now();
|
|
2519
|
+
const steps = [];
|
|
2520
|
+
const finish = async (result) => {
|
|
2521
|
+
await writeShellPresetProcessReceipt({
|
|
2522
|
+
runDir: meta.runDir,
|
|
2523
|
+
nodeId: input.task.id,
|
|
2524
|
+
preset: "frontendBrowserToolPreflight",
|
|
2525
|
+
ok: result.ok,
|
|
2526
|
+
durationMs: result.durationMs,
|
|
2527
|
+
steps: steps.length > 0
|
|
2528
|
+
? steps
|
|
2529
|
+
: [
|
|
2530
|
+
{
|
|
2531
|
+
id: "run",
|
|
2532
|
+
label: "浏览器工具能力探测",
|
|
2533
|
+
ok: result.ok,
|
|
2534
|
+
detail: result.ok ? result.stdout : result.stderr,
|
|
2535
|
+
durationMs: result.durationMs,
|
|
2536
|
+
},
|
|
2537
|
+
],
|
|
2538
|
+
});
|
|
2539
|
+
return result;
|
|
2540
|
+
};
|
|
2099
2541
|
try {
|
|
2100
2542
|
if (resolvePiBackend() === "cli-only") {
|
|
2543
|
+
steps.push({
|
|
2544
|
+
id: "pi-backend",
|
|
2545
|
+
label: "Pi SDK 后端可用",
|
|
2546
|
+
ok: false,
|
|
2547
|
+
detail: "CODE_AGENT_PI_BACKEND=cli-only",
|
|
2548
|
+
});
|
|
2101
2549
|
throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
|
|
2102
2550
|
}
|
|
2551
|
+
steps.push({
|
|
2552
|
+
id: "pi-backend",
|
|
2553
|
+
label: "Pi SDK 后端可用",
|
|
2554
|
+
ok: true,
|
|
2555
|
+
});
|
|
2103
2556
|
const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
|
|
2104
2557
|
if (!sdkCapability.ok) {
|
|
2558
|
+
steps.push({
|
|
2559
|
+
id: "sdk-custom-tool",
|
|
2560
|
+
label: "Pi SDK custom-tool 能力",
|
|
2561
|
+
ok: false,
|
|
2562
|
+
detail: sdkCapability.detail,
|
|
2563
|
+
});
|
|
2105
2564
|
throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
|
|
2106
2565
|
}
|
|
2566
|
+
steps.push({
|
|
2567
|
+
id: "sdk-custom-tool",
|
|
2568
|
+
label: "Pi SDK custom-tool 能力",
|
|
2569
|
+
ok: true,
|
|
2570
|
+
});
|
|
2571
|
+
const layout = frontendTestLayoutFromSpec(meta.spec);
|
|
2107
2572
|
const toolProbe = await createPlaywrightCliTool({
|
|
2108
2573
|
repoRoot: input.cwd,
|
|
2109
2574
|
runDir: meta.runDir,
|
|
2110
2575
|
nodeId: input.task.id,
|
|
2111
2576
|
caseId: "frontend-browser-preflight",
|
|
2112
|
-
evidenceDir:
|
|
2113
|
-
// Capability-only registration probe. The tested URL is selected later by
|
|
2114
|
-
// retrieve-frontend-test-context-pi and validated by the environment shell.
|
|
2577
|
+
evidenceDir: `${layout.evidenceDir}/frontend-browser-preflight`,
|
|
2115
2578
|
baseUrl: "http://localhost:5173/",
|
|
2116
|
-
inputRoot:
|
|
2579
|
+
inputRoot: layout.fixturesDir,
|
|
2580
|
+
layout,
|
|
2117
2581
|
});
|
|
2118
2582
|
if (toolProbe.name !== "playwright_cli") {
|
|
2583
|
+
steps.push({
|
|
2584
|
+
id: "structured-tool",
|
|
2585
|
+
label: "注册 playwright_cli",
|
|
2586
|
+
ok: false,
|
|
2587
|
+
detail: toolProbe.name,
|
|
2588
|
+
});
|
|
2119
2589
|
throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
|
|
2120
2590
|
}
|
|
2591
|
+
steps.push({
|
|
2592
|
+
id: "structured-tool",
|
|
2593
|
+
label: "注册 playwright_cli",
|
|
2594
|
+
ok: true,
|
|
2595
|
+
});
|
|
2121
2596
|
const launcher = resolvePlaywrightCliLauncher(input.cwd);
|
|
2122
2597
|
const result = await defaultPlaywrightCliCommandRunner({
|
|
2123
2598
|
executable: launcher.executable,
|
|
@@ -2130,8 +2605,19 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2130
2605
|
if (result.exitCode !== 0 ||
|
|
2131
2606
|
result.timedOut ||
|
|
2132
2607
|
!["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
|
|
2608
|
+
steps.push({
|
|
2609
|
+
id: "playwright-cli-help",
|
|
2610
|
+
label: "playwright-cli --help 合同",
|
|
2611
|
+
ok: false,
|
|
2612
|
+
detail: help.slice(0, 8 * 1024),
|
|
2613
|
+
});
|
|
2133
2614
|
throw new Error("playwright-cli-contract-incompatible");
|
|
2134
2615
|
}
|
|
2616
|
+
steps.push({
|
|
2617
|
+
id: "playwright-cli-help",
|
|
2618
|
+
label: "playwright-cli --help 合同",
|
|
2619
|
+
ok: true,
|
|
2620
|
+
});
|
|
2135
2621
|
const capability = {
|
|
2136
2622
|
schemaVersion: 1,
|
|
2137
2623
|
status: "ready",
|
|
@@ -2142,22 +2628,28 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2142
2628
|
targetUrlAuthority: "retrieve-context-and-environment-probe",
|
|
2143
2629
|
};
|
|
2144
2630
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
|
|
2145
|
-
|
|
2631
|
+
steps.push({
|
|
2632
|
+
id: "capability-artifact",
|
|
2633
|
+
label: "写出 frontend-browser-capability.json",
|
|
2634
|
+
ok: true,
|
|
2635
|
+
detail: JSON.stringify(capability, null, 2),
|
|
2636
|
+
});
|
|
2637
|
+
return await finish({
|
|
2146
2638
|
ok: true,
|
|
2147
2639
|
stdout: JSON.stringify(capability),
|
|
2148
2640
|
stderr: "",
|
|
2149
2641
|
failureCategory: "success",
|
|
2150
2642
|
durationMs: Date.now() - started,
|
|
2151
|
-
};
|
|
2643
|
+
});
|
|
2152
2644
|
}
|
|
2153
2645
|
catch (error) {
|
|
2154
|
-
return {
|
|
2646
|
+
return await finish({
|
|
2155
2647
|
ok: false,
|
|
2156
2648
|
stdout: "",
|
|
2157
2649
|
stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
2158
2650
|
failureCategory: "tool-policy",
|
|
2159
2651
|
durationMs: Date.now() - started,
|
|
2160
|
-
};
|
|
2652
|
+
});
|
|
2161
2653
|
}
|
|
2162
2654
|
}
|
|
2163
2655
|
if (shell?.frontendPrewriteGate) {
|
|
@@ -2227,7 +2719,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2227
2719
|
return executeFrontendTestCaseManifest(input, meta);
|
|
2228
2720
|
}
|
|
2229
2721
|
if (shell?.frontendTestEvidenceValidation) {
|
|
2230
|
-
return executeFrontendTestEvidenceValidation(input);
|
|
2722
|
+
return executeFrontendTestEvidenceValidation(input, meta);
|
|
2231
2723
|
}
|
|
2232
2724
|
if (shell?.frontendTestResultFinalize) {
|
|
2233
2725
|
return executeFrontendTestResultFinalize(input, meta);
|
|
@@ -2246,6 +2738,13 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2246
2738
|
}
|
|
2247
2739
|
if (shell?.jsonArtifactGate) {
|
|
2248
2740
|
const started = Date.now();
|
|
2741
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
2742
|
+
? {
|
|
2743
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
2744
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
2745
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
2746
|
+
}
|
|
2747
|
+
: undefined);
|
|
2249
2748
|
try {
|
|
2250
2749
|
const gate = shell.jsonArtifactGate;
|
|
2251
2750
|
const sourceNodeId = gate.fallbackFromNodeIds?.length
|
|
@@ -2273,6 +2772,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2273
2772
|
fromNodeId: sourceNodeId,
|
|
2274
2773
|
artifactName: gate.artifactName,
|
|
2275
2774
|
outputDir: gate.outputDir,
|
|
2775
|
+
allowedTestRoot: backendLayout.testRoot,
|
|
2276
2776
|
});
|
|
2277
2777
|
break;
|
|
2278
2778
|
case "backend-test-result-v1":
|
|
@@ -2282,6 +2782,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2282
2782
|
artifactName: gate.artifactName,
|
|
2283
2783
|
outputDir: gate.outputDir,
|
|
2284
2784
|
junitRelativePath: gate.junitRelativePath,
|
|
2785
|
+
testRoot: backendLayout.testRoot,
|
|
2285
2786
|
});
|
|
2286
2787
|
break;
|
|
2287
2788
|
case "backend-test-classification-v1":
|
|
@@ -2325,6 +2826,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2325
2826
|
artifactName: gate.artifactName,
|
|
2326
2827
|
outputDir: gate.outputDir,
|
|
2327
2828
|
sourceBinding: meta.spec.sourceBinding,
|
|
2829
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2328
2830
|
});
|
|
2329
2831
|
break;
|
|
2330
2832
|
default:
|
|
@@ -2570,6 +3072,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2570
3072
|
const cwd = resolveShellCwd(input.cwd, shell.cwd);
|
|
2571
3073
|
const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
2572
3074
|
const failFast = shell.failFast !== false;
|
|
3075
|
+
const attemptNumber = input.attempt ?? 1;
|
|
2573
3076
|
const results = [];
|
|
2574
3077
|
const executedCommandKeys = new Set();
|
|
2575
3078
|
let beforeStatus;
|
|
@@ -2665,8 +3168,8 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2665
3168
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2666
3169
|
reportActivity: input.reportActivity,
|
|
2667
3170
|
outputArtifacts: {
|
|
2668
|
-
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2669
|
-
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
3171
|
+
stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
|
|
3172
|
+
stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
|
|
2670
3173
|
},
|
|
2671
3174
|
});
|
|
2672
3175
|
results.push(result);
|
|
@@ -2725,6 +3228,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2725
3228
|
schemaVersion: 1,
|
|
2726
3229
|
runId: meta.runId,
|
|
2727
3230
|
nodeId: input.task.id,
|
|
3231
|
+
attempt: attemptNumber,
|
|
2728
3232
|
workspaceFingerprint: workspaceFingerprint ?? null,
|
|
2729
3233
|
failFast,
|
|
2730
3234
|
commands: results.map((result) => ({
|
|
@@ -2743,6 +3247,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2743
3247
|
subchecks: parseCheckRepoSubcheckDurations(result),
|
|
2744
3248
|
})),
|
|
2745
3249
|
};
|
|
3250
|
+
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
|
|
2746
3251
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
|
|
2747
3252
|
const summaryMarkdown = buildShellResultSummaryMarkdown({
|
|
2748
3253
|
nodeId: input.task.id,
|
|
@@ -2794,8 +3299,24 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2794
3299
|
stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
|
|
2795
3300
|
stderr: stderrParts.filter(Boolean).join("\n\n"),
|
|
2796
3301
|
failureCategory,
|
|
3302
|
+
...(firstFailure?.failureCategory
|
|
3303
|
+
? { rawFailureCategory: firstFailure.failureCategory }
|
|
3304
|
+
: {}),
|
|
2797
3305
|
durationMs: Date.now() - started,
|
|
2798
3306
|
// extra field consumed by node-execution if supported; also embedded in stdout marker
|
|
2799
3307
|
...{ commandResults },
|
|
2800
3308
|
};
|
|
2801
3309
|
}
|
|
3310
|
+
export async function executeDagShellNode(input, meta) {
|
|
3311
|
+
const handler = namedShellHandler(input.task.shell);
|
|
3312
|
+
const result = await executeDagShellNodeInner(input, meta);
|
|
3313
|
+
if (handler) {
|
|
3314
|
+
await ensureShellPresetProcessReceipt({
|
|
3315
|
+
runDir: meta.runDir,
|
|
3316
|
+
nodeId: input.task.id,
|
|
3317
|
+
preset: handler,
|
|
3318
|
+
result,
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
return result;
|
|
3322
|
+
}
|