@tea-agent/loop-agent 0.38.1 → 0.39.0-beta.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/CHANGELOG.md +115 -13
- package/README.md +2 -0
- 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/commands/init.js +2 -14
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +41 -4
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-sdk-executor.js +7 -26
- package/dist/executors/shell-executor.js +270 -16
- package/dist/governance/manifest-types.js +0 -2
- package/dist/shared/operator/capabilities.js +87 -22
- 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/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/pi-runtime.js +10 -5
- package/dist/worker/console/chat/routes.js +458 -47
- package/dist/worker/console/chat/semantic-activity.js +13 -13
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/harness-pi-model-matrix.js +2 -11
- package/dist/worker/console/operator-actions.js +5 -4
- 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/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
- package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-Bj2M8iO5.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DvBN4JZc.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CUZgsiRC.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
- package/dist/worker/console/static/assets/index-DTZOKgAn.js +404 -0
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DzqWXNO_.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-DdnapKIH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-BVjAT9b8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CcfkA-Dq.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DH8FjnOK.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +0 -12
- 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 +38 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -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 +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +809 -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-validator-registrations.js +2 -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/frontend-implementation-contract.js +319 -13
- package/dist/workflows/dag/frontend-prewrite-gate.js +200 -45
- package/dist/workflows/dag/init-hybrid.js +689 -91
- package/dist/workflows/dag/node-execution.js +71 -18
- package/dist/workflows/dag/prompt.js +68 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +93 -27
- package/dist/workflows/dag/rerun-run.js +3 -0
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/types.js +104 -0
- package/dist/workflows/dag/upstream-artifacts.js +4 -0
- 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/backend-test-dag.json +60 -35
- package/docs/templates/harness.schema.json +0 -8
- package/harness.json +1 -8
- package/package.json +8 -4
- 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/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/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
- package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from "./pi-executor.js";
|
|
3
|
+
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractPiEventStopReason, extractAssistantTextFromPiJson, } from "./pi-executor.js";
|
|
4
4
|
import { serializeSessionEvent } from "./pi-event-serializer.js";
|
|
5
5
|
import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
|
|
6
6
|
let sdkSessionFactoryOverride;
|
|
@@ -44,15 +44,15 @@ export async function resetPiSdkReuseScopeForTests() {
|
|
|
44
44
|
export function setPiSdkSessionFactoryForTests(factory) {
|
|
45
45
|
sdkSessionFactoryOverride = factory;
|
|
46
46
|
}
|
|
47
|
-
/** Test hook: simulate SDK import success/failure without relying on
|
|
47
|
+
/** Test hook: simulate SDK import success/failure without relying on installed SDK state. */
|
|
48
48
|
export function setPiSdkImportOverrideForTests(fn) {
|
|
49
49
|
sdkImportOverrideForTests = fn;
|
|
50
50
|
}
|
|
51
|
-
/** Test hook: inject a mock Pi SDK module to exercise resolveSdkSessionFactory without
|
|
51
|
+
/** Test hook: inject a mock Pi SDK module to exercise resolveSdkSessionFactory without loading the real SDK. */
|
|
52
52
|
export function setPiSdkModuleOverrideForTests(fn) {
|
|
53
53
|
sdkModuleOverrideForTests = fn;
|
|
54
54
|
}
|
|
55
|
-
/** Check whether the Pi SDK
|
|
55
|
+
/** Check whether the Pi SDK runtime dependency satisfies the 0.83.0 runtime contract. */
|
|
56
56
|
export async function checkPiSdkAvailability(_repoRoot) {
|
|
57
57
|
if (sdkSessionFactoryOverride) {
|
|
58
58
|
return { ok: true, detail: "pi SDK session factory override active" };
|
|
@@ -68,10 +68,10 @@ export async function checkPiSdkAvailability(_repoRoot) {
|
|
|
68
68
|
typeof ModelRuntime?.create !== "function") {
|
|
69
69
|
return {
|
|
70
70
|
ok: false,
|
|
71
|
-
detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.
|
|
71
|
+
detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.83.0 contract)",
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
-
return { ok: true, detail: "pi SDK 0.
|
|
74
|
+
return { ok: true, detail: "pi SDK 0.83.0 contract available" };
|
|
75
75
|
}
|
|
76
76
|
catch (error) {
|
|
77
77
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -404,25 +404,6 @@ function extractSdkUsageSample(event) {
|
|
|
404
404
|
const responseKey = responseKeyCandidates.find((value) => typeof value === "string" && value.length > 0);
|
|
405
405
|
return responseKey ? { responseKey, tokens } : { tokens };
|
|
406
406
|
}
|
|
407
|
-
/** Extract a non-empty stop reason from a terminal SDK event.
|
|
408
|
-
* Probes multiple field shapes across SDK versions so a missing field fails
|
|
409
|
-
* open (undefined) rather than misclassifying an attempt. */
|
|
410
|
-
function readStopReason(event) {
|
|
411
|
-
const message = isRecord(event.message) ? event.message : undefined;
|
|
412
|
-
const candidates = [
|
|
413
|
-
message?.stop_reason,
|
|
414
|
-
message?.stopReason,
|
|
415
|
-
event.stopReason,
|
|
416
|
-
event.stop_reason,
|
|
417
|
-
];
|
|
418
|
-
for (const candidate of candidates) {
|
|
419
|
-
if (typeof candidate === "string" &&
|
|
420
|
-
candidate.trim().length > 0) {
|
|
421
|
-
return candidate.trim();
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return undefined;
|
|
425
|
-
}
|
|
426
407
|
function aggregateSdkTokenUsage(samples) {
|
|
427
408
|
const identified = new Map();
|
|
428
409
|
let anonymousMaximum = 0;
|
|
@@ -494,7 +475,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
494
475
|
type === "message_end" ||
|
|
495
476
|
type === "agent_end" ||
|
|
496
477
|
type === "agent_settled") {
|
|
497
|
-
const candidate =
|
|
478
|
+
const candidate = extractPiEventStopReason(event);
|
|
498
479
|
if (candidate)
|
|
499
480
|
stopReason = candidate;
|
|
500
481
|
}
|
|
@@ -27,11 +27,13 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
|
|
|
27
27
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
28
28
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
29
29
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
30
|
+
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
31
|
+
import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
|
|
30
32
|
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
31
33
|
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";
|
|
34
|
+
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
35
|
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";
|
|
36
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
35
37
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
36
38
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
37
39
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -48,6 +50,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
48
50
|
phase: "initial",
|
|
49
51
|
repairAttempt: 0,
|
|
50
52
|
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
53
|
+
layout: input.layout,
|
|
51
54
|
});
|
|
52
55
|
const initialArtifacts = await writeBackendScenarioParamArtifacts({
|
|
53
56
|
runDir: input.runDir,
|
|
@@ -73,6 +76,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
73
76
|
phase: "final",
|
|
74
77
|
repairAttempt: 1,
|
|
75
78
|
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
79
|
+
layout: input.layout,
|
|
76
80
|
});
|
|
77
81
|
finalFacts = reassessed.facts;
|
|
78
82
|
finalMarkdown = reassessed.markdown;
|
|
@@ -492,11 +496,114 @@ async function readAdvisoryRunReport(reportsDir, filename, title) {
|
|
|
492
496
|
async function executeBackendTestPipeline(input, meta) {
|
|
493
497
|
const pipeline = input.task.shell?.backendTestPipeline;
|
|
494
498
|
const started = Date.now();
|
|
499
|
+
// Plan A: prefer the frozen layout carried by the compiled spec; fall back
|
|
500
|
+
// to the historical default for hand-written legacy DAGs.
|
|
501
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
502
|
+
? {
|
|
503
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
504
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
505
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
506
|
+
}
|
|
507
|
+
: undefined);
|
|
495
508
|
try {
|
|
496
509
|
if (!pipeline)
|
|
497
510
|
throw new Error("missing backend-test pipeline id");
|
|
498
511
|
const outputs = [];
|
|
499
|
-
if (pipeline === "
|
|
512
|
+
if (pipeline === "ingest-backend-test-gap") {
|
|
513
|
+
// Plan D: deterministic gap ingest. Identity (taskId + requirement
|
|
514
|
+
// sha256) is fail-closed; a mismatched plan blocks every writer.
|
|
515
|
+
const gapDocRel = meta.spec.globalConstraints
|
|
516
|
+
?.map((item) => /^backendTestGapDoc=(.+)$/i.exec(item.trim())?.[1])
|
|
517
|
+
.find(Boolean);
|
|
518
|
+
const binding = meta.spec.sourceBinding;
|
|
519
|
+
const requirementSha256 = binding?.sources.find((s) => s.kind === "requirement")?.sha256 ?? "";
|
|
520
|
+
if (!gapDocRel || !binding || !requirementSha256) {
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
stdout: outputs.join("\n"),
|
|
524
|
+
stderr: "backend-test gap ingest requires the bound gap document path and the task source binding with a requirement sha256",
|
|
525
|
+
failureCategory: "invalid-output",
|
|
526
|
+
durationMs: Date.now() - started,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const gapDocAbs = path.join(input.cwd, ...gapDocRel.split("/"));
|
|
530
|
+
let parsed;
|
|
531
|
+
try {
|
|
532
|
+
parsed = JSON.parse(await readFile(gapDocAbs, "utf8"));
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
stdout: outputs.join("\n"),
|
|
538
|
+
stderr: `backend-test gap document is unreadable JSON at ${gapDocRel}: ${error instanceof Error ? error.message : String(error)}`,
|
|
539
|
+
failureCategory: "invalid-output",
|
|
540
|
+
durationMs: Date.now() - started,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
const doc = backendTestGapDocSchema.safeParse(parsed);
|
|
544
|
+
if (!doc.success) {
|
|
545
|
+
return {
|
|
546
|
+
ok: false,
|
|
547
|
+
stdout: outputs.join("\n"),
|
|
548
|
+
stderr: `invalid backend-test-gap-v1 document: ${doc.error.issues
|
|
549
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
550
|
+
.join("; ")}`,
|
|
551
|
+
failureCategory: "invalid-output",
|
|
552
|
+
durationMs: Date.now() - started,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
// previous N6 facts (may be absent on a fresh checkout)
|
|
556
|
+
const factsPath = path.join(input.cwd, "contracts", "backend-test-case-coverage-facts.json");
|
|
557
|
+
let previousMissingSlots;
|
|
558
|
+
let previousCoveredSlots;
|
|
559
|
+
try {
|
|
560
|
+
const facts = JSON.parse(await readFile(factsPath, "utf8"));
|
|
561
|
+
previousMissingSlots = facts.scenarioPartitions?.missingSlots;
|
|
562
|
+
previousCoveredSlots = (facts.rules ?? []).flatMap((rule) => rule.coveredTestPoints ?? []);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// no previous facts in the workspace: the gap doc alone drives the plan
|
|
566
|
+
}
|
|
567
|
+
// module index comes from the README on disk
|
|
568
|
+
let existingModuleStems = [];
|
|
569
|
+
try {
|
|
570
|
+
const files = await markdownFiles(input.cwd, backendLayout);
|
|
571
|
+
existingModuleStems = files
|
|
572
|
+
.map((file) => path.basename(file, ".md"))
|
|
573
|
+
.filter((stem) => stem.toLowerCase() !== "readme");
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
// no markdown tree: every slot becomes a conflict
|
|
577
|
+
}
|
|
578
|
+
const plan = ingestBackendTestGap({
|
|
579
|
+
gapDoc: doc.data,
|
|
580
|
+
previousMissingSlots,
|
|
581
|
+
previousCoveredSlots,
|
|
582
|
+
existingModuleStems,
|
|
583
|
+
sharedSetupPath: meta.spec.backendTestSharedSetup?.path ?? null,
|
|
584
|
+
layout: backendLayout,
|
|
585
|
+
taskId: binding.taskId,
|
|
586
|
+
requirementSha256,
|
|
587
|
+
});
|
|
588
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
589
|
+
await mkdir(contractsDir, { recursive: true });
|
|
590
|
+
const planPath = path.join(contractsDir, "backend-test-gap-plan-v1.json");
|
|
591
|
+
await writeFile(planPath, JSON.stringify(plan, null, 2), "utf8");
|
|
592
|
+
outputs.push(`gapPlan=${planPath}`, `gapPlanStatus=${plan.status}`);
|
|
593
|
+
if (plan.status === "error") {
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
stdout: outputs.join("\n"),
|
|
597
|
+
stderr: [
|
|
598
|
+
"backend-test gap ingest failed closed (identity/conflicts):",
|
|
599
|
+
...plan.conflicts,
|
|
600
|
+
].join("\n"),
|
|
601
|
+
failureCategory: "invalid-output",
|
|
602
|
+
durationMs: Date.now() - started,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
else if (pipeline === "markdown-environment") {
|
|
500
607
|
const results = await executePipelineCommands(input, meta);
|
|
501
608
|
if (!results.every((result) => result.ok)) {
|
|
502
609
|
const failure = results.find((result) => !result.ok);
|
|
@@ -530,6 +637,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
530
637
|
environmentMarkdown,
|
|
531
638
|
requiredRequirementIds,
|
|
532
639
|
sourceBinding: meta.spec.sourceBinding,
|
|
640
|
+
layout: backendLayout,
|
|
641
|
+
sharedSetup: meta.spec.backendTestSharedSetup ?? null,
|
|
533
642
|
});
|
|
534
643
|
}
|
|
535
644
|
catch (error) {
|
|
@@ -583,6 +692,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
583
692
|
const coverage = await analyzeBackendTestCaseCoverage({
|
|
584
693
|
workspaceRoot: input.cwd,
|
|
585
694
|
sourceBinding: normalizedBinding,
|
|
695
|
+
layout: backendLayout,
|
|
586
696
|
});
|
|
587
697
|
const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
|
|
588
698
|
const contractsDir = path.join(meta.runDir, "contracts");
|
|
@@ -590,6 +700,23 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
590
700
|
const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
|
|
591
701
|
await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
|
|
592
702
|
outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
|
|
703
|
+
// Plan C: scenario-partition slot coverage is fail-closed. General
|
|
704
|
+
// coverage findings stay advisory, but a declared filter axis missing
|
|
705
|
+
// an each-value/omitted slot or its complement slot must block pytest
|
|
706
|
+
// generation until the Markdown declares the slot.
|
|
707
|
+
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));
|
|
708
|
+
if (partitionBlocking.length > 0) {
|
|
709
|
+
return {
|
|
710
|
+
ok: false,
|
|
711
|
+
stdout: outputs.join("\n\n"),
|
|
712
|
+
stderr: [
|
|
713
|
+
"backend-test scenario partitions are incomplete (fail-closed):",
|
|
714
|
+
...partitionBlocking,
|
|
715
|
+
].join("\n"),
|
|
716
|
+
failureCategory: "invalid-output",
|
|
717
|
+
durationMs: Date.now() - started,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
593
720
|
}
|
|
594
721
|
catch (error) {
|
|
595
722
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -606,9 +733,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
606
733
|
}
|
|
607
734
|
}
|
|
608
735
|
else if (pipeline === "markdown-collection-assess") {
|
|
609
|
-
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
|
|
736
|
+
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
610
737
|
let facts;
|
|
611
|
-
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
738
|
+
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
|
|
612
739
|
const priorityOnlyFacts = assessPriorityOnlyBackendPytestModules({
|
|
613
740
|
phase: "initial",
|
|
614
741
|
mappedScripts: resolution.expectedScripts,
|
|
@@ -618,7 +745,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
618
745
|
facts = priorityOnlyFacts;
|
|
619
746
|
}
|
|
620
747
|
else if (resolution.missingScripts.length > 0) {
|
|
621
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
748
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
|
|
622
749
|
facts = assessMissingBackendPytestScripts({
|
|
623
750
|
mappedScripts: resolution.expectedScripts,
|
|
624
751
|
existingMappedScripts: resolution.existingScripts,
|
|
@@ -632,10 +759,11 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
632
759
|
const scenario = await runBackendScenarioParamPreflight({
|
|
633
760
|
workspaceRoot: input.cwd,
|
|
634
761
|
runDir: meta.runDir,
|
|
762
|
+
layout: backendLayout,
|
|
635
763
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
636
764
|
});
|
|
637
765
|
outputs.push(...scenario.outputs);
|
|
638
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
766
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
|
|
639
767
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
640
768
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
641
769
|
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
@@ -669,6 +797,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
669
797
|
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
670
798
|
}
|
|
671
799
|
facts = assessBackendPytestCollection({
|
|
800
|
+
layout: backendLayout,
|
|
672
801
|
phase: "initial",
|
|
673
802
|
mappedScripts,
|
|
674
803
|
inventory,
|
|
@@ -678,6 +807,43 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
678
807
|
fixtureResolution,
|
|
679
808
|
});
|
|
680
809
|
}
|
|
810
|
+
try {
|
|
811
|
+
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
812
|
+
workspaceRoot: input.cwd,
|
|
813
|
+
layout: backendLayout,
|
|
814
|
+
taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
|
|
815
|
+
});
|
|
816
|
+
const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
|
|
817
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
818
|
+
await mkdir(contractsDir, { recursive: true });
|
|
819
|
+
const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
|
|
820
|
+
await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
|
|
821
|
+
outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
|
|
822
|
+
if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
|
|
823
|
+
const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
|
|
824
|
+
.filter((entry) => entry.status !== "EXACT_1_TO_1")
|
|
825
|
+
.flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
|
|
826
|
+
.filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
|
|
827
|
+
if (correspondenceRepairPaths.length > 0) {
|
|
828
|
+
facts.status = "REPAIRABLE";
|
|
829
|
+
facts.repairEligible = true;
|
|
830
|
+
facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
|
|
831
|
+
facts.findings.push({
|
|
832
|
+
kind: "markdown-pytest-correspondence",
|
|
833
|
+
classification: "test-asset-defect",
|
|
834
|
+
repairability: "repairable",
|
|
835
|
+
detail: correspondence.facts.entries
|
|
836
|
+
.filter((entry) => entry.status !== "EXACT_1_TO_1")
|
|
837
|
+
.flatMap((entry) => entry.findings)
|
|
838
|
+
.join("; ")
|
|
839
|
+
.slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
|
|
846
|
+
}
|
|
681
847
|
const artifacts = await writeBackendPytestCollectionArtifacts({
|
|
682
848
|
runDir: meta.runDir,
|
|
683
849
|
stem: "initial",
|
|
@@ -711,14 +877,15 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
711
877
|
});
|
|
712
878
|
}
|
|
713
879
|
else if (initial.status === "REPAIRABLE") {
|
|
714
|
-
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
880
|
+
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
715
881
|
const scenario = await runBackendScenarioParamPreflight({
|
|
716
882
|
workspaceRoot: input.cwd,
|
|
717
883
|
runDir: meta.runDir,
|
|
884
|
+
layout: backendLayout,
|
|
718
885
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
719
886
|
});
|
|
720
887
|
outputs.push(...scenario.outputs);
|
|
721
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
888
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
|
|
722
889
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
723
890
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
724
891
|
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
@@ -753,6 +920,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
753
920
|
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
754
921
|
}
|
|
755
922
|
const finalFacts = assessBackendPytestCollection({
|
|
923
|
+
layout: backendLayout,
|
|
756
924
|
phase: "final",
|
|
757
925
|
mappedScripts,
|
|
758
926
|
inventory,
|
|
@@ -776,12 +944,57 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
776
944
|
facts: effective,
|
|
777
945
|
});
|
|
778
946
|
const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
|
|
947
|
+
const correspondence = meta.spec.sourceBinding
|
|
948
|
+
? await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
949
|
+
workspaceRoot: input.cwd,
|
|
950
|
+
layout: backendLayout,
|
|
951
|
+
taskId: meta.spec.sourceBinding.taskId,
|
|
952
|
+
})
|
|
953
|
+
: undefined;
|
|
954
|
+
const eligibility = correspondence
|
|
955
|
+
? buildBackendTestItemEligibility(effective.collectedItemIds, {
|
|
956
|
+
correspondenceEntries: correspondence.facts.entries,
|
|
957
|
+
scenarioEntries: scenarioFacts.entries,
|
|
958
|
+
})
|
|
959
|
+
: { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
|
|
960
|
+
const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
|
|
961
|
+
const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
|
|
962
|
+
await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
|
|
963
|
+
await writeFile(eligibilityFactsPath, `${JSON.stringify({
|
|
964
|
+
schemaId: "backend-test-execution-eligibility-v1",
|
|
965
|
+
status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
|
|
966
|
+
collectedItemCount: effective.collectedItemIds.length,
|
|
967
|
+
eligibleItemCount: eligibility.eligibleItemIds.length,
|
|
968
|
+
excludedItemCount: eligibility.excludedItems.length,
|
|
969
|
+
eligibleItemIds: eligibility.eligibleItemIds,
|
|
970
|
+
excludedItems: eligibility.excludedItems,
|
|
971
|
+
inputHashes: eligibilityInputHashes,
|
|
972
|
+
}, null, 2)}\n`, "utf8");
|
|
973
|
+
const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
|
|
974
|
+
"# Backend Test Execution Eligibility",
|
|
975
|
+
"",
|
|
976
|
+
"## Status",
|
|
977
|
+
"",
|
|
978
|
+
eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
|
|
979
|
+
"",
|
|
980
|
+
`- Collected items: ${effective.collectedItemIds.length}`,
|
|
981
|
+
`- Eligible items: ${eligibility.eligibleItemIds.length}`,
|
|
982
|
+
`- Excluded items: ${eligibility.excludedItems.length}`,
|
|
983
|
+
"",
|
|
984
|
+
"## Excluded Items",
|
|
985
|
+
"",
|
|
986
|
+
...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
|
|
987
|
+
"",
|
|
988
|
+
].join("\n"));
|
|
989
|
+
outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
|
|
779
990
|
const readiness = await materializeBackendTestExecutionReadiness({
|
|
780
991
|
runDir: meta.runDir,
|
|
781
992
|
workspaceRoot: input.cwd,
|
|
782
993
|
effective,
|
|
783
994
|
scenarioParamStatus: scenarioFacts.overallStatus,
|
|
784
995
|
scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
|
|
996
|
+
eligibility,
|
|
997
|
+
eligibilityInputHashes,
|
|
785
998
|
});
|
|
786
999
|
return {
|
|
787
1000
|
ok: true,
|
|
@@ -792,6 +1005,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
792
1005
|
collectedItemCount: effective.collectedItemCount,
|
|
793
1006
|
fixtureResolutionStatus: effective.fixtureResolutionStatus,
|
|
794
1007
|
executionReadinessStatus: readiness.status,
|
|
1008
|
+
eligibleItemCount: readiness.eligibleItemIds.length,
|
|
1009
|
+
excludedItemCount: readiness.excludedItems.length,
|
|
795
1010
|
factsPath: "contracts/backend-test-pytest-collection-effective.json",
|
|
796
1011
|
readinessPath: "contracts/backend-test-execution-readiness.json",
|
|
797
1012
|
reportPath: artifacts.reportPath,
|
|
@@ -826,6 +1041,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
826
1041
|
try {
|
|
827
1042
|
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
828
1043
|
workspaceRoot: input.cwd,
|
|
1044
|
+
layout: backendLayout,
|
|
829
1045
|
taskId: sourceBinding.taskId,
|
|
830
1046
|
});
|
|
831
1047
|
const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
|
|
@@ -888,9 +1104,32 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
888
1104
|
else if (pipeline === "markdown-execute-html") {
|
|
889
1105
|
const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
|
|
890
1106
|
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
|
|
891
|
-
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
892
|
-
|
|
893
|
-
|
|
1107
|
+
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
1108
|
+
let eligibleItemIds = executionReadiness.eligibleItemIds;
|
|
1109
|
+
// Plan D gap-fill executeScope=affected: when a gap plan exists in
|
|
1110
|
+
// this run, prefer the newly planned items (pytest node ids contain
|
|
1111
|
+
// the TP slot id) plus every collected item of the patched modules;
|
|
1112
|
+
// scope=all keeps the full eligible set.
|
|
1113
|
+
const affectedScope = meta.spec.globalConstraints?.some((item) => /^backendTestExecuteScope=affected$/i.test(item.trim())) ?? false;
|
|
1114
|
+
if (affectedScope) {
|
|
1115
|
+
try {
|
|
1116
|
+
const plan = JSON.parse(await readFile(path.join(meta.runDir, "contracts", "backend-test-gap-plan-v1.json"), "utf8"));
|
|
1117
|
+
const newPoints = new Set(plan.newTestPoints ?? []);
|
|
1118
|
+
const modules = new Set((plan.targetModules ?? []).map((stem) => `test_${stem}.py`));
|
|
1119
|
+
const affected = eligibleItemIds.filter((id) => modules.size === 0 ||
|
|
1120
|
+
[...modules].some((script) => id.includes(script)) ||
|
|
1121
|
+
[...newPoints].some((point) => id.includes(point)));
|
|
1122
|
+
if (affected.length > 0)
|
|
1123
|
+
eligibleItemIds = affected;
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
// no gap plan in this run: execute the full eligible set
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
|
|
1130
|
+
const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
|
|
1131
|
+
await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
|
|
1132
|
+
const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
|
|
894
1133
|
// Split into short bash -c commands (aligned with markdown-environment).
|
|
895
1134
|
// A single ultra-long compound command has been observed on Windows to
|
|
896
1135
|
// exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
|
|
@@ -967,8 +1206,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
967
1206
|
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
968
1207
|
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
969
1208
|
'echo "STEP=pytest-run"',
|
|
970
|
-
`echo "- pytestCommand: python -m pytest
|
|
971
|
-
`
|
|
1209
|
+
`echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
|
|
1210
|
+
`echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
|
|
1211
|
+
`echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
|
|
1212
|
+
`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
1213
|
"STATUS=$?",
|
|
973
1214
|
'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
974
1215
|
'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
|
|
@@ -1053,11 +1294,13 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1053
1294
|
htmlContent: pytestHtmlContent,
|
|
1054
1295
|
pytestExitCode,
|
|
1055
1296
|
});
|
|
1056
|
-
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
1297
|
+
const cases = await collectBackendTestHumanCaseCatalog(input.cwd, backendLayout);
|
|
1057
1298
|
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
1058
1299
|
const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
|
|
1059
1300
|
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
1060
|
-
const
|
|
1301
|
+
const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
|
|
1302
|
+
const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
|
|
1303
|
+
const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
|
|
1061
1304
|
const htmlContent = renderBackendTestHtml({
|
|
1062
1305
|
title: meta.spec.title,
|
|
1063
1306
|
parsed,
|
|
@@ -1245,6 +1488,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1245
1488
|
fromNodeId: "backend-test-execution-envelope",
|
|
1246
1489
|
artifactName: "backend-test-execution.json",
|
|
1247
1490
|
outputDir: "contracts",
|
|
1491
|
+
allowedTestRoot: backendLayout.testRoot,
|
|
1248
1492
|
});
|
|
1249
1493
|
outputs.push(`analysis=${analysis.path}`, `execution=${execution.path}`);
|
|
1250
1494
|
}
|
|
@@ -1360,6 +1604,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1360
1604
|
artifactName: "backend-test-result-initial.json",
|
|
1361
1605
|
outputDir: "contracts",
|
|
1362
1606
|
junitRelativePath: "reports/backend-test-initial-junit.xml",
|
|
1607
|
+
testRoot: backendLayout.testRoot,
|
|
1363
1608
|
});
|
|
1364
1609
|
outputs.push(...results.map((result) => result.stdout), `result=${artifact.path}`);
|
|
1365
1610
|
}
|
|
@@ -2246,6 +2491,13 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2246
2491
|
}
|
|
2247
2492
|
if (shell?.jsonArtifactGate) {
|
|
2248
2493
|
const started = Date.now();
|
|
2494
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
2495
|
+
? {
|
|
2496
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
2497
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
2498
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
2499
|
+
}
|
|
2500
|
+
: undefined);
|
|
2249
2501
|
try {
|
|
2250
2502
|
const gate = shell.jsonArtifactGate;
|
|
2251
2503
|
const sourceNodeId = gate.fallbackFromNodeIds?.length
|
|
@@ -2273,6 +2525,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2273
2525
|
fromNodeId: sourceNodeId,
|
|
2274
2526
|
artifactName: gate.artifactName,
|
|
2275
2527
|
outputDir: gate.outputDir,
|
|
2528
|
+
allowedTestRoot: backendLayout.testRoot,
|
|
2276
2529
|
});
|
|
2277
2530
|
break;
|
|
2278
2531
|
case "backend-test-result-v1":
|
|
@@ -2282,6 +2535,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2282
2535
|
artifactName: gate.artifactName,
|
|
2283
2536
|
outputDir: gate.outputDir,
|
|
2284
2537
|
junitRelativePath: gate.junitRelativePath,
|
|
2538
|
+
testRoot: backendLayout.testRoot,
|
|
2285
2539
|
});
|
|
2286
2540
|
break;
|
|
2287
2541
|
case "backend-test-classification-v1":
|
|
@@ -78,8 +78,6 @@ export const executorManifestSchema = z
|
|
|
78
78
|
LOW: executorTierModelSchema.optional(),
|
|
79
79
|
MED: executorTierModelSchema.optional(),
|
|
80
80
|
HIGH: executorTierModelSchema.optional(),
|
|
81
|
-
/** Console 模型矩阵下拉专用列表;仅供 UI 使用,不参与 DAG 模型路由。 */
|
|
82
|
-
modelOptions: z.array(z.string().min(1)).optional(),
|
|
83
81
|
})
|
|
84
82
|
.strict();
|
|
85
83
|
export const workflowPolicyProfileNameSchema = z.enum([
|