@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.30
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 +267 -65
- 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 +118 -14
- 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 +8 -1
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +533 -29
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +181 -27
- 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 +22 -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/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/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +478 -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 +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +137 -0
- package/dist/worker/console/operator-actions.js +244 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +12 -5
- 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 +83 -10
- package/dist/worker/console/server.js +42 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-CiGOV5eG.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-Ds9Gyfd8.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-C6V6uC98.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DezF7Ol9.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BCWk3gCT.js} +1 -1
- package/dist/worker/console/static/assets/channel-DMvu1nmi.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DbYr-Cxy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-XSlusW4G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-BtcKAj9i.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BF57lhXj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-DJIlR25o.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-DklNc7dm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-fZNtEdQZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-BBgLqV4h.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-Dl9e5n_J.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-B0JK51NT.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-Ch-ajv4O.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-xn_NxcfB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DRD0fyga.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-COtnKp4B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BQp_CAqG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-x0y-vNgq.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-N2ezwL31.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-CSaxRH6Q.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-6Ro7Kpbd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CrIIQEAI.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-DivKcq8g.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BXrfDo8y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-LTTQmMRJ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DP3o_5qd.js} +1 -1
- package/dist/worker/console/static/assets/index-C2jmYBfu.js +404 -0
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-wVfSY1WH.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BRKfFL0I.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DEml3jLb.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-DTzBmOqN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-Cpy1d873.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-C1cSavOy.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-BEBSKvYM.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BrLs4Oac.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-B47AZcLa.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-s0dVeYR2.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-lW8aJ7Q6.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-BcHJPf6Q.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DdegpUUf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-BxFWqNv8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-cOdUDPbR.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-DpxlYl8P.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BwecGAkM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DQOfurE0.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D5WW_LbN.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-CIp3tC6V.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-C-adIPKn.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CiYrar9M.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-WHrdTV0-.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +39 -0
- 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/operator-chat/chat-sse-events.js +82 -22
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
- 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 +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- 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/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/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +805 -113
- package/dist/workflows/dag/node-execution.js +194 -6
- 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 +113 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +157 -1
- package/dist/workflows/dag/validate.js +41 -1
- 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-implementation-contract.schema.json +9 -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 +3 -1
- 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/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +9 -6
- 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/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.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,13 @@ 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 { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
|
|
30
34
|
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
31
35
|
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";
|
|
36
|
+
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
37
|
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";
|
|
38
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
35
39
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
36
40
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
37
41
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -48,6 +52,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
48
52
|
phase: "initial",
|
|
49
53
|
repairAttempt: 0,
|
|
50
54
|
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
55
|
+
layout: input.layout,
|
|
51
56
|
});
|
|
52
57
|
const initialArtifacts = await writeBackendScenarioParamArtifacts({
|
|
53
58
|
runDir: input.runDir,
|
|
@@ -73,6 +78,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
73
78
|
phase: "final",
|
|
74
79
|
repairAttempt: 1,
|
|
75
80
|
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
81
|
+
layout: input.layout,
|
|
76
82
|
});
|
|
77
83
|
finalFacts = reassessed.facts;
|
|
78
84
|
finalMarkdown = reassessed.markdown;
|
|
@@ -117,6 +123,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
|
|
|
117
123
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
118
124
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
119
125
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
126
|
+
const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
|
|
127
|
+
const TEST_TIMEOUT_MARKERS = [
|
|
128
|
+
/Test timed out in \d+ms/i,
|
|
129
|
+
/Timeout of \d+ms exceeded/i,
|
|
130
|
+
];
|
|
131
|
+
const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
|
|
132
|
+
/AssertionError/i,
|
|
133
|
+
/expected:?\s/i,
|
|
134
|
+
/received:?\s/i,
|
|
135
|
+
/Snapshot /i,
|
|
136
|
+
/toMatchSnapshot/i,
|
|
137
|
+
/SyntaxError/i,
|
|
138
|
+
/TS\d{4}/,
|
|
139
|
+
/error TS/i,
|
|
140
|
+
/eslint/i,
|
|
141
|
+
/compile error/i,
|
|
142
|
+
/Failed to compile/i,
|
|
143
|
+
];
|
|
144
|
+
function isIdentifiedTestCommand(command) {
|
|
145
|
+
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);
|
|
146
|
+
}
|
|
147
|
+
function classifyShellCommandFailure(input) {
|
|
148
|
+
if (input.processCategory !== "nonzero-exit")
|
|
149
|
+
return input.processCategory;
|
|
150
|
+
if (!isIdentifiedTestCommand(input.command))
|
|
151
|
+
return "nonzero-exit";
|
|
152
|
+
const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
|
|
153
|
+
const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
|
|
154
|
+
if (!hasTimeoutMarker)
|
|
155
|
+
return "nonzero-exit";
|
|
156
|
+
const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
|
|
157
|
+
return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
|
|
158
|
+
}
|
|
120
159
|
const NODE_STDOUT_MAX = 12_000;
|
|
121
160
|
const NODE_STDERR_MAX = 8_000;
|
|
122
161
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
@@ -323,16 +362,21 @@ export async function executeShellCommand(input) {
|
|
|
323
362
|
});
|
|
324
363
|
});
|
|
325
364
|
child.on("close", (code) => {
|
|
326
|
-
let
|
|
365
|
+
let processCategory = "nonzero-exit";
|
|
327
366
|
if (timedOut) {
|
|
328
|
-
|
|
367
|
+
processCategory = "timeout";
|
|
329
368
|
}
|
|
330
369
|
else if (code === 0) {
|
|
331
|
-
|
|
370
|
+
processCategory = "success";
|
|
332
371
|
}
|
|
333
372
|
finish({
|
|
334
373
|
exitCode: code,
|
|
335
|
-
failureCategory
|
|
374
|
+
failureCategory: classifyShellCommandFailure({
|
|
375
|
+
command: input.command,
|
|
376
|
+
processCategory,
|
|
377
|
+
stdout: formatBoundedOutput(stdout),
|
|
378
|
+
stderr: formatBoundedOutput(stderr),
|
|
379
|
+
}),
|
|
336
380
|
ok: code === 0 && !timedOut,
|
|
337
381
|
});
|
|
338
382
|
});
|
|
@@ -492,11 +536,114 @@ async function readAdvisoryRunReport(reportsDir, filename, title) {
|
|
|
492
536
|
async function executeBackendTestPipeline(input, meta) {
|
|
493
537
|
const pipeline = input.task.shell?.backendTestPipeline;
|
|
494
538
|
const started = Date.now();
|
|
539
|
+
// Plan A: prefer the frozen layout carried by the compiled spec; fall back
|
|
540
|
+
// to the historical default for hand-written legacy DAGs.
|
|
541
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
542
|
+
? {
|
|
543
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
544
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
545
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
546
|
+
}
|
|
547
|
+
: undefined);
|
|
495
548
|
try {
|
|
496
549
|
if (!pipeline)
|
|
497
550
|
throw new Error("missing backend-test pipeline id");
|
|
498
551
|
const outputs = [];
|
|
499
|
-
if (pipeline === "
|
|
552
|
+
if (pipeline === "ingest-backend-test-gap") {
|
|
553
|
+
// Plan D: deterministic gap ingest. Identity (taskId + requirement
|
|
554
|
+
// sha256) is fail-closed; a mismatched plan blocks every writer.
|
|
555
|
+
const gapDocRel = meta.spec.globalConstraints
|
|
556
|
+
?.map((item) => /^backendTestGapDoc=(.+)$/i.exec(item.trim())?.[1])
|
|
557
|
+
.find(Boolean);
|
|
558
|
+
const binding = meta.spec.sourceBinding;
|
|
559
|
+
const requirementSha256 = binding?.sources.find((s) => s.kind === "requirement")?.sha256 ?? "";
|
|
560
|
+
if (!gapDocRel || !binding || !requirementSha256) {
|
|
561
|
+
return {
|
|
562
|
+
ok: false,
|
|
563
|
+
stdout: outputs.join("\n"),
|
|
564
|
+
stderr: "backend-test gap ingest requires the bound gap document path and the task source binding with a requirement sha256",
|
|
565
|
+
failureCategory: "invalid-output",
|
|
566
|
+
durationMs: Date.now() - started,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const gapDocAbs = path.join(input.cwd, ...gapDocRel.split("/"));
|
|
570
|
+
let parsed;
|
|
571
|
+
try {
|
|
572
|
+
parsed = JSON.parse(await readFile(gapDocAbs, "utf8"));
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
stdout: outputs.join("\n"),
|
|
578
|
+
stderr: `backend-test gap document is unreadable JSON at ${gapDocRel}: ${error instanceof Error ? error.message : String(error)}`,
|
|
579
|
+
failureCategory: "invalid-output",
|
|
580
|
+
durationMs: Date.now() - started,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const doc = backendTestGapDocSchema.safeParse(parsed);
|
|
584
|
+
if (!doc.success) {
|
|
585
|
+
return {
|
|
586
|
+
ok: false,
|
|
587
|
+
stdout: outputs.join("\n"),
|
|
588
|
+
stderr: `invalid backend-test-gap-v1 document: ${doc.error.issues
|
|
589
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
590
|
+
.join("; ")}`,
|
|
591
|
+
failureCategory: "invalid-output",
|
|
592
|
+
durationMs: Date.now() - started,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
// previous N6 facts (may be absent on a fresh checkout)
|
|
596
|
+
const factsPath = path.join(input.cwd, "contracts", "backend-test-case-coverage-facts.json");
|
|
597
|
+
let previousMissingSlots;
|
|
598
|
+
let previousCoveredSlots;
|
|
599
|
+
try {
|
|
600
|
+
const facts = JSON.parse(await readFile(factsPath, "utf8"));
|
|
601
|
+
previousMissingSlots = facts.scenarioPartitions?.missingSlots;
|
|
602
|
+
previousCoveredSlots = (facts.rules ?? []).flatMap((rule) => rule.coveredTestPoints ?? []);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// no previous facts in the workspace: the gap doc alone drives the plan
|
|
606
|
+
}
|
|
607
|
+
// module index comes from the README on disk
|
|
608
|
+
let existingModuleStems = [];
|
|
609
|
+
try {
|
|
610
|
+
const files = await markdownFiles(input.cwd, backendLayout);
|
|
611
|
+
existingModuleStems = files
|
|
612
|
+
.map((file) => path.basename(file, ".md"))
|
|
613
|
+
.filter((stem) => stem.toLowerCase() !== "readme");
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// no markdown tree: every slot becomes a conflict
|
|
617
|
+
}
|
|
618
|
+
const plan = ingestBackendTestGap({
|
|
619
|
+
gapDoc: doc.data,
|
|
620
|
+
previousMissingSlots,
|
|
621
|
+
previousCoveredSlots,
|
|
622
|
+
existingModuleStems,
|
|
623
|
+
sharedSetupPath: meta.spec.backendTestSharedSetup?.path ?? null,
|
|
624
|
+
layout: backendLayout,
|
|
625
|
+
taskId: binding.taskId,
|
|
626
|
+
requirementSha256,
|
|
627
|
+
});
|
|
628
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
629
|
+
await mkdir(contractsDir, { recursive: true });
|
|
630
|
+
const planPath = path.join(contractsDir, "backend-test-gap-plan-v1.json");
|
|
631
|
+
await writeFile(planPath, JSON.stringify(plan, null, 2), "utf8");
|
|
632
|
+
outputs.push(`gapPlan=${planPath}`, `gapPlanStatus=${plan.status}`);
|
|
633
|
+
if (plan.status === "error") {
|
|
634
|
+
return {
|
|
635
|
+
ok: false,
|
|
636
|
+
stdout: outputs.join("\n"),
|
|
637
|
+
stderr: [
|
|
638
|
+
"backend-test gap ingest failed closed (identity/conflicts):",
|
|
639
|
+
...plan.conflicts,
|
|
640
|
+
].join("\n"),
|
|
641
|
+
failureCategory: "invalid-output",
|
|
642
|
+
durationMs: Date.now() - started,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else if (pipeline === "markdown-environment") {
|
|
500
647
|
const results = await executePipelineCommands(input, meta);
|
|
501
648
|
if (!results.every((result) => result.ok)) {
|
|
502
649
|
const failure = results.find((result) => !result.ok);
|
|
@@ -530,6 +677,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
530
677
|
environmentMarkdown,
|
|
531
678
|
requiredRequirementIds,
|
|
532
679
|
sourceBinding: meta.spec.sourceBinding,
|
|
680
|
+
layout: backendLayout,
|
|
681
|
+
sharedSetup: meta.spec.backendTestSharedSetup ?? null,
|
|
533
682
|
});
|
|
534
683
|
}
|
|
535
684
|
catch (error) {
|
|
@@ -583,6 +732,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
583
732
|
const coverage = await analyzeBackendTestCaseCoverage({
|
|
584
733
|
workspaceRoot: input.cwd,
|
|
585
734
|
sourceBinding: normalizedBinding,
|
|
735
|
+
layout: backendLayout,
|
|
586
736
|
});
|
|
587
737
|
const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
|
|
588
738
|
const contractsDir = path.join(meta.runDir, "contracts");
|
|
@@ -590,6 +740,23 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
590
740
|
const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
|
|
591
741
|
await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
|
|
592
742
|
outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
|
|
743
|
+
// Plan C: scenario-partition slot coverage is fail-closed. General
|
|
744
|
+
// coverage findings stay advisory, but a declared filter axis missing
|
|
745
|
+
// an each-value/omitted slot or its complement slot must block pytest
|
|
746
|
+
// generation until the Markdown declares the slot.
|
|
747
|
+
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));
|
|
748
|
+
if (partitionBlocking.length > 0) {
|
|
749
|
+
return {
|
|
750
|
+
ok: false,
|
|
751
|
+
stdout: outputs.join("\n\n"),
|
|
752
|
+
stderr: [
|
|
753
|
+
"backend-test scenario partitions are incomplete (fail-closed):",
|
|
754
|
+
...partitionBlocking,
|
|
755
|
+
].join("\n"),
|
|
756
|
+
failureCategory: "invalid-output",
|
|
757
|
+
durationMs: Date.now() - started,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
593
760
|
}
|
|
594
761
|
catch (error) {
|
|
595
762
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -606,9 +773,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
606
773
|
}
|
|
607
774
|
}
|
|
608
775
|
else if (pipeline === "markdown-collection-assess") {
|
|
609
|
-
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
|
|
776
|
+
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
610
777
|
let facts;
|
|
611
|
-
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
778
|
+
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
|
|
612
779
|
const priorityOnlyFacts = assessPriorityOnlyBackendPytestModules({
|
|
613
780
|
phase: "initial",
|
|
614
781
|
mappedScripts: resolution.expectedScripts,
|
|
@@ -618,7 +785,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
618
785
|
facts = priorityOnlyFacts;
|
|
619
786
|
}
|
|
620
787
|
else if (resolution.missingScripts.length > 0) {
|
|
621
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
788
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
|
|
622
789
|
facts = assessMissingBackendPytestScripts({
|
|
623
790
|
mappedScripts: resolution.expectedScripts,
|
|
624
791
|
existingMappedScripts: resolution.existingScripts,
|
|
@@ -632,10 +799,11 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
632
799
|
const scenario = await runBackendScenarioParamPreflight({
|
|
633
800
|
workspaceRoot: input.cwd,
|
|
634
801
|
runDir: meta.runDir,
|
|
802
|
+
layout: backendLayout,
|
|
635
803
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
636
804
|
});
|
|
637
805
|
outputs.push(...scenario.outputs);
|
|
638
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
806
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
|
|
639
807
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
640
808
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
641
809
|
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
@@ -669,6 +837,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
669
837
|
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
670
838
|
}
|
|
671
839
|
facts = assessBackendPytestCollection({
|
|
840
|
+
layout: backendLayout,
|
|
672
841
|
phase: "initial",
|
|
673
842
|
mappedScripts,
|
|
674
843
|
inventory,
|
|
@@ -678,6 +847,43 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
678
847
|
fixtureResolution,
|
|
679
848
|
});
|
|
680
849
|
}
|
|
850
|
+
try {
|
|
851
|
+
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
852
|
+
workspaceRoot: input.cwd,
|
|
853
|
+
layout: backendLayout,
|
|
854
|
+
taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
|
|
855
|
+
});
|
|
856
|
+
const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
|
|
857
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
858
|
+
await mkdir(contractsDir, { recursive: true });
|
|
859
|
+
const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
|
|
860
|
+
await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
|
|
861
|
+
outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
|
|
862
|
+
if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
|
|
863
|
+
const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
|
|
864
|
+
.filter((entry) => entry.status !== "EXACT_1_TO_1")
|
|
865
|
+
.flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
|
|
866
|
+
.filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
|
|
867
|
+
if (correspondenceRepairPaths.length > 0) {
|
|
868
|
+
facts.status = "REPAIRABLE";
|
|
869
|
+
facts.repairEligible = true;
|
|
870
|
+
facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
|
|
871
|
+
facts.findings.push({
|
|
872
|
+
kind: "markdown-pytest-correspondence",
|
|
873
|
+
classification: "test-asset-defect",
|
|
874
|
+
repairability: "repairable",
|
|
875
|
+
detail: correspondence.facts.entries
|
|
876
|
+
.filter((entry) => entry.status !== "EXACT_1_TO_1")
|
|
877
|
+
.flatMap((entry) => entry.findings)
|
|
878
|
+
.join("; ")
|
|
879
|
+
.slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
catch (error) {
|
|
885
|
+
outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
|
|
886
|
+
}
|
|
681
887
|
const artifacts = await writeBackendPytestCollectionArtifacts({
|
|
682
888
|
runDir: meta.runDir,
|
|
683
889
|
stem: "initial",
|
|
@@ -711,14 +917,15 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
711
917
|
});
|
|
712
918
|
}
|
|
713
919
|
else if (initial.status === "REPAIRABLE") {
|
|
714
|
-
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
920
|
+
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
715
921
|
const scenario = await runBackendScenarioParamPreflight({
|
|
716
922
|
workspaceRoot: input.cwd,
|
|
717
923
|
runDir: meta.runDir,
|
|
924
|
+
layout: backendLayout,
|
|
718
925
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
719
926
|
});
|
|
720
927
|
outputs.push(...scenario.outputs);
|
|
721
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
928
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
|
|
722
929
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
723
930
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
724
931
|
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
@@ -753,6 +960,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
753
960
|
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
754
961
|
}
|
|
755
962
|
const finalFacts = assessBackendPytestCollection({
|
|
963
|
+
layout: backendLayout,
|
|
756
964
|
phase: "final",
|
|
757
965
|
mappedScripts,
|
|
758
966
|
inventory,
|
|
@@ -776,12 +984,57 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
776
984
|
facts: effective,
|
|
777
985
|
});
|
|
778
986
|
const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
|
|
987
|
+
const correspondence = meta.spec.sourceBinding
|
|
988
|
+
? await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
989
|
+
workspaceRoot: input.cwd,
|
|
990
|
+
layout: backendLayout,
|
|
991
|
+
taskId: meta.spec.sourceBinding.taskId,
|
|
992
|
+
})
|
|
993
|
+
: undefined;
|
|
994
|
+
const eligibility = correspondence
|
|
995
|
+
? buildBackendTestItemEligibility(effective.collectedItemIds, {
|
|
996
|
+
correspondenceEntries: correspondence.facts.entries,
|
|
997
|
+
scenarioEntries: scenarioFacts.entries,
|
|
998
|
+
})
|
|
999
|
+
: { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
|
|
1000
|
+
const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
|
|
1001
|
+
const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
|
|
1002
|
+
await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
|
|
1003
|
+
await writeFile(eligibilityFactsPath, `${JSON.stringify({
|
|
1004
|
+
schemaId: "backend-test-execution-eligibility-v1",
|
|
1005
|
+
status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
|
|
1006
|
+
collectedItemCount: effective.collectedItemIds.length,
|
|
1007
|
+
eligibleItemCount: eligibility.eligibleItemIds.length,
|
|
1008
|
+
excludedItemCount: eligibility.excludedItems.length,
|
|
1009
|
+
eligibleItemIds: eligibility.eligibleItemIds,
|
|
1010
|
+
excludedItems: eligibility.excludedItems,
|
|
1011
|
+
inputHashes: eligibilityInputHashes,
|
|
1012
|
+
}, null, 2)}\n`, "utf8");
|
|
1013
|
+
const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
|
|
1014
|
+
"# Backend Test Execution Eligibility",
|
|
1015
|
+
"",
|
|
1016
|
+
"## Status",
|
|
1017
|
+
"",
|
|
1018
|
+
eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
|
|
1019
|
+
"",
|
|
1020
|
+
`- Collected items: ${effective.collectedItemIds.length}`,
|
|
1021
|
+
`- Eligible items: ${eligibility.eligibleItemIds.length}`,
|
|
1022
|
+
`- Excluded items: ${eligibility.excludedItems.length}`,
|
|
1023
|
+
"",
|
|
1024
|
+
"## Excluded Items",
|
|
1025
|
+
"",
|
|
1026
|
+
...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
|
|
1027
|
+
"",
|
|
1028
|
+
].join("\n"));
|
|
1029
|
+
outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
|
|
779
1030
|
const readiness = await materializeBackendTestExecutionReadiness({
|
|
780
1031
|
runDir: meta.runDir,
|
|
781
1032
|
workspaceRoot: input.cwd,
|
|
782
1033
|
effective,
|
|
783
1034
|
scenarioParamStatus: scenarioFacts.overallStatus,
|
|
784
1035
|
scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
|
|
1036
|
+
eligibility,
|
|
1037
|
+
eligibilityInputHashes,
|
|
785
1038
|
});
|
|
786
1039
|
return {
|
|
787
1040
|
ok: true,
|
|
@@ -792,6 +1045,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
792
1045
|
collectedItemCount: effective.collectedItemCount,
|
|
793
1046
|
fixtureResolutionStatus: effective.fixtureResolutionStatus,
|
|
794
1047
|
executionReadinessStatus: readiness.status,
|
|
1048
|
+
eligibleItemCount: readiness.eligibleItemIds.length,
|
|
1049
|
+
excludedItemCount: readiness.excludedItems.length,
|
|
795
1050
|
factsPath: "contracts/backend-test-pytest-collection-effective.json",
|
|
796
1051
|
readinessPath: "contracts/backend-test-execution-readiness.json",
|
|
797
1052
|
reportPath: artifacts.reportPath,
|
|
@@ -826,6 +1081,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
826
1081
|
try {
|
|
827
1082
|
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
828
1083
|
workspaceRoot: input.cwd,
|
|
1084
|
+
layout: backendLayout,
|
|
829
1085
|
taskId: sourceBinding.taskId,
|
|
830
1086
|
});
|
|
831
1087
|
const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
|
|
@@ -888,9 +1144,32 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
888
1144
|
else if (pipeline === "markdown-execute-html") {
|
|
889
1145
|
const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
|
|
890
1146
|
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
|
|
891
|
-
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
892
|
-
|
|
893
|
-
|
|
1147
|
+
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
1148
|
+
let eligibleItemIds = executionReadiness.eligibleItemIds;
|
|
1149
|
+
// Plan D gap-fill executeScope=affected: when a gap plan exists in
|
|
1150
|
+
// this run, prefer the newly planned items (pytest node ids contain
|
|
1151
|
+
// the TP slot id) plus every collected item of the patched modules;
|
|
1152
|
+
// scope=all keeps the full eligible set.
|
|
1153
|
+
const affectedScope = meta.spec.globalConstraints?.some((item) => /^backendTestExecuteScope=affected$/i.test(item.trim())) ?? false;
|
|
1154
|
+
if (affectedScope) {
|
|
1155
|
+
try {
|
|
1156
|
+
const plan = JSON.parse(await readFile(path.join(meta.runDir, "contracts", "backend-test-gap-plan-v1.json"), "utf8"));
|
|
1157
|
+
const newPoints = new Set(plan.newTestPoints ?? []);
|
|
1158
|
+
const modules = new Set((plan.targetModules ?? []).map((stem) => `test_${stem}.py`));
|
|
1159
|
+
const affected = eligibleItemIds.filter((id) => modules.size === 0 ||
|
|
1160
|
+
[...modules].some((script) => id.includes(script)) ||
|
|
1161
|
+
[...newPoints].some((point) => id.includes(point)));
|
|
1162
|
+
if (affected.length > 0)
|
|
1163
|
+
eligibleItemIds = affected;
|
|
1164
|
+
}
|
|
1165
|
+
catch {
|
|
1166
|
+
// no gap plan in this run: execute the full eligible set
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
|
|
1170
|
+
const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
|
|
1171
|
+
await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
|
|
1172
|
+
const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
|
|
894
1173
|
// Split into short bash -c commands (aligned with markdown-environment).
|
|
895
1174
|
// A single ultra-long compound command has been observed on Windows to
|
|
896
1175
|
// exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
|
|
@@ -967,8 +1246,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
967
1246
|
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
968
1247
|
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
969
1248
|
'echo "STEP=pytest-run"',
|
|
970
|
-
`echo "- pytestCommand: python -m pytest
|
|
971
|
-
`
|
|
1249
|
+
`echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
|
|
1250
|
+
`echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
|
|
1251
|
+
`echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
|
|
1252
|
+
`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
1253
|
"STATUS=$?",
|
|
973
1254
|
'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
974
1255
|
'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
|
|
@@ -1053,11 +1334,13 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1053
1334
|
htmlContent: pytestHtmlContent,
|
|
1054
1335
|
pytestExitCode,
|
|
1055
1336
|
});
|
|
1056
|
-
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
1337
|
+
const cases = await collectBackendTestHumanCaseCatalog(input.cwd, backendLayout);
|
|
1057
1338
|
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
1058
1339
|
const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
|
|
1059
1340
|
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
1060
|
-
const
|
|
1341
|
+
const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
|
|
1342
|
+
const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
|
|
1343
|
+
const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
|
|
1061
1344
|
const htmlContent = renderBackendTestHtml({
|
|
1062
1345
|
title: meta.spec.title,
|
|
1063
1346
|
parsed,
|
|
@@ -1245,6 +1528,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1245
1528
|
fromNodeId: "backend-test-execution-envelope",
|
|
1246
1529
|
artifactName: "backend-test-execution.json",
|
|
1247
1530
|
outputDir: "contracts",
|
|
1531
|
+
allowedTestRoot: backendLayout.testRoot,
|
|
1248
1532
|
});
|
|
1249
1533
|
outputs.push(`analysis=${analysis.path}`, `execution=${execution.path}`);
|
|
1250
1534
|
}
|
|
@@ -1360,6 +1644,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1360
1644
|
artifactName: "backend-test-result-initial.json",
|
|
1361
1645
|
outputDir: "contracts",
|
|
1362
1646
|
junitRelativePath: "reports/backend-test-initial-junit.xml",
|
|
1647
|
+
testRoot: backendLayout.testRoot,
|
|
1363
1648
|
});
|
|
1364
1649
|
outputs.push(...results.map((result) => result.stdout), `result=${artifact.path}`);
|
|
1365
1650
|
}
|
|
@@ -1816,6 +2101,52 @@ async function executeFrontendTestHtmlReport(input, meta) {
|
|
|
1816
2101
|
};
|
|
1817
2102
|
}
|
|
1818
2103
|
}
|
|
2104
|
+
async function executeFrontendTestStandardScenarios(input) {
|
|
2105
|
+
const started = Date.now();
|
|
2106
|
+
try {
|
|
2107
|
+
const result = await copyFrontendTestStandardScenarios({
|
|
2108
|
+
workspaceRoot: input.cwd,
|
|
2109
|
+
});
|
|
2110
|
+
return {
|
|
2111
|
+
ok: true,
|
|
2112
|
+
stdout: JSON.stringify(result),
|
|
2113
|
+
stderr: "",
|
|
2114
|
+
failureCategory: "success",
|
|
2115
|
+
durationMs: Date.now() - started,
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
catch (error) {
|
|
2119
|
+
return {
|
|
2120
|
+
ok: false,
|
|
2121
|
+
stdout: "",
|
|
2122
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2123
|
+
failureCategory: "invalid-output",
|
|
2124
|
+
durationMs: Date.now() - started,
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
async function executeFrontendTestEnvironmentProbe(input) {
|
|
2129
|
+
const started = Date.now();
|
|
2130
|
+
const result = await probeFrontendTestEnvironment({
|
|
2131
|
+
workspaceRoot: input.cwd,
|
|
2132
|
+
});
|
|
2133
|
+
if (result.ok) {
|
|
2134
|
+
return {
|
|
2135
|
+
ok: true,
|
|
2136
|
+
stdout: result.stdout,
|
|
2137
|
+
stderr: "",
|
|
2138
|
+
failureCategory: "success",
|
|
2139
|
+
durationMs: Date.now() - started,
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
return {
|
|
2143
|
+
ok: false,
|
|
2144
|
+
stdout: "",
|
|
2145
|
+
stderr: result.stderr,
|
|
2146
|
+
failureCategory: "nonzero-exit",
|
|
2147
|
+
durationMs: Date.now() - started,
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
1819
2150
|
async function executeFrontendTestEvidenceValidation(input) {
|
|
1820
2151
|
const started = Date.now();
|
|
1821
2152
|
try {
|
|
@@ -2055,7 +2386,76 @@ async function executeFrontendLintBaseline(input, meta) {
|
|
|
2055
2386
|
};
|
|
2056
2387
|
}
|
|
2057
2388
|
}
|
|
2058
|
-
|
|
2389
|
+
async function writeShellPresetProcessReceipt(input) {
|
|
2390
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
|
|
2391
|
+
schemaVersion: 1,
|
|
2392
|
+
kind: "shell-preset",
|
|
2393
|
+
preset: input.preset,
|
|
2394
|
+
ok: input.ok,
|
|
2395
|
+
durationMs: input.durationMs,
|
|
2396
|
+
steps: input.steps,
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
async function ensureShellPresetProcessReceipt(input) {
|
|
2400
|
+
const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
|
|
2401
|
+
if (existsSync(receiptPath))
|
|
2402
|
+
return;
|
|
2403
|
+
if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const detail = input.result.ok ? input.result.stdout : input.result.stderr;
|
|
2407
|
+
await writeShellPresetProcessReceipt({
|
|
2408
|
+
runDir: input.runDir,
|
|
2409
|
+
nodeId: input.nodeId,
|
|
2410
|
+
preset: input.preset,
|
|
2411
|
+
ok: input.result.ok,
|
|
2412
|
+
durationMs: input.result.durationMs,
|
|
2413
|
+
steps: [
|
|
2414
|
+
{
|
|
2415
|
+
id: "run",
|
|
2416
|
+
label: input.preset,
|
|
2417
|
+
ok: input.result.ok,
|
|
2418
|
+
detail: detail?.slice(0, 8 * 1024),
|
|
2419
|
+
durationMs: input.result.durationMs,
|
|
2420
|
+
},
|
|
2421
|
+
],
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
function namedShellHandler(shell) {
|
|
2425
|
+
if (!shell)
|
|
2426
|
+
return null;
|
|
2427
|
+
const record = shell;
|
|
2428
|
+
for (const key of [
|
|
2429
|
+
"finalWriteSetApprovalGate",
|
|
2430
|
+
"frontendTestStandardScenarios",
|
|
2431
|
+
"frontendTestEnvironmentProbe",
|
|
2432
|
+
"frontendBrowserToolPreflight",
|
|
2433
|
+
"frontendPrewriteGate",
|
|
2434
|
+
"frontendLintBaseline",
|
|
2435
|
+
"frontendVerificationBundle",
|
|
2436
|
+
"frontendReviewContext",
|
|
2437
|
+
"frontendTestCaseChecklist",
|
|
2438
|
+
"frontendTestCaseManifest",
|
|
2439
|
+
"frontendTestEvidenceValidation",
|
|
2440
|
+
"frontendTestResultFinalize",
|
|
2441
|
+
"frontendTestL5Report",
|
|
2442
|
+
"frontendTestHtmlReport",
|
|
2443
|
+
"frontendTestReports",
|
|
2444
|
+
"backendTestPipeline",
|
|
2445
|
+
"jsonArtifactGate",
|
|
2446
|
+
"projectGovernanceGate",
|
|
2447
|
+
"requirementCoverageGate",
|
|
2448
|
+
"verdictGate",
|
|
2449
|
+
"verifyEvidence",
|
|
2450
|
+
"repairArtifactGate",
|
|
2451
|
+
"jacocoCoverage",
|
|
2452
|
+
]) {
|
|
2453
|
+
if (record[key] != null)
|
|
2454
|
+
return key;
|
|
2455
|
+
}
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
async function executeDagShellNodeInner(input, meta) {
|
|
2059
2459
|
const shell = input.task.shell;
|
|
2060
2460
|
if (shell?.finalWriteSetApprovalGate) {
|
|
2061
2461
|
const started = Date.now();
|
|
@@ -2094,30 +2494,89 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2094
2494
|
};
|
|
2095
2495
|
}
|
|
2096
2496
|
}
|
|
2497
|
+
if (shell?.frontendTestStandardScenarios) {
|
|
2498
|
+
return executeFrontendTestStandardScenarios(input);
|
|
2499
|
+
}
|
|
2500
|
+
if (shell?.frontendTestEnvironmentProbe) {
|
|
2501
|
+
return executeFrontendTestEnvironmentProbe(input);
|
|
2502
|
+
}
|
|
2097
2503
|
if (shell?.frontendBrowserToolPreflight) {
|
|
2098
2504
|
const started = Date.now();
|
|
2505
|
+
const steps = [];
|
|
2506
|
+
const finish = async (result) => {
|
|
2507
|
+
await writeShellPresetProcessReceipt({
|
|
2508
|
+
runDir: meta.runDir,
|
|
2509
|
+
nodeId: input.task.id,
|
|
2510
|
+
preset: "frontendBrowserToolPreflight",
|
|
2511
|
+
ok: result.ok,
|
|
2512
|
+
durationMs: result.durationMs,
|
|
2513
|
+
steps: steps.length > 0
|
|
2514
|
+
? steps
|
|
2515
|
+
: [
|
|
2516
|
+
{
|
|
2517
|
+
id: "run",
|
|
2518
|
+
label: "浏览器工具能力探测",
|
|
2519
|
+
ok: result.ok,
|
|
2520
|
+
detail: result.ok ? result.stdout : result.stderr,
|
|
2521
|
+
durationMs: result.durationMs,
|
|
2522
|
+
},
|
|
2523
|
+
],
|
|
2524
|
+
});
|
|
2525
|
+
return result;
|
|
2526
|
+
};
|
|
2099
2527
|
try {
|
|
2100
2528
|
if (resolvePiBackend() === "cli-only") {
|
|
2529
|
+
steps.push({
|
|
2530
|
+
id: "pi-backend",
|
|
2531
|
+
label: "Pi SDK 后端可用",
|
|
2532
|
+
ok: false,
|
|
2533
|
+
detail: "CODE_AGENT_PI_BACKEND=cli-only",
|
|
2534
|
+
});
|
|
2101
2535
|
throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
|
|
2102
2536
|
}
|
|
2537
|
+
steps.push({
|
|
2538
|
+
id: "pi-backend",
|
|
2539
|
+
label: "Pi SDK 后端可用",
|
|
2540
|
+
ok: true,
|
|
2541
|
+
});
|
|
2103
2542
|
const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
|
|
2104
2543
|
if (!sdkCapability.ok) {
|
|
2544
|
+
steps.push({
|
|
2545
|
+
id: "sdk-custom-tool",
|
|
2546
|
+
label: "Pi SDK custom-tool 能力",
|
|
2547
|
+
ok: false,
|
|
2548
|
+
detail: sdkCapability.detail,
|
|
2549
|
+
});
|
|
2105
2550
|
throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
|
|
2106
2551
|
}
|
|
2552
|
+
steps.push({
|
|
2553
|
+
id: "sdk-custom-tool",
|
|
2554
|
+
label: "Pi SDK custom-tool 能力",
|
|
2555
|
+
ok: true,
|
|
2556
|
+
});
|
|
2107
2557
|
const toolProbe = await createPlaywrightCliTool({
|
|
2108
2558
|
repoRoot: input.cwd,
|
|
2109
2559
|
runDir: meta.runDir,
|
|
2110
2560
|
nodeId: input.task.id,
|
|
2111
2561
|
caseId: "frontend-browser-preflight",
|
|
2112
2562
|
evidenceDir: "testcase/frontend/evidence/frontend-browser-preflight",
|
|
2113
|
-
// Capability-only registration probe. The tested URL is selected later by
|
|
2114
|
-
// retrieve-frontend-test-context-pi and validated by the environment shell.
|
|
2115
2563
|
baseUrl: "http://localhost:5173/",
|
|
2116
2564
|
inputRoot: "testcase/frontend/fixtures",
|
|
2117
2565
|
});
|
|
2118
2566
|
if (toolProbe.name !== "playwright_cli") {
|
|
2567
|
+
steps.push({
|
|
2568
|
+
id: "structured-tool",
|
|
2569
|
+
label: "注册 playwright_cli",
|
|
2570
|
+
ok: false,
|
|
2571
|
+
detail: toolProbe.name,
|
|
2572
|
+
});
|
|
2119
2573
|
throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
|
|
2120
2574
|
}
|
|
2575
|
+
steps.push({
|
|
2576
|
+
id: "structured-tool",
|
|
2577
|
+
label: "注册 playwright_cli",
|
|
2578
|
+
ok: true,
|
|
2579
|
+
});
|
|
2121
2580
|
const launcher = resolvePlaywrightCliLauncher(input.cwd);
|
|
2122
2581
|
const result = await defaultPlaywrightCliCommandRunner({
|
|
2123
2582
|
executable: launcher.executable,
|
|
@@ -2130,8 +2589,19 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2130
2589
|
if (result.exitCode !== 0 ||
|
|
2131
2590
|
result.timedOut ||
|
|
2132
2591
|
!["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
|
|
2592
|
+
steps.push({
|
|
2593
|
+
id: "playwright-cli-help",
|
|
2594
|
+
label: "playwright-cli --help 合同",
|
|
2595
|
+
ok: false,
|
|
2596
|
+
detail: help.slice(0, 8 * 1024),
|
|
2597
|
+
});
|
|
2133
2598
|
throw new Error("playwright-cli-contract-incompatible");
|
|
2134
2599
|
}
|
|
2600
|
+
steps.push({
|
|
2601
|
+
id: "playwright-cli-help",
|
|
2602
|
+
label: "playwright-cli --help 合同",
|
|
2603
|
+
ok: true,
|
|
2604
|
+
});
|
|
2135
2605
|
const capability = {
|
|
2136
2606
|
schemaVersion: 1,
|
|
2137
2607
|
status: "ready",
|
|
@@ -2142,22 +2612,28 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2142
2612
|
targetUrlAuthority: "retrieve-context-and-environment-probe",
|
|
2143
2613
|
};
|
|
2144
2614
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
|
|
2145
|
-
|
|
2615
|
+
steps.push({
|
|
2616
|
+
id: "capability-artifact",
|
|
2617
|
+
label: "写出 frontend-browser-capability.json",
|
|
2618
|
+
ok: true,
|
|
2619
|
+
detail: JSON.stringify(capability, null, 2),
|
|
2620
|
+
});
|
|
2621
|
+
return await finish({
|
|
2146
2622
|
ok: true,
|
|
2147
2623
|
stdout: JSON.stringify(capability),
|
|
2148
2624
|
stderr: "",
|
|
2149
2625
|
failureCategory: "success",
|
|
2150
2626
|
durationMs: Date.now() - started,
|
|
2151
|
-
};
|
|
2627
|
+
});
|
|
2152
2628
|
}
|
|
2153
2629
|
catch (error) {
|
|
2154
|
-
return {
|
|
2630
|
+
return await finish({
|
|
2155
2631
|
ok: false,
|
|
2156
2632
|
stdout: "",
|
|
2157
2633
|
stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
2158
2634
|
failureCategory: "tool-policy",
|
|
2159
2635
|
durationMs: Date.now() - started,
|
|
2160
|
-
};
|
|
2636
|
+
});
|
|
2161
2637
|
}
|
|
2162
2638
|
}
|
|
2163
2639
|
if (shell?.frontendPrewriteGate) {
|
|
@@ -2246,6 +2722,13 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2246
2722
|
}
|
|
2247
2723
|
if (shell?.jsonArtifactGate) {
|
|
2248
2724
|
const started = Date.now();
|
|
2725
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
2726
|
+
? {
|
|
2727
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
2728
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
2729
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
2730
|
+
}
|
|
2731
|
+
: undefined);
|
|
2249
2732
|
try {
|
|
2250
2733
|
const gate = shell.jsonArtifactGate;
|
|
2251
2734
|
const sourceNodeId = gate.fallbackFromNodeIds?.length
|
|
@@ -2273,6 +2756,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2273
2756
|
fromNodeId: sourceNodeId,
|
|
2274
2757
|
artifactName: gate.artifactName,
|
|
2275
2758
|
outputDir: gate.outputDir,
|
|
2759
|
+
allowedTestRoot: backendLayout.testRoot,
|
|
2276
2760
|
});
|
|
2277
2761
|
break;
|
|
2278
2762
|
case "backend-test-result-v1":
|
|
@@ -2282,6 +2766,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2282
2766
|
artifactName: gate.artifactName,
|
|
2283
2767
|
outputDir: gate.outputDir,
|
|
2284
2768
|
junitRelativePath: gate.junitRelativePath,
|
|
2769
|
+
testRoot: backendLayout.testRoot,
|
|
2285
2770
|
});
|
|
2286
2771
|
break;
|
|
2287
2772
|
case "backend-test-classification-v1":
|
|
@@ -2570,6 +3055,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2570
3055
|
const cwd = resolveShellCwd(input.cwd, shell.cwd);
|
|
2571
3056
|
const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
2572
3057
|
const failFast = shell.failFast !== false;
|
|
3058
|
+
const attemptNumber = input.attempt ?? 1;
|
|
2573
3059
|
const results = [];
|
|
2574
3060
|
const executedCommandKeys = new Set();
|
|
2575
3061
|
let beforeStatus;
|
|
@@ -2665,8 +3151,8 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2665
3151
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2666
3152
|
reportActivity: input.reportActivity,
|
|
2667
3153
|
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`),
|
|
3154
|
+
stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
|
|
3155
|
+
stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
|
|
2670
3156
|
},
|
|
2671
3157
|
});
|
|
2672
3158
|
results.push(result);
|
|
@@ -2725,6 +3211,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2725
3211
|
schemaVersion: 1,
|
|
2726
3212
|
runId: meta.runId,
|
|
2727
3213
|
nodeId: input.task.id,
|
|
3214
|
+
attempt: attemptNumber,
|
|
2728
3215
|
workspaceFingerprint: workspaceFingerprint ?? null,
|
|
2729
3216
|
failFast,
|
|
2730
3217
|
commands: results.map((result) => ({
|
|
@@ -2743,6 +3230,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2743
3230
|
subchecks: parseCheckRepoSubcheckDurations(result),
|
|
2744
3231
|
})),
|
|
2745
3232
|
};
|
|
3233
|
+
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
|
|
2746
3234
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
|
|
2747
3235
|
const summaryMarkdown = buildShellResultSummaryMarkdown({
|
|
2748
3236
|
nodeId: input.task.id,
|
|
@@ -2794,8 +3282,24 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2794
3282
|
stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
|
|
2795
3283
|
stderr: stderrParts.filter(Boolean).join("\n\n"),
|
|
2796
3284
|
failureCategory,
|
|
3285
|
+
...(firstFailure?.failureCategory
|
|
3286
|
+
? { rawFailureCategory: firstFailure.failureCategory }
|
|
3287
|
+
: {}),
|
|
2797
3288
|
durationMs: Date.now() - started,
|
|
2798
3289
|
// extra field consumed by node-execution if supported; also embedded in stdout marker
|
|
2799
3290
|
...{ commandResults },
|
|
2800
3291
|
};
|
|
2801
3292
|
}
|
|
3293
|
+
export async function executeDagShellNode(input, meta) {
|
|
3294
|
+
const handler = namedShellHandler(input.task.shell);
|
|
3295
|
+
const result = await executeDagShellNodeInner(input, meta);
|
|
3296
|
+
if (handler) {
|
|
3297
|
+
await ensureShellPresetProcessReceipt({
|
|
3298
|
+
runDir: meta.runDir,
|
|
3299
|
+
nodeId: input.task.id,
|
|
3300
|
+
preset: handler,
|
|
3301
|
+
result,
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
return result;
|
|
3305
|
+
}
|