@tea-agent/loop-agent 0.16.1 → 0.16.2
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 +21 -0
- package/dist/executors/dag-pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +66 -3
- package/dist/executors/shell-executor.js +212 -29
- package/dist/executors/shell-presets.js +12 -2
- package/dist/executors/shell-write-guard.js +20 -1
- package/dist/shared/git-progress.js +9 -2
- package/dist/worker/observability/read-model.js +56 -0
- package/dist/worker/observe/server.js +6 -3
- package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
- package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
- package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
- package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
- package/dist/workflows/dag/backend-test-result-contract.js +6 -4
- package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
- package/dist/workflows/dag/failure-routing.js +1 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +32 -16
- package/dist/workflows/dag/init-hybrid.js +591 -119
- package/dist/workflows/dag/lifecycle.js +33 -2
- package/dist/workflows/dag/scheduler.js +87 -17
- package/dist/workflows/dag/types.js +31 -0
- package/dist/workflows/dag/validate.js +20 -14
- package/docs/templates/agent-dag.schema.json +25 -2
- package/docs/templates/backend-test-analysis.schema.json +9 -16
- package/docs/templates/backend-test-dag.json +493 -197
- package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
- package/docs/templates/backend-test-execution.schema.json +6 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/hybrid-dag.md +4 -1
|
@@ -18,6 +18,7 @@ import { resolveVerifyPreset } from "../../executors/shell-verification.js";
|
|
|
18
18
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
19
19
|
import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
|
|
20
20
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
21
|
+
import { buildBackendTestEffectiveResultSelectorShellSnippet, buildBackendTestRepairEligibilityShellSnippet, buildBackendTestRepairSafetyShellSnippet, } from "./backend-test-repair-contract.js";
|
|
21
22
|
import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
|
|
22
23
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
23
24
|
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
@@ -932,6 +933,22 @@ function buildDagSourceBinding(sources) {
|
|
|
932
933
|
requirementIds: extractExplicitRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
|
|
933
934
|
};
|
|
934
935
|
}
|
|
936
|
+
function buildBackendTestAnalysisSourceBindingContract(sources) {
|
|
937
|
+
const binding = buildDagSourceBinding(sources);
|
|
938
|
+
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
939
|
+
if (!requirement) {
|
|
940
|
+
throw new Error("backend-test analysis requires a requirement source binding");
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
taskId: binding.taskId,
|
|
944
|
+
requirementPath: requirement.path,
|
|
945
|
+
requirementSha256: requirement.sha256,
|
|
946
|
+
referencePaths: binding.sources
|
|
947
|
+
.filter((source) => source.kind === "reference")
|
|
948
|
+
.map((source) => source.path),
|
|
949
|
+
requirementIds: binding.requirementIds,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
935
952
|
function buildSourceContextBlock(sources) {
|
|
936
953
|
const requirementRef = toTaskRelativeSourcePath(sources, sources.requirementPath);
|
|
937
954
|
const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
|
|
@@ -2424,6 +2441,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2424
2441
|
// Backend test DAG template
|
|
2425
2442
|
// ---------------------------------------------------------------------------
|
|
2426
2443
|
function buildAnalyzeInputsNode(sources) {
|
|
2444
|
+
const sourceBindingContract = buildBackendTestAnalysisSourceBindingContract(sources);
|
|
2427
2445
|
return {
|
|
2428
2446
|
id: "analyze-inputs-pi",
|
|
2429
2447
|
depends_on: [],
|
|
@@ -2433,14 +2451,19 @@ function buildAnalyzeInputsNode(sources) {
|
|
|
2433
2451
|
writePolicy: "read-only",
|
|
2434
2452
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
2435
2453
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2436
|
-
outputContract: "Pure Backend Test Analysis
|
|
2454
|
+
outputContract: "Pure Backend Test Analysis v2 JSON object matching docs/templates/backend-test-analysis.schema.json. No Markdown prose and no file writes.",
|
|
2437
2455
|
subtask_prompt: [
|
|
2438
|
-
"Read the task source materials and return exactly one JSON object matching Backend Test Analysis
|
|
2456
|
+
"Read the task source materials and return exactly one JSON object matching Backend Test Analysis v2.",
|
|
2439
2457
|
"Do not wrap it in explanatory prose. A single fenced json block is tolerated, but pure JSON is preferred.",
|
|
2440
|
-
"Copy
|
|
2441
|
-
"
|
|
2458
|
+
"Copy the sourceBinding object exactly from the JSON block below; do not infer, add, remove, or reclassify source paths.",
|
|
2459
|
+
"Only kind=reference sources belong in referencePaths; kind=constraint sources MUST NOT be included in referencePaths.",
|
|
2460
|
+
"## Exact Backend Test Analysis sourceBinding JSON",
|
|
2461
|
+
JSON.stringify(sourceBindingContract, null, 2),
|
|
2462
|
+
"For every endpoint, explicitly set responseBody.kind=array|object|scalar|empty|unknown and ordering=specified|unspecified|not-applicable. Add itemSchemaRef for arrays when documented.",
|
|
2463
|
+
"For response fields, use comparison=exact|parseable-only|semantic when the source defines assertion semantics; date-time fields whose precision is unspecified should use parseable-only, not string equality.",
|
|
2464
|
+
"Endpoint sourceRefs and field sourceRefs must cite only requirement/reference evidence actually read. Empty sourceRefs are allowed only when normalizing legacy v1 input; newly generated v2 should cite evidence.",
|
|
2442
2465
|
"Use empty arrays for categories not documented. Never include credentials, tokens, private keys, or secret values.",
|
|
2443
|
-
"Required top-level keys: schemaVersion, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
|
|
2466
|
+
"Required top-level keys: schemaVersion=2, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
|
|
2444
2467
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2445
2468
|
buildSourceContextBlock(sources),
|
|
2446
2469
|
].join("\n\n"),
|
|
@@ -2456,13 +2479,13 @@ function buildBackendTestAnalysisContractGateNode(sources) {
|
|
|
2456
2479
|
writePolicy: "read-only",
|
|
2457
2480
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
2458
2481
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2459
|
-
outputContract: "Validated run-owned Backend Test Analysis
|
|
2482
|
+
outputContract: "Validated run-owned Backend Test Analysis v2 artifact pointer, schema ID, and SHA-256 (legacy v1 input is normalized to v2).",
|
|
2460
2483
|
subtask_prompt: "Materialize and validate the backend-test analysis contract under the current DAG run.",
|
|
2461
2484
|
shell: {
|
|
2462
2485
|
commands: [],
|
|
2463
2486
|
jsonArtifactGate: {
|
|
2464
2487
|
fromNodeId: "analyze-inputs-pi",
|
|
2465
|
-
schemaId: "backend-test-analysis-
|
|
2488
|
+
schemaId: "backend-test-analysis-v2",
|
|
2466
2489
|
artifactName: "backend-test-analysis.json",
|
|
2467
2490
|
outputDir: "contracts",
|
|
2468
2491
|
},
|
|
@@ -2489,11 +2512,12 @@ function buildBackendTestEnvironmentScoutNode(sources) {
|
|
|
2489
2512
|
"Discover only non-secret evidence: pytest config files (pytest.ini / pyproject.toml / setup.cfg test paths), candidate test roots, existing fixtures/clients, documented run commands, and env *names* (not values).",
|
|
2490
2513
|
"Do NOT search the whole repo for secrets, .env values, tokens, private keys, or production credentials.",
|
|
2491
2514
|
'framework must be "pytest". Default targetMode to "in-process" unless evidence clearly shows an external service base URL env name or documented managed start/stop with sourceRef.',
|
|
2492
|
-
'Do NOT select targetMode "managed-command" unless task source documents a safe start/stop command with an explicit sourceRef; otherwise leave managedCommand absent and
|
|
2515
|
+
'Do NOT select targetMode "managed-command" unless task source documents a safe start/stop command with an explicit sourceRef; otherwise leave managedCommand absent (do not invent managed mode). For external-running-service, missing managed start/stop is expected and is NOT an evidenceGap.',
|
|
2493
2516
|
"testRoot and workingDirectory must be repo-relative posix paths without .. or absolute form. Adapter default testRoot is testcase when evidence is incomplete.",
|
|
2494
2517
|
"runner must not include secret values. report.format must be junit with a relativeHint under the run (e.g. reports/backend-test-junit.xml).",
|
|
2495
2518
|
"requiredEnvNames lists env NAMES only. baseUrlEnvName is required only for external-running-service and must match ^[A-Z_][A-Z0-9_]*$.",
|
|
2496
|
-
"
|
|
2519
|
+
"evidenceGaps are optional notes only. Do NOT list greenfield/expected-later items as gaps: missing test_*.py / conftest (generate-pytest will create them), missing pytest.ini when testRoot defaults to testcase/, projected schema under ai_workspace/** instead of docs/templates/**, or optional API_BASE_URL when a documented default base URL exists.",
|
|
2520
|
+
"Prefer evidenceGaps: [] for MVP greenfield external pytest. Use evidenceGaps only for true blockers the later generate nodes cannot fix (e.g. no viable testRoot at all). Populate evidenceRefs with repo-relative paths actually read.",
|
|
2497
2521
|
"Required top-level keys: schemaVersion, framework, runner, testRoot, workingDirectory, report, targetMode, existingFixtures, authenticationMode, requiredEnvNames, dataIsolation, evidenceGaps, evidenceRefs.",
|
|
2498
2522
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2499
2523
|
buildSourceContextBlock(sources),
|
|
@@ -2528,7 +2552,10 @@ function buildBackendTestExecutionContractGateNode(sources) {
|
|
|
2528
2552
|
function buildGenerateBackendFunctionalCasesNode(sources) {
|
|
2529
2553
|
return {
|
|
2530
2554
|
id: "generate-backend-functional-cases-pi",
|
|
2531
|
-
depends_on: [
|
|
2555
|
+
depends_on: [
|
|
2556
|
+
"backend-test-analysis-contract-shell",
|
|
2557
|
+
"backend-test-execution-contract-shell",
|
|
2558
|
+
],
|
|
2532
2559
|
role: "implementer",
|
|
2533
2560
|
executor: "pi",
|
|
2534
2561
|
toolProfile: "write",
|
|
@@ -2540,19 +2567,31 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
|
|
|
2540
2567
|
// 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
|
|
2541
2568
|
// 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
|
|
2542
2569
|
subtask_prompt: [
|
|
2543
|
-
"Read
|
|
2544
|
-
,
|
|
2570
|
+
"Read both validated run-owned contracts before generating functional cases:",
|
|
2571
|
+
"- contracts/backend-test-analysis.json: authoritative requirements, AC IDs, endpoints, fields, rules, boundaries, risks, and evidence gaps.",
|
|
2572
|
+
"- contracts/backend-test-execution.json: pytest target mode, base URL env name, readiness, fixtures, and data-isolation constraints.",
|
|
2573
|
+
"Generate cases from the analysis contract; use the execution contract only to keep preconditions and automation feasibility realistic.",
|
|
2574
|
+
"Do not proceed from the execution contract alone. Do not re-read source documents or fall back to free-form analysis.",
|
|
2545
2575
|
"",
|
|
2546
2576
|
"## Output Steps (do in order):",
|
|
2547
2577
|
"1. First, output a brief summary: how many modules, how many cases planned per module",
|
|
2548
2578
|
"2. Then write each test case file under testcase/md/",
|
|
2549
2579
|
"",
|
|
2550
2580
|
"## Format Rules:",
|
|
2551
|
-
"- Each test case ID: BE-<MODULE>-<NNN> (e.g. BE-ORDER-001)",
|
|
2581
|
+
"- Each test case ID: BE-<MODULE>-<NNN> (e.g. BE-ORDER-001) — always write the FULL id; never abbreviate as 002, 003 in matrices",
|
|
2552
2582
|
"- Each file covers one module",
|
|
2553
|
-
"- Case structure: ID, Title, Precondition, Steps, Expected Result",
|
|
2583
|
+
"- Case structure: ID, Title, Acceptance Criteria, Business Rules, Precondition, Steps, Expected Result",
|
|
2584
|
+
"- Every emitted case MUST declare at least one semantically applicable explicit AC-* under Acceptance Criteria; list BR-* separately under Business Rules",
|
|
2585
|
+
"- If a BR-only scenario has no semantically valid in-scope AC, do not create a standalone case for it; record the limitation in the summary for the manifest evidenceGaps instead",
|
|
2586
|
+
"- Never relabel a negative/boundary/BR-only behavior as AC-002 or another unrelated AC merely to make acIds non-empty",
|
|
2554
2587
|
"- Map each case to acceptance criteria (AC-xxx)",
|
|
2555
2588
|
"",
|
|
2589
|
+
"## AC ↔ case consistency (CRITICAL — prevents review request-revision):",
|
|
2590
|
+
"- Every AC-xxx listed on a case body MUST appear only on cases that truly exercise that AC",
|
|
2591
|
+
"- Any AC-coverage matrix / summary table MUST list the same full BE-* case IDs that the case bodies claim — never 'all cases' / '全部用例' unless every case body maps that AC",
|
|
2592
|
+
"- Prefer one primary BE-* case for suite-level ACs (e.g. AC-008 pytest exit 0) rather than tagging every case",
|
|
2593
|
+
"- Out-of-scope ACs (Flyway, frontend e2e, mvn test, etc.) must NOT be claimed in MD; leave them for manifest evidenceGaps",
|
|
2594
|
+
"",
|
|
2556
2595
|
"## Coverage Requirements:",
|
|
2557
2596
|
"- Positive paths: happy path for each acceptance criterion",
|
|
2558
2597
|
"- Negative paths: error scenarios (invalid input, not found, state violations)",
|
|
@@ -2567,16 +2606,28 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
|
|
|
2567
2606
|
"",
|
|
2568
2607
|
"## Constraints:",
|
|
2569
2608
|
"- Stay within writeSet: testcase/md/**",
|
|
2570
|
-
"- Do NOT re-read source documents or fall back to free-form analysis
|
|
2571
|
-
,
|
|
2609
|
+
"- Do NOT re-read source documents or fall back to free-form analysis; use the two validated run-owned contracts only",
|
|
2572
2610
|
"- Do not write root artifacts/**",
|
|
2573
2611
|
].join("\n\n"),
|
|
2574
2612
|
};
|
|
2575
2613
|
}
|
|
2576
|
-
|
|
2614
|
+
const BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS = [
|
|
2615
|
+
"The final fenced JSON block is authoritative and MUST conform exactly to Backend Test Case Manifest v1.",
|
|
2616
|
+
"Top-level keys MUST be exactly: schemaVersion, sourceBinding, cases, evidenceGaps, and optional coverageSummary. Set schemaVersion to numeric 1. Do NOT emit schemaId, manifestType, taskId, modules, acCoverage, brCoverage, dataIsolation, readiness, or other custom top-level keys.",
|
|
2617
|
+
"Copy sourceBinding exactly from contracts/backend-test-analysis.json: taskId, requirementPath, requirementSha256, referencePaths, requirementIds. Preserve Unicode paths exactly; never replace characters in source/需求.md or other paths.",
|
|
2618
|
+
"Each cases[] item MUST use exactly: caseId, non-empty acIds, title, category, automationStatus; optional endpointRef, ruleRefs, file, symbol, gapReason, evidenceRef. Do NOT use id, module, brIds, endpoint, or priority.",
|
|
2619
|
+
"category MUST be exactly one of: positive, negative, boundary, state-transition, auth, timeout, concurrency, other.",
|
|
2620
|
+
"Before pytest generation, set automationStatus=planned. Use generated only with both file and symbol. Use skipped or unsupported only with gapReason.",
|
|
2621
|
+
"Each evidenceGaps[] item MUST use exactly: optional acId, optional caseId, required description, optional evidenceRef. Every gap requires at least acId or caseId. Do NOT use requirementId, relatedBrIds, or sourceRef.",
|
|
2622
|
+
"Every case must map to at least one semantically applicable explicit AC-* in acIds. If no AC applies, omit that case and bind an evidence gap to the nearest applicable acId or caseId; never emit an unbound informational gap.",
|
|
2623
|
+
"acIds MUST exactly match the explicit AC-* values in the written case body; do not infer ACs from Business Rules or summary matrices.",
|
|
2624
|
+
"Use full BE-<MODULE>-<NNN> caseId strings. Do not invent coverage percentages; omit coverageSummary unless all deterministic counts are exact.",
|
|
2625
|
+
"Minimal shape example: {\"schemaVersion\":1,\"sourceBinding\":{\"taskId\":\"...\",\"requirementPath\":\"source/需求.md\",\"requirementSha256\":\"<64 lowercase hex>\",\"referencePaths\":[],\"requirementIds\":[\"AC-001\"]},\"cases\":[{\"caseId\":\"BE-MODULE-001\",\"acIds\":[\"AC-001\"],\"title\":\"...\",\"category\":\"positive\",\"automationStatus\":\"planned\",\"evidenceRef\":\"testcase/md/module.md\"}],\"evidenceGaps\":[]}",
|
|
2626
|
+
].join("\n\n");
|
|
2627
|
+
function buildEmitBackendCaseManifestNode(sources, options) {
|
|
2577
2628
|
return {
|
|
2578
|
-
id: "emit-backend-case-manifest-pi",
|
|
2579
|
-
depends_on: [
|
|
2629
|
+
id: options?.id ?? "emit-backend-case-manifest-pi",
|
|
2630
|
+
depends_on: options?.dependsOn ?? [
|
|
2580
2631
|
"generate-backend-functional-cases-pi",
|
|
2581
2632
|
"backend-test-analysis-contract-shell",
|
|
2582
2633
|
],
|
|
@@ -2589,20 +2640,17 @@ function buildEmitBackendCaseManifestNode(sources) {
|
|
|
2589
2640
|
outputContract: "Pure Backend Test Case Manifest v1 JSON (schema docs/templates/backend-test-case-manifest.schema.json). No file writes; model must not write .harness/**.",
|
|
2590
2641
|
subtask_prompt: [
|
|
2591
2642
|
"Emit Backend Test Case Manifest v1 as pure JSON (or one fenced json block with no trailing text).",
|
|
2643
|
+
BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS,
|
|
2592
2644
|
"Read-only: use validated contracts/backend-test-analysis.json pointer + testcase/md/** only. Do not write repository files or .harness/**.",
|
|
2593
|
-
"sourceBinding must match the analysis contract / DAG source binding exactly (taskId, requirementPath, requirementSha256, referencePaths, requirementIds).",
|
|
2594
|
-
"For each functional case under testcase/md/: caseId BE-<MODULE>-<NNN>, acIds[], title, category, automationStatus.",
|
|
2595
|
-
"After case generation (pre-pytest), default automationStatus=planned. Use skipped/unsupported only with gapReason. Use generated only when file+symbol already exist.",
|
|
2596
|
-
"evidenceGaps: structured gaps for explicit AC-* that cannot be mapped to a case.",
|
|
2597
|
-
"Do NOT invent coverage percentages. Optional coverageSummary must match deterministic counts (gate recomputes/validates).",
|
|
2598
2645
|
"No secrets or credential-shaped fields.",
|
|
2599
2646
|
].join("\n\n"),
|
|
2600
2647
|
};
|
|
2601
2648
|
}
|
|
2602
|
-
function buildBackendTestCaseManifestGateNode(sources) {
|
|
2649
|
+
function buildBackendTestCaseManifestGateNode(sources, options) {
|
|
2650
|
+
const fromNodeId = options?.fromNodeId ?? "emit-backend-case-manifest-pi";
|
|
2603
2651
|
return {
|
|
2604
|
-
id: "backend-test-case-manifest-shell",
|
|
2605
|
-
depends_on: ["emit-backend-case-manifest-pi"],
|
|
2652
|
+
id: options?.id ?? "backend-test-case-manifest-shell",
|
|
2653
|
+
depends_on: options?.dependsOn ?? ["emit-backend-case-manifest-pi"],
|
|
2606
2654
|
role: "verifier",
|
|
2607
2655
|
executor: "shell",
|
|
2608
2656
|
complexity: "LOW",
|
|
@@ -2614,7 +2662,7 @@ function buildBackendTestCaseManifestGateNode(sources) {
|
|
|
2614
2662
|
shell: {
|
|
2615
2663
|
commands: [],
|
|
2616
2664
|
jsonArtifactGate: {
|
|
2617
|
-
fromNodeId
|
|
2665
|
+
fromNodeId,
|
|
2618
2666
|
schemaId: "backend-test-case-manifest-v1",
|
|
2619
2667
|
artifactName: "backend-test-case-manifest.json",
|
|
2620
2668
|
outputDir: "contracts",
|
|
@@ -2624,13 +2672,18 @@ function buildBackendTestCaseManifestGateNode(sources) {
|
|
|
2624
2672
|
},
|
|
2625
2673
|
};
|
|
2626
2674
|
}
|
|
2627
|
-
function buildBackendTestTraceabilityGateNode(sources) {
|
|
2675
|
+
function buildBackendTestTraceabilityGateNode(sources, options = {}) {
|
|
2628
2676
|
return {
|
|
2629
|
-
id: "backend-test-traceability-gate-shell",
|
|
2630
|
-
depends_on: [
|
|
2677
|
+
id: options.id ?? "backend-test-traceability-gate-shell",
|
|
2678
|
+
depends_on: options.dependsOn ?? [
|
|
2631
2679
|
"generate-backend-pytest-pi",
|
|
2680
|
+
// Effective Case Manifest v1 path (exclusive condition branches):
|
|
2681
|
+
// pass → first manifest shell; request-revision → final manifest shell.
|
|
2682
|
+
// Artifact path is always contracts/backend-test-case-manifest.json.
|
|
2632
2683
|
"backend-test-case-manifest-shell",
|
|
2684
|
+
"backend-test-case-manifest-final-shell",
|
|
2633
2685
|
],
|
|
2686
|
+
dependsPolicy: "all-or-condition-skip",
|
|
2634
2687
|
role: "verifier",
|
|
2635
2688
|
executor: "shell",
|
|
2636
2689
|
complexity: "LOW",
|
|
@@ -2646,10 +2699,12 @@ function buildBackendTestTraceabilityGateNode(sources) {
|
|
|
2646
2699
|
},
|
|
2647
2700
|
};
|
|
2648
2701
|
}
|
|
2649
|
-
function buildReviewBackendCasesNode(sources) {
|
|
2702
|
+
function buildReviewBackendCasesNode(sources, options) {
|
|
2703
|
+
const phase = options?.phase ?? "first";
|
|
2704
|
+
const isFinal = phase === "final";
|
|
2650
2705
|
return {
|
|
2651
|
-
id: "review-backend-cases-pi",
|
|
2652
|
-
depends_on: [
|
|
2706
|
+
id: options?.id ?? "review-backend-cases-pi",
|
|
2707
|
+
depends_on: options?.dependsOn ?? [
|
|
2653
2708
|
"backend-test-case-manifest-shell",
|
|
2654
2709
|
"backend-test-analysis-contract-shell",
|
|
2655
2710
|
],
|
|
@@ -2659,21 +2714,25 @@ function buildReviewBackendCasesNode(sources) {
|
|
|
2659
2714
|
writePolicy: "read-only",
|
|
2660
2715
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
2661
2716
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2662
|
-
outputContract:
|
|
2717
|
+
outputContract: isFinal
|
|
2718
|
+
? "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision after at most one revision; followed by Findings and Coverage Assessment. No file writes. Final gate accepts pass only."
|
|
2719
|
+
: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings, Coverage Assessment, and Required revisions when requesting revision. No file writes.",
|
|
2663
2720
|
subtask_prompt: [
|
|
2664
|
-
|
|
2721
|
+
isFinal
|
|
2722
|
+
? "Final review of backend functional cases under testcase/md/ and the re-validated Case Manifest v1 after at most one revision pass."
|
|
2723
|
+
: "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
|
|
2665
2724
|
"",
|
|
2666
2725
|
"## Mandatory First Line:",
|
|
2667
2726
|
"First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
|
|
2668
2727
|
"",
|
|
2669
2728
|
"## Review Checklist:",
|
|
2670
|
-
"- ID format: every case uses BE-<MODULE>-<NNN>",
|
|
2671
|
-
"- Positive coverage: each acceptance criterion (AC-xxx) has happy-path case",
|
|
2729
|
+
"- ID format: every case uses BE-<MODULE>-<NNN> (full ids only in bodies and matrices)",
|
|
2730
|
+
"- Positive coverage: each in-scope acceptance criterion (AC-xxx) has happy-path case",
|
|
2672
2731
|
"- Negative coverage: error scenarios (invalid input, not found, state violations)",
|
|
2673
|
-
"- Traceability: each AC maps to
|
|
2732
|
+
"- Traceability: each explicit AC maps to a case ID or an evidenceGap in contracts/backend-test-case-manifest.json",
|
|
2674
2733
|
"- Case structure: ID, Title, Precondition, Steps, Expected Result",
|
|
2675
2734
|
"- No duplicate IDs across files",
|
|
2676
|
-
"- Manifest consistency: MD
|
|
2735
|
+
"- Manifest consistency (Critical): every AC claimed in MD case bodies/matrices must match manifest caseId→acIds; never accept 'all cases cover AC-xxx' unless every case maps that AC",
|
|
2677
2736
|
"",
|
|
2678
2737
|
"## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
|
|
2679
2738
|
"- Boundary coverage: check ONLY if analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
|
|
@@ -2683,40 +2742,123 @@ function buildReviewBackendCasesNode(sources) {
|
|
|
2683
2742
|
"- Concurrency coverage: check ONLY if analyze-inputs-pi mentions concurrency/idempotency rules",
|
|
2684
2743
|
"- If not mentioned, do NOT flag as missing",
|
|
2685
2744
|
"",
|
|
2745
|
+
"## Do NOT treat as Critical alone:",
|
|
2746
|
+
"- Missing test_*.py / automation still planned (expected before generate-backend-pytest-pi)",
|
|
2747
|
+
"- Out-of-scope ACs already listed in manifest evidenceGaps (Flyway, frontend e2e, mvn test)",
|
|
2748
|
+
"",
|
|
2686
2749
|
"## Verdict Rules:",
|
|
2687
2750
|
"- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
|
|
2688
2751
|
"- Any Critical fails OR Important > 2 → VERDICT: request-revision",
|
|
2752
|
+
isFinal
|
|
2753
|
+
? "- This is the FINAL review after one revision opportunity; remaining Critical issues must still request-revision (final gate will stop the DAG)."
|
|
2754
|
+
: "- When requesting revision, list numbered Required revisions concrete enough for revise-backend-cases-pi to edit testcase/md/**.",
|
|
2689
2755
|
"",
|
|
2690
2756
|
"## Output After Verdict:",
|
|
2691
|
-
"1. Coverage Assessment table (AC → case IDs) using manifest + MD",
|
|
2757
|
+
"1. Coverage Assessment table (AC → full BE-* case IDs) using manifest + MD",
|
|
2692
2758
|
"2. Findings list (Critical/Important/Informational)",
|
|
2693
2759
|
"3. Statistics (total cases, positive/negative/boundary breakdown)",
|
|
2760
|
+
isFinal ? "" : "4. Required revisions (only when request-revision)",
|
|
2694
2761
|
"",
|
|
2695
2762
|
"## Constraints:",
|
|
2696
2763
|
"- Read-only: do not modify files",
|
|
2697
2764
|
"- Read validated analysis + case manifest artifacts; do not recompute coverage percentages",
|
|
2698
2765
|
"- Use testcase/md/ files for case review",
|
|
2766
|
+
]
|
|
2767
|
+
.filter((line) => line !== "")
|
|
2768
|
+
.join("\n\n"),
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
function buildReviewBackendCasesBranchConditionNode(sources) {
|
|
2772
|
+
return {
|
|
2773
|
+
id: "review-backend-cases-branch-condition",
|
|
2774
|
+
depends_on: ["review-backend-cases-pi"],
|
|
2775
|
+
role: "verifier",
|
|
2776
|
+
executor: "static",
|
|
2777
|
+
complexity: "LOW",
|
|
2778
|
+
writePolicy: "none",
|
|
2779
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
2780
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2781
|
+
outputContract: "Dynamic condition: select direct pass vs single revision chain from first review firstVerdictLine; malformed VERDICT fails closed (no default). The pass target is the already-finished first review, so the effective gate remains outside the exclusive branch targets.",
|
|
2782
|
+
subtask_prompt: "Branch on review-backend-cases-pi VERDICT: pass → already-finished first review (revision chain skipped); request-revision → revise-backend-cases-pi.",
|
|
2783
|
+
static: {
|
|
2784
|
+
resultMarkdown: "Backend case review branch condition (direct pass vs single revision).",
|
|
2785
|
+
},
|
|
2786
|
+
dynamicCondition: {
|
|
2787
|
+
workflowNodeId: "review-backend-cases-branch-condition",
|
|
2788
|
+
cases: [
|
|
2789
|
+
{
|
|
2790
|
+
when: "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: pass'",
|
|
2791
|
+
then: "review-backend-cases-pi",
|
|
2792
|
+
},
|
|
2793
|
+
{
|
|
2794
|
+
when: "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
|
|
2795
|
+
then: "revise-backend-cases-pi",
|
|
2796
|
+
},
|
|
2797
|
+
],
|
|
2798
|
+
},
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
function buildReviseBackendCasesNode(sources) {
|
|
2802
|
+
return {
|
|
2803
|
+
id: "revise-backend-cases-pi",
|
|
2804
|
+
depends_on: ["review-backend-cases-branch-condition"],
|
|
2805
|
+
role: "implementer",
|
|
2806
|
+
executor: "pi",
|
|
2807
|
+
toolProfile: "write",
|
|
2808
|
+
complexity: "MED",
|
|
2809
|
+
writePolicy: "exclusive",
|
|
2810
|
+
writeSet: ["testcase/md/**"],
|
|
2811
|
+
allowedPaths: ["testcase/md/**"],
|
|
2812
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2813
|
+
outputContract: "Only scheduled when first review is VERDICT: request-revision. Apply Required revisions under testcase/md/** then summarize changes (single revision pass).",
|
|
2814
|
+
subtask_prompt: [
|
|
2815
|
+
"You are the single backend case revision pass (max one per DAG run).",
|
|
2816
|
+
"This node is only scheduled when review-backend-cases-pi emitted VERDICT: request-revision.",
|
|
2817
|
+
"",
|
|
2818
|
+
"## Inputs",
|
|
2819
|
+
"- First review: review-backend-cases-pi Markdown (VERDICT + Findings + Required revisions)",
|
|
2820
|
+
"- Current cases: testcase/md/**",
|
|
2821
|
+
"- Validated analysis + case manifest under the current run contracts/",
|
|
2822
|
+
"",
|
|
2823
|
+
"## Required work",
|
|
2824
|
+
"1. Edit only testcase/md/** to address every Critical finding and Required revision item",
|
|
2825
|
+
"2. Fix AC matrices to list FULL BE-* ids matching case bodies; remove false 'all cases' AC claims",
|
|
2826
|
+
"3. Keep BE-<MODULE>-<NNN> ids stable when possible; do not invent out-of-scope AC coverage",
|
|
2827
|
+
"4. Stay within writeSet; do not write pytest or production code",
|
|
2828
|
+
"5. End with a short summary of files touched",
|
|
2829
|
+
"",
|
|
2830
|
+
"Downstream will re-emit and re-validate Case Manifest v1, then run a FINAL review (pass-only gate).",
|
|
2699
2831
|
].join("\n\n"),
|
|
2700
2832
|
};
|
|
2701
2833
|
}
|
|
2702
2834
|
function buildReviewBackendCasesGateNode(sources) {
|
|
2703
2835
|
return {
|
|
2704
2836
|
id: "review-backend-cases-gate-shell",
|
|
2705
|
-
|
|
2837
|
+
// OR-join tips: pass-path barrier vs final review after one revise.
|
|
2838
|
+
// Soft condition-skip on the unused tip still allows the gate to run.
|
|
2839
|
+
depends_on: [
|
|
2840
|
+
"review-backend-cases-branch-condition",
|
|
2841
|
+
"review-backend-cases-final-pi",
|
|
2842
|
+
// Always finished; also required for verdictGate.fallbackFromNodeIds validate.
|
|
2843
|
+
"review-backend-cases-pi",
|
|
2844
|
+
],
|
|
2845
|
+
dependsPolicy: "all-or-condition-skip",
|
|
2706
2846
|
role: "verifier",
|
|
2707
2847
|
executor: "shell",
|
|
2708
2848
|
complexity: "LOW",
|
|
2709
2849
|
writePolicy: "read-only",
|
|
2710
2850
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
2711
2851
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2712
|
-
outputContract: "Deterministic backend case review gate: exit 0 only when review-backend-
|
|
2713
|
-
subtask_prompt: "Deterministic gate: block pytest generation unless backend case review emitted VERDICT: pass.",
|
|
2852
|
+
outputContract: "Deterministic backend case review gate: exit 0 only when the effective review emits VERDICT: pass (sole authorization for generate-backend-pytest-pi). Prefers final review JSON when present (revision path); else first review (pass path).",
|
|
2853
|
+
subtask_prompt: "Deterministic gate: block pytest generation unless the effective backend case review (final after revision, else first) emitted VERDICT: pass.",
|
|
2714
2854
|
shell: {
|
|
2715
2855
|
commands: [],
|
|
2716
2856
|
verdictGate: {
|
|
2717
|
-
|
|
2857
|
+
// Prefer final (revision path) when its artifact exists; fall back to first review.
|
|
2858
|
+
fromNodeId: "review-backend-cases-final-pi",
|
|
2859
|
+
fallbackFromNodeIds: ["review-backend-cases-pi"],
|
|
2718
2860
|
accept: ["VERDICT: pass"],
|
|
2719
|
-
label: "backend case review",
|
|
2861
|
+
label: "backend case effective review",
|
|
2720
2862
|
lineMode: "first-verdict-line",
|
|
2721
2863
|
},
|
|
2722
2864
|
cwd: ".",
|
|
@@ -2845,7 +2987,149 @@ function buildGenerateBackendPytestNode(sources) {
|
|
|
2845
2987
|
].join("\n\n"),
|
|
2846
2988
|
};
|
|
2847
2989
|
}
|
|
2848
|
-
function
|
|
2990
|
+
function buildBackendTestSemanticReviewNode(sources, options = {}) {
|
|
2991
|
+
const final = options.final ?? false;
|
|
2992
|
+
return {
|
|
2993
|
+
id: options.id ?? "review-generated-backend-pytest-pi",
|
|
2994
|
+
depends_on: options.dependsOn ?? [
|
|
2995
|
+
"generate-backend-pytest-pi",
|
|
2996
|
+
"backend-test-analysis-contract-shell",
|
|
2997
|
+
"backend-test-case-manifest-shell",
|
|
2998
|
+
"backend-test-case-manifest-final-shell",
|
|
2999
|
+
],
|
|
3000
|
+
dependsPolicy: "all-or-condition-skip",
|
|
3001
|
+
role: "reviewer",
|
|
3002
|
+
executor: "pi",
|
|
3003
|
+
complexity: "MED",
|
|
3004
|
+
writePolicy: "read-only",
|
|
3005
|
+
allowedPaths: ["testcase/**"],
|
|
3006
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3007
|
+
outputContract: "Pure Backend Test Semantic Review v1 JSON: verdict, findings[], summary. No file writes.",
|
|
3008
|
+
subtask_prompt: [
|
|
3009
|
+
final ? "Final semantic review after the single generated-pytest revision." : "Review generated pytest semantics before the first execution.",
|
|
3010
|
+
"Use only compact authoritative inputs: contracts/backend-test-analysis.json, contracts/backend-test-case-manifest.json, testcase/md/**, and generated testcase/**/test_*.py/helpers/factories.",
|
|
3011
|
+
"Return exactly one pure JSON object with only verdict, findings, summary; no Markdown fence or surrounding prose.",
|
|
3012
|
+
"verdict must be pass or request-revision. Each findings[] item must contain exactly severity, caseId, testFile, testSymbol, contractRefs, issue, requiredChange.",
|
|
3013
|
+
"severity must be exactly Critical, Important, or Informational; contractRefs must be a non-empty string array. A request-revision verdict requires at least one finding; pass must not contain Critical findings.",
|
|
3014
|
+
"Minimal shape: {\"verdict\":\"pass\",\"findings\":[],\"summary\":\"No contract-backed semantic contradiction found.\"}",
|
|
3015
|
+
"Check responseBody.kind (array vs object/items), ordering, field comparison (especially parseable-only date-time precision), documented status/error fields, and each caseId→symbol assertion meaning.",
|
|
3016
|
+
"Do not use aliases such as file, symbol, refs, finding, or requiredFix; the strict contract requires testFile, testSymbol, contractRefs, issue, requiredChange.",
|
|
3017
|
+
"request-revision only for concrete semantic contradiction with reviewed cases/formal analysis evidence. No style findings.",
|
|
3018
|
+
"Read-only; do not edit tests or production code.",
|
|
3019
|
+
].join("\n\n"),
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
function buildBackendTestSemanticReviewGateNode(sources, options = {}) {
|
|
3023
|
+
const fromNodeId = options.fromNodeId ?? "review-generated-backend-pytest-pi";
|
|
3024
|
+
return {
|
|
3025
|
+
id: options.id ?? "backend-test-semantic-review-shell",
|
|
3026
|
+
depends_on: options.dependsOn ?? [fromNodeId],
|
|
3027
|
+
role: "verifier",
|
|
3028
|
+
executor: "shell",
|
|
3029
|
+
complexity: "LOW",
|
|
3030
|
+
writePolicy: "read-only",
|
|
3031
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
3032
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3033
|
+
outputContract: "Validated run-owned Backend Test Semantic Review v1 artifact.",
|
|
3034
|
+
subtask_prompt: "Validate semantic review JSON before branch selection.",
|
|
3035
|
+
shell: {
|
|
3036
|
+
commands: [],
|
|
3037
|
+
jsonArtifactGate: {
|
|
3038
|
+
fromNodeId,
|
|
3039
|
+
schemaId: "backend-test-semantic-review-v1",
|
|
3040
|
+
artifactName: options.id?.includes("final") ? "backend-test-semantic-review-final.json" : "backend-test-semantic-review.json",
|
|
3041
|
+
outputDir: "contracts",
|
|
3042
|
+
},
|
|
3043
|
+
cwd: ".",
|
|
3044
|
+
timeoutMs: 60000,
|
|
3045
|
+
},
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
function buildBackendTestSemanticReviewConditionNode(sources) {
|
|
3049
|
+
return {
|
|
3050
|
+
id: "backend-test-semantic-review-condition",
|
|
3051
|
+
depends_on: ["backend-test-semantic-review-shell"],
|
|
3052
|
+
role: "verifier",
|
|
3053
|
+
executor: "static",
|
|
3054
|
+
complexity: "LOW",
|
|
3055
|
+
writePolicy: "none",
|
|
3056
|
+
allowedPaths: [],
|
|
3057
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3058
|
+
outputContract: "Select direct semantic pass or one testcase-only revision; the pass target is the already-finished validated semantic review so the final gate remains outside the exclusive branch targets.",
|
|
3059
|
+
subtask_prompt: "Branch deterministically from semantic review verdict.",
|
|
3060
|
+
static: { resultMarkdown: "Backend pytest semantic review selector." },
|
|
3061
|
+
dynamicCondition: {
|
|
3062
|
+
workflowNodeId: "backend-test-semantic-review-condition",
|
|
3063
|
+
cases: [
|
|
3064
|
+
{ when: "$.nodes['backend-test-semantic-review-shell'].json.verdict == 'pass'", then: "backend-test-semantic-review-shell" },
|
|
3065
|
+
],
|
|
3066
|
+
default: "revise-generated-backend-pytest-pi",
|
|
3067
|
+
},
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
function buildReviseGeneratedBackendPytestNode(sources) {
|
|
3071
|
+
const writeSet = ["testcase/**/test_*.py", "testcase/**/helpers/**", "testcase/**/factories/**"];
|
|
3072
|
+
return {
|
|
3073
|
+
id: "revise-generated-backend-pytest-pi",
|
|
3074
|
+
depends_on: ["backend-test-semantic-review-condition"],
|
|
3075
|
+
role: "implementer",
|
|
3076
|
+
executor: "pi",
|
|
3077
|
+
toolProfile: "write",
|
|
3078
|
+
complexity: "MED",
|
|
3079
|
+
writePolicy: "exclusive",
|
|
3080
|
+
writeSet,
|
|
3081
|
+
allowedPaths: writeSet,
|
|
3082
|
+
forbiddenPaths: [...commonForbiddenPaths(sources), "apps/**", "src/**", "testcase/md/**", "**/conftest.py", "**/pytest.ini"],
|
|
3083
|
+
outputContract: "Apply one bounded semantic correction to generated pytest from validated findings only.",
|
|
3084
|
+
subtask_prompt: [
|
|
3085
|
+
"This is the only pre-execution generated-pytest semantic revision (1/1).",
|
|
3086
|
+
"Read contracts/backend-test-semantic-review.json and edit only the cited generated test files/helpers/factories.",
|
|
3087
|
+
"Preserve case IDs, symbols, test count, target mode, base URL and real-service path.",
|
|
3088
|
+
"Do not delete tests, add skip/xfail, swallow failures, substitute mocks, or weaken assertions beyond the formal comparison/shape contract.",
|
|
3089
|
+
"Do not modify product code, testcase/md/**, conftest.py, pytest.ini, or .harness/**.",
|
|
3090
|
+
].join("\n\n"),
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
function buildBackendTestSemanticFinalGateNode(sources) {
|
|
3094
|
+
return {
|
|
3095
|
+
id: "backend-test-semantic-final-gate-shell",
|
|
3096
|
+
depends_on: ["backend-test-semantic-review-condition", "backend-test-semantic-review-final-shell"],
|
|
3097
|
+
dependsPolicy: "all-or-condition-skip",
|
|
3098
|
+
role: "verifier",
|
|
3099
|
+
executor: "shell",
|
|
3100
|
+
complexity: "LOW",
|
|
3101
|
+
writePolicy: "read-only",
|
|
3102
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
3103
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3104
|
+
outputContract: "Pass-only semantic authorization for traceability and initial pytest.",
|
|
3105
|
+
subtask_prompt: "Accept initial pass path or final semantic review pass; fail closed otherwise.",
|
|
3106
|
+
shell: {
|
|
3107
|
+
commands: [[
|
|
3108
|
+
'test -n "${HARNESS_DAG_RUN_DIR:-}" || exit 2',
|
|
3109
|
+
'node -e \'const fs=require("fs"),path=require("path");const r=process.env.HARNESS_DAG_RUN_DIR;const final=path.join(r,"contracts","backend-test-semantic-review-final.json");const first=path.join(r,"contracts","backend-test-semantic-review.json");const p=fs.existsSync(final)?final:first;const v=JSON.parse(fs.readFileSync(p,"utf8"));if(v.verdict!=="pass")throw new Error("backend pytest semantic review did not pass");console.log("backend pytest semantic gate: pass");\'',
|
|
3110
|
+
].join("; ")],
|
|
3111
|
+
cwd: ".",
|
|
3112
|
+
timeoutMs: 60000,
|
|
3113
|
+
},
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function collectBackendTestShellEnvAllowlist(sources) {
|
|
3117
|
+
const names = new Set();
|
|
3118
|
+
for (const verify of sources.taskConfig.verifyCommands) {
|
|
3119
|
+
const assignmentPattern = /(?:^|[\s;&|])([A-Z_][A-Z0-9_]*)\s*=/g;
|
|
3120
|
+
for (const match of verify.command.matchAll(assignmentPattern)) {
|
|
3121
|
+
if (match[1])
|
|
3122
|
+
names.add(match[1]);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
return [...names].sort();
|
|
3126
|
+
}
|
|
3127
|
+
function taskAllowsBackendTestReportWrite(sources) {
|
|
3128
|
+
return sources.taskConfig.allowedPaths.some((pattern) => pattern === "docs/test-reports/**" ||
|
|
3129
|
+
pattern === "docs/**" ||
|
|
3130
|
+
pattern === "**");
|
|
3131
|
+
}
|
|
3132
|
+
function buildExecuteBackendPytestNode(sources, options = {}) {
|
|
2849
3133
|
// Keep the target worktree read-only: JUnit is runner-owned evidence under
|
|
2850
3134
|
// the current DAG run and moves with active → completed/paused lifecycle.
|
|
2851
3135
|
// Adapter default testRoot is frozen at DAG generation time (auditable) and
|
|
@@ -2856,12 +3140,16 @@ function buildExecuteBackendPytestNode(sources) {
|
|
|
2856
3140
|
});
|
|
2857
3141
|
// Map pytest exit 0/1 → node success ONLY when JUnit exists (assertion-fail is a
|
|
2858
3142
|
// legal result). Do not change global shell ok semantics. Persist raw exit for parse.
|
|
3143
|
+
const nodeId = options.id ?? "execute-backend-pytest-shell";
|
|
3144
|
+
const reportStem = options.reportStem ?? "backend-test";
|
|
3145
|
+
const reportName = `${reportStem}-junit.xml`;
|
|
3146
|
+
const exitName = `${reportStem}-pytest-exit.txt`;
|
|
2859
3147
|
const pytestCommand = [
|
|
2860
3148
|
preflightCommand,
|
|
2861
|
-
|
|
2862
|
-
|
|
3149
|
+
`REPORT="\${HARNESS_DAG_RUN_DIR}/reports/${reportName}"`,
|
|
3150
|
+
`EXIT_FILE="\${HARNESS_DAG_RUN_DIR}/reports/${exitName}"`,
|
|
2863
3151
|
'mkdir -p "$(dirname "${REPORT}")"',
|
|
2864
|
-
`PYTHONDONTWRITEBYTECODE=1 python -m pytest ${frozenTestRoot}/ -v -p no:cacheprovider --junitxml="\${REPORT}"`,
|
|
3152
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -m pytest ${frozenTestRoot}/ -v -p no:cacheprovider --junitxml="\${REPORT}"`,
|
|
2865
3153
|
"STATUS=$?",
|
|
2866
3154
|
'printf "%s" "${STATUS}" > "${EXIT_FILE}"',
|
|
2867
3155
|
'printf "JUnit report: %s\\n" "${REPORT}"',
|
|
@@ -2870,9 +3158,9 @@ function buildExecuteBackendPytestNode(sources) {
|
|
|
2870
3158
|
'exit "${STATUS}"',
|
|
2871
3159
|
].join("; ");
|
|
2872
3160
|
return {
|
|
2873
|
-
id:
|
|
2874
|
-
depends_on: [
|
|
2875
|
-
"backend-test-
|
|
3161
|
+
id: nodeId,
|
|
3162
|
+
depends_on: options.dependsOn ?? [
|
|
3163
|
+
"backend-test-semantic-final-gate-shell",
|
|
2876
3164
|
"backend-test-execution-contract-shell",
|
|
2877
3165
|
],
|
|
2878
3166
|
role: "verifier",
|
|
@@ -2885,6 +3173,7 @@ function buildExecuteBackendPytestNode(sources) {
|
|
|
2885
3173
|
subtask_prompt: "Run pytest for the backend test suite; write JUnit + pytestExitCode evidence only under the current HARNESS_DAG_RUN_DIR/reports/.",
|
|
2886
3174
|
shell: {
|
|
2887
3175
|
commands: [pytestCommand],
|
|
3176
|
+
envAllowlist: collectBackendTestShellEnvAllowlist(sources),
|
|
2888
3177
|
verifyEvidence: buildVerifyEvidence({
|
|
2889
3178
|
phase: "final",
|
|
2890
3179
|
quota: "full",
|
|
@@ -2897,10 +3186,13 @@ function buildExecuteBackendPytestNode(sources) {
|
|
|
2897
3186
|
},
|
|
2898
3187
|
};
|
|
2899
3188
|
}
|
|
2900
|
-
function buildParseBackendTestResultNode(sources) {
|
|
3189
|
+
function buildParseBackendTestResultNode(sources, options = {}) {
|
|
3190
|
+
const id = options.id ?? "parse-backend-test-result-shell";
|
|
3191
|
+
const fromNodeId = options.fromNodeId ?? "execute-backend-pytest-shell";
|
|
3192
|
+
const artifactName = options.artifactName ?? "backend-test-result.json";
|
|
2901
3193
|
return {
|
|
2902
|
-
id
|
|
2903
|
-
depends_on: [
|
|
3194
|
+
id,
|
|
3195
|
+
depends_on: options.dependsOn ?? [fromNodeId],
|
|
2904
3196
|
role: "verifier",
|
|
2905
3197
|
executor: "shell",
|
|
2906
3198
|
complexity: "LOW",
|
|
@@ -2912,10 +3204,13 @@ function buildParseBackendTestResultNode(sources) {
|
|
|
2912
3204
|
shell: {
|
|
2913
3205
|
commands: [],
|
|
2914
3206
|
jsonArtifactGate: {
|
|
2915
|
-
fromNodeId
|
|
3207
|
+
fromNodeId,
|
|
2916
3208
|
schemaId: "backend-test-result-v1",
|
|
2917
|
-
artifactName
|
|
3209
|
+
artifactName,
|
|
2918
3210
|
outputDir: "contracts",
|
|
3211
|
+
...(options.junitRelativePath
|
|
3212
|
+
? { junitRelativePath: options.junitRelativePath }
|
|
3213
|
+
: {}),
|
|
2919
3214
|
},
|
|
2920
3215
|
cwd: ".",
|
|
2921
3216
|
timeoutMs: 60000,
|
|
@@ -2925,7 +3220,7 @@ function buildParseBackendTestResultNode(sources) {
|
|
|
2925
3220
|
function buildClassifyBackendTestResultNode(sources) {
|
|
2926
3221
|
return {
|
|
2927
3222
|
id: "classify-backend-test-result-pi",
|
|
2928
|
-
depends_on: ["parse-backend-test-result-shell"],
|
|
3223
|
+
depends_on: ["parse-backend-test-result-initial-shell"],
|
|
2929
3224
|
role: "reviewer",
|
|
2930
3225
|
executor: "pi",
|
|
2931
3226
|
complexity: "MED",
|
|
@@ -2936,7 +3231,9 @@ function buildClassifyBackendTestResultNode(sources) {
|
|
|
2936
3231
|
subtask_prompt: [
|
|
2937
3232
|
"Read-only classifier for Backend Test Result v1.",
|
|
2938
3233
|
"Return exactly one JSON object (prefer pure JSON; single fenced json block tolerated; no trailing prose).",
|
|
2939
|
-
"
|
|
3234
|
+
"The object must contain exactly category, evidence, confidence, notes. evidence must be a non-empty array of strings, confidence must be a number from 0 through 1, and notes must be a non-empty string. Do not emit schemaVersion or custom fields.",
|
|
3235
|
+
"Minimal shape: {\"category\":\"Unknown\",\"evidence\":[\"outcome=completed-with-failures\"],\"confidence\":0.5,\"notes\":\"Single-run evidence is insufficient for a stronger classification.\"}",
|
|
3236
|
+
"Read contracts/backend-test-result-initial.json (run-owned initial Result v1). Do NOT invent pass rates from raw logs.",
|
|
2940
3237
|
"category must be one of: ProductBug, TestBug, EnvFailure, ContractMismatch, FlakyTest, Unknown.",
|
|
2941
3238
|
"Hard constraints:",
|
|
2942
3239
|
"- Single-run failure MUST NOT use FlakyTest (use Unknown, TestBug, or ProductBug).",
|
|
@@ -2948,24 +3245,187 @@ function buildClassifyBackendTestResultNode(sources) {
|
|
|
2948
3245
|
].join("\n\n"),
|
|
2949
3246
|
};
|
|
2950
3247
|
}
|
|
3248
|
+
function buildBackendTestClassificationGateNode(sources) {
|
|
3249
|
+
return {
|
|
3250
|
+
id: "backend-test-classification-shell",
|
|
3251
|
+
depends_on: ["classify-backend-test-result-pi"],
|
|
3252
|
+
role: "verifier",
|
|
3253
|
+
executor: "shell",
|
|
3254
|
+
complexity: "LOW",
|
|
3255
|
+
writePolicy: "read-only",
|
|
3256
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
3257
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3258
|
+
outputContract: "Validated run-owned Backend Test Classification v1 at contracts/backend-test-classification.json.",
|
|
3259
|
+
subtask_prompt: "Validate and materialize the read-only backend-test classification for deterministic repair routing.",
|
|
3260
|
+
shell: {
|
|
3261
|
+
commands: [],
|
|
3262
|
+
jsonArtifactGate: {
|
|
3263
|
+
fromNodeId: "classify-backend-test-result-pi",
|
|
3264
|
+
schemaId: "backend-test-classification-v1",
|
|
3265
|
+
artifactName: "backend-test-classification.json",
|
|
3266
|
+
outputDir: "contracts",
|
|
3267
|
+
},
|
|
3268
|
+
cwd: ".",
|
|
3269
|
+
timeoutMs: 60000,
|
|
3270
|
+
},
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
function buildBackendTestRepairEligibilityNode(sources) {
|
|
3274
|
+
const command = buildBackendTestRepairEligibilityShellSnippet();
|
|
3275
|
+
return {
|
|
3276
|
+
id: "backend-test-repair-eligibility-shell",
|
|
3277
|
+
depends_on: ["backend-test-classification-shell"],
|
|
3278
|
+
role: "verifier",
|
|
3279
|
+
executor: "shell",
|
|
3280
|
+
complexity: "LOW",
|
|
3281
|
+
writePolicy: "read-only",
|
|
3282
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
3283
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3284
|
+
outputContract: "Pure JSON {schemaVersion,eligible,reason,revisionAttempt,category,confidence}; eligible only for completed TestBug assertion failures with error=0.",
|
|
3285
|
+
subtask_prompt: "Deterministically decide whether this run may use its single testcase-only repair attempt.",
|
|
3286
|
+
shell: { commands: [command], cwd: ".", timeoutMs: 60000 },
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
function buildBackendTestRepairConditionNode(sources) {
|
|
3290
|
+
return {
|
|
3291
|
+
id: "backend-test-repair-condition",
|
|
3292
|
+
depends_on: ["backend-test-repair-eligibility-shell"],
|
|
3293
|
+
role: "verifier",
|
|
3294
|
+
executor: "static",
|
|
3295
|
+
complexity: "LOW",
|
|
3296
|
+
writePolicy: "none",
|
|
3297
|
+
allowedPaths: [],
|
|
3298
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3299
|
+
outputContract: "Select repair or skip repair exactly once; ineligible runs select the already-finished eligibility evidence so the effective-result shell remains outside the exclusive branch targets.",
|
|
3300
|
+
subtask_prompt: "Route eligible TestBug to one repair attempt; otherwise select the already-finished eligibility evidence and skip repair.",
|
|
3301
|
+
static: { resultMarkdown: "Backend-test repair branch selector." },
|
|
3302
|
+
dynamicCondition: {
|
|
3303
|
+
workflowNodeId: "backend-test-repair-condition",
|
|
3304
|
+
cases: [
|
|
3305
|
+
{
|
|
3306
|
+
when: "$.nodes['backend-test-repair-eligibility-shell'].json.eligible == true",
|
|
3307
|
+
then: "repair-backend-pytest-pi",
|
|
3308
|
+
},
|
|
3309
|
+
],
|
|
3310
|
+
default: "backend-test-repair-eligibility-shell",
|
|
3311
|
+
},
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
function buildRepairBackendPytestNode(sources) {
|
|
3315
|
+
const writeSet = [
|
|
3316
|
+
"testcase/**/test_*.py",
|
|
3317
|
+
"testcase/**/helpers/**",
|
|
3318
|
+
"testcase/**/factories/**",
|
|
3319
|
+
];
|
|
3320
|
+
return {
|
|
3321
|
+
id: "repair-backend-pytest-pi",
|
|
3322
|
+
depends_on: ["backend-test-repair-condition"],
|
|
3323
|
+
role: "implementer",
|
|
3324
|
+
executor: "pi",
|
|
3325
|
+
toolProfile: "write",
|
|
3326
|
+
complexity: "HIGH",
|
|
3327
|
+
writePolicy: "exclusive",
|
|
3328
|
+
writeSet,
|
|
3329
|
+
allowedPaths: writeSet,
|
|
3330
|
+
forbiddenPaths: [
|
|
3331
|
+
...commonForbiddenPaths(sources),
|
|
3332
|
+
"apps/**",
|
|
3333
|
+
"src/**",
|
|
3334
|
+
".env*",
|
|
3335
|
+
"**/migrations/**",
|
|
3336
|
+
],
|
|
3337
|
+
outputContract: "Repair only existing generated pytest tests/helpers/factories for classified TestBug findings. No production/config/runtime evidence writes.",
|
|
3338
|
+
subtask_prompt: [
|
|
3339
|
+
"This is the only automatic TestBug repair attempt (1/1).",
|
|
3340
|
+
"Read contracts/backend-test-result-initial.json, contracts/backend-test-classification.json, contracts/backend-test-analysis.json, contracts/backend-test-case-manifest.json, and only the generated pytest files named in failures[].name or manifest mappings.",
|
|
3341
|
+
"Do not re-read the full task source tree: the run-owned analysis/manifest are the compact authoritative context.",
|
|
3342
|
+
"Repair only test implementation defects directly supported by reviewed Expected Results or formal API contract evidence.",
|
|
3343
|
+
"Never modify product code, migrations, service configuration, conftest.py, pytest.ini, pyproject.toml, setup.cfg, source requirements, testcase/md/**, or .harness/**.",
|
|
3344
|
+
"Never delete a test, remove a case mapping, add skip/skipif/xfail, swallow AssertionError/network exceptions, switch to a mock server, or weaken documented status/value assertions.",
|
|
3345
|
+
"Keep the full suite runnable and preserve each BE-* case ID and pytest symbol mapping.",
|
|
3346
|
+
"Stay within writeSet: testcase/**/test_*.py, helpers/**, factories/**.",
|
|
3347
|
+
].join("\n\n"),
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
function buildBackendTestRepairSafetyGateNode(sources) {
|
|
3351
|
+
const command = buildBackendTestRepairSafetyShellSnippet();
|
|
3352
|
+
return {
|
|
3353
|
+
id: "backend-test-repair-safety-gate-shell",
|
|
3354
|
+
depends_on: ["repair-backend-pytest-pi"],
|
|
3355
|
+
role: "verifier",
|
|
3356
|
+
executor: "shell",
|
|
3357
|
+
complexity: "LOW",
|
|
3358
|
+
writePolicy: "read-only",
|
|
3359
|
+
allowedPaths: ["testcase/**"],
|
|
3360
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3361
|
+
outputContract: "Fail-closed repair safety gate rejecting skip/xfail and broad failure swallowing before final traceability and pytest.",
|
|
3362
|
+
subtask_prompt: "Check that automatic TestBug repair did not manufacture success.",
|
|
3363
|
+
shell: { commands: [command], cwd: ".", timeoutMs: 60000 },
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
function buildBackendTestEffectiveResultNode(sources) {
|
|
3367
|
+
const command = buildBackendTestEffectiveResultSelectorShellSnippet();
|
|
3368
|
+
// Inline retrospective context: relocated verbatim from the former
|
|
3369
|
+
// buildBackendTestRetrospectiveContextNode so test-retrospect-pi keeps a
|
|
3370
|
+
// single upstream. Runs after `command` which materializes
|
|
3371
|
+
// contracts/backend-test-result.json from the effective (final|initial) result.
|
|
3372
|
+
const retroCommand = [
|
|
3373
|
+
'test -n "${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for backend-test retrospective context" >&2; exit 2; }',
|
|
3374
|
+
'node -e \'const fs=require("fs"),path=require("path"); const root=process.env.HARNESS_DAG_RUN_DIR; const read=(name)=>JSON.parse(fs.readFileSync(path.join(root,"contracts",name),"utf8")); process.stdout.write(JSON.stringify({schemaVersion:1,result:read("backend-test-result.json"),initialResult:read("backend-test-result-initial.json"),finalResult:fs.existsSync(path.join(root,"contracts","backend-test-result-final.json"))?read("backend-test-result-final.json"):null,manifest:read("backend-test-case-manifest.json"),classification:read("backend-test-classification.json")},null,2));\'',
|
|
3375
|
+
].join("; ");
|
|
3376
|
+
return {
|
|
3377
|
+
id: "select-effective-backend-test-result-shell",
|
|
3378
|
+
depends_on: [
|
|
3379
|
+
"backend-test-repair-condition",
|
|
3380
|
+
"parse-backend-test-result-final-shell",
|
|
3381
|
+
"backend-test-case-manifest-shell",
|
|
3382
|
+
"backend-test-case-manifest-final-shell",
|
|
3383
|
+
"backend-test-classification-shell",
|
|
3384
|
+
],
|
|
3385
|
+
dependsPolicy: "all-or-condition-skip",
|
|
3386
|
+
role: "verifier",
|
|
3387
|
+
executor: "shell",
|
|
3388
|
+
complexity: "LOW",
|
|
3389
|
+
writePolicy: "read-only",
|
|
3390
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
3391
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3392
|
+
outputContract: "Materialize contracts/backend-test-result.json from final result when present, otherwise initial result; emit effective result metadata, and emit the complete retrospective context (Result v1 + Case Manifest v1 + classifier output) for downstream retrospective consumption.",
|
|
3393
|
+
subtask_prompt: "Select the effective backend-test result deterministically without changing initial/final evidence, then emit the complete retrospective context.",
|
|
3394
|
+
shell: { commands: [command, retroCommand], cwd: ".", timeoutMs: 60000 },
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
2951
3397
|
function buildTestRetrospectNode(sources) {
|
|
3398
|
+
const canWriteReport = taskAllowsBackendTestReportWrite(sources);
|
|
2952
3399
|
return {
|
|
2953
3400
|
id: "test-retrospect-pi",
|
|
2954
|
-
depends_on: ["
|
|
3401
|
+
depends_on: ["select-effective-backend-test-result-shell"],
|
|
2955
3402
|
role: "closeout",
|
|
2956
3403
|
executor: "pi",
|
|
2957
|
-
toolProfile: "write",
|
|
2958
3404
|
complexity: "MED",
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
3405
|
+
...(canWriteReport
|
|
3406
|
+
? {
|
|
3407
|
+
toolProfile: "write",
|
|
3408
|
+
writePolicy: "exclusive",
|
|
3409
|
+
writeSet: ["docs/test-reports/**"],
|
|
3410
|
+
allowedPaths: ["docs/test-reports/**"],
|
|
3411
|
+
}
|
|
3412
|
+
: {
|
|
3413
|
+
writePolicy: "read-only",
|
|
3414
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
3415
|
+
}),
|
|
2962
3416
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
3417
|
+
outputContract: canWriteReport
|
|
3418
|
+
? "Maturity rating in assistant output plus a report written under docs/test-reports/**."
|
|
3419
|
+
: "Read-only maturity rating and retrospective in assistant output; no repository file writes because task allowedPaths do not authorize docs/test-reports/**.",
|
|
2963
3420
|
subtask_prompt: [
|
|
2964
|
-
"Read
|
|
3421
|
+
"Read the complete JSON from direct upstream select-effective-backend-test-result-shell and generate a test retrospective report.",
|
|
3422
|
+
"That JSON contains result, manifest (including coverageSummary), and classification. Treat those fields as authoritative; do not rely on pointer/hash summaries.",
|
|
2965
3423
|
"",
|
|
2966
3424
|
"## Output Steps (do in order):",
|
|
2967
3425
|
"1. First, output the maturity rating on the first line: Rating: A/B/C/D",
|
|
2968
|
-
|
|
3426
|
+
canWriteReport
|
|
3427
|
+
? "2. Then write the full report under docs/test-reports/"
|
|
3428
|
+
: "2. Keep the full retrospective in assistant output only; do not write repository files because docs/test-reports/** is outside task allowedPaths.",
|
|
2969
3429
|
"",
|
|
2970
3430
|
"## Stats authority (deterministic only):",
|
|
2971
3431
|
"- Pass rate, failed/error/skipped counts, and failure list MUST come from contracts/backend-test-result.json only.",
|
|
@@ -2987,7 +3447,9 @@ function buildTestRetrospectNode(sources) {
|
|
|
2987
3447
|
"- D: below C thresholds",
|
|
2988
3448
|
"",
|
|
2989
3449
|
"## Constraints:",
|
|
2990
|
-
|
|
3450
|
+
canWriteReport
|
|
3451
|
+
? "- Stay within writeSet: docs/test-reports/**"
|
|
3452
|
+
: "- Read-only: do not modify repository files",
|
|
2991
3453
|
"- Do NOT re-read source documents — use upstream outputs only",
|
|
2992
3454
|
"- Do not write root artifacts/**",
|
|
2993
3455
|
].join("\n\n"),
|
|
@@ -3034,63 +3496,73 @@ const BACKEND_TEST_SKILLS_BY_ROLE = {
|
|
|
3034
3496
|
};
|
|
3035
3497
|
function buildBackendTestHybridDag(sources) {
|
|
3036
3498
|
const { taskConfig } = sources;
|
|
3037
|
-
const sourceContext = buildSourceContextBlock(sources);
|
|
3038
|
-
const readOnlyPaths = commonReadOnlyPaths(sources);
|
|
3039
|
-
const forbiddenPaths = commonForbiddenPaths(sources);
|
|
3040
3499
|
const globalConstraints = [
|
|
3041
3500
|
...taskConfig.hardConstraints,
|
|
3042
|
-
...(sources.constraintMarkdown
|
|
3043
|
-
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
3044
|
-
: []),
|
|
3045
3501
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
3046
|
-
"backend-test-dag
|
|
3502
|
+
"backend-test-dag uses exactly 24 real top-level tasks; bounded revision/repair branches are controlled by fail-closed runIf expressions.",
|
|
3503
|
+
"Analysis, execution, manifest, semantic review, initial/final/effective results, classification, eligibility, repair safety, traceability and outcome evidence remain run-owned and fail-closed.",
|
|
3047
3504
|
"Functional test case IDs must use BE-<MODULE>-<NNN> format.",
|
|
3048
|
-
"pytest
|
|
3049
|
-
"pytest automation scripts must use test_ filename prefix for pytest discovery.",
|
|
3050
|
-
"generate-backend-pytest-pi may create only new files under testcase/**/test_*.py, testcase/**/helpers/**, and testcase/**/factories/**; modifying conftest.py, pytest.ini, pyproject.toml, or production code is forbidden.",
|
|
3051
|
-
"review-backend-cases-gate-shell must block pytest generation unless the review verdict is exactly VERDICT: pass.",
|
|
3052
|
-
"If a target test filename already exists under testcase/, add a numeric suffix (_01, _02, ...); never overwrite or append to existing files.",
|
|
3053
|
-
"execute-backend-pytest-shell must not modify test assertions or production code to make tests pass; test failures indicate potential implementation issues and must be reported honestly.",
|
|
3054
|
-
"parse-backend-test-result-shell materializes Backend Test Result v1 from JUnit + pytestExitCode; classify/retrospect run on pass and assertion-fail; backend-test-outcome-gate-shell uses result.outcome only.",
|
|
3055
|
-
"backend-test-case-manifest-shell validates schemaId backend-test-case-manifest-v1 and materializes contracts/backend-test-case-manifest.json; AC coverage is fail-closed and deterministic.",
|
|
3056
|
-
"backend-test-traceability-gate-shell verifies generated file/symbol existence after pytest generation and before execute; models must not invent coverage percentages.",
|
|
3505
|
+
"pytest writers may only change declared testcase assets; production code, config, skip/xfail, swallowed failures and mock substitution are forbidden.",
|
|
3057
3506
|
];
|
|
3058
|
-
const
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
// the DAG; regenerate after fixing cases. Controller still keys off
|
|
3068
|
-
// hard-verify-shell, which this template does not include.
|
|
3069
|
-
defaults: {
|
|
3070
|
-
...BACKEND_TEST_DEFAULTS,
|
|
3071
|
-
contextProfile: taskConfig.contextProfile,
|
|
3072
|
-
},
|
|
3073
|
-
skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
|
|
3074
|
-
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
3075
|
-
tasks: [
|
|
3076
|
-
buildAnalyzeInputsNode(sources),
|
|
3077
|
-
buildBackendTestAnalysisContractGateNode(sources),
|
|
3078
|
-
buildBackendTestEnvironmentScoutNode(sources),
|
|
3079
|
-
buildBackendTestExecutionContractGateNode(sources),
|
|
3080
|
-
buildGenerateBackendFunctionalCasesNode(sources),
|
|
3081
|
-
buildEmitBackendCaseManifestNode(sources),
|
|
3082
|
-
buildBackendTestCaseManifestGateNode(sources),
|
|
3083
|
-
buildReviewBackendCasesNode(sources),
|
|
3084
|
-
buildReviewBackendCasesGateNode(sources),
|
|
3085
|
-
buildGenerateBackendPytestNode(sources),
|
|
3086
|
-
buildBackendTestTraceabilityGateNode(sources),
|
|
3087
|
-
buildExecuteBackendPytestNode(sources),
|
|
3088
|
-
buildParseBackendTestResultNode(sources),
|
|
3089
|
-
buildClassifyBackendTestResultNode(sources),
|
|
3090
|
-
buildTestRetrospectNode(sources),
|
|
3091
|
-
buildBackendTestOutcomeGateNode(sources),
|
|
3092
|
-
],
|
|
3507
|
+
const analyze = buildAnalyzeInputsNode(sources);
|
|
3508
|
+
analyze.id = "analyze-and-discover-backend-test-pi";
|
|
3509
|
+
analyze.outputContract = "Pure JSON envelope {analysis: Backend Test Analysis v2, execution: Backend Test Execution Contract v1}; no prose or writes.";
|
|
3510
|
+
analyze.subtask_prompt = `${analyze.subtask_prompt}\n\nAlso perform the read-only environment discovery described by Backend Test Execution Contract v1. Return exactly one JSON envelope with top-level keys analysis and execution; analysis must satisfy v2 and execution must satisfy v1.`;
|
|
3511
|
+
const contracts = {
|
|
3512
|
+
id: "validate-backend-test-contracts-shell", depends_on: [analyze.id], role: "verifier", executor: "shell", complexity: "LOW",
|
|
3513
|
+
writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
|
|
3514
|
+
outputContract: "Materialize and validate contracts/backend-test-analysis.json and contracts/backend-test-execution.json.",
|
|
3515
|
+
subtask_prompt: "Validate both backend-test intake contracts fail-closed.", shell: { commands: [], backendTestPipeline: "contracts", cwd: ".", timeoutMs: 60000 },
|
|
3093
3516
|
};
|
|
3517
|
+
const generateCases = buildGenerateBackendFunctionalCasesNode(sources);
|
|
3518
|
+
generateCases.id = "generate-backend-cases-and-manifest-pi";
|
|
3519
|
+
generateCases.depends_on = [contracts.id];
|
|
3520
|
+
generateCases.outputContract = "Write testcase/md/** and end with one fenced json Backend Test Case Manifest v1 block matching the strict field contract.";
|
|
3521
|
+
generateCases.subtask_prompt += `\n\nAfter writing Markdown, end assistant output with exactly one fenced json block containing Backend Test Case Manifest v1 derived from the written cases.\n\n${BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS}`;
|
|
3522
|
+
const manifest = buildBackendTestCaseManifestGateNode(sources, { dependsOn: [generateCases.id], fromNodeId: generateCases.id });
|
|
3523
|
+
const reviewCases = buildReviewBackendCasesNode(sources, { dependsOn: [manifest.id, contracts.id] });
|
|
3524
|
+
const reviseCases = buildReviseBackendCasesNode(sources);
|
|
3525
|
+
reviseCases.depends_on = [reviewCases.id];
|
|
3526
|
+
reviseCases.runIf = "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'";
|
|
3527
|
+
reviseCases.outputContract = "Apply one case revision and end with one fenced json final Case Manifest v1 block matching the strict field contract.";
|
|
3528
|
+
reviseCases.subtask_prompt += `\n\nAfter edits, end assistant output with exactly one fenced json block containing the complete final Backend Test Case Manifest v1.\n\n${BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS}`;
|
|
3529
|
+
const finalManifest = buildBackendTestCaseManifestGateNode(sources, { id: "backend-test-case-manifest-final-shell", dependsOn: [reviseCases.id], fromNodeId: reviseCases.id });
|
|
3530
|
+
finalManifest.runIf = reviseCases.runIf;
|
|
3531
|
+
const finalCaseReview = buildReviewBackendCasesNode(sources, { id: "review-backend-cases-final-pi", phase: "final", dependsOn: [finalManifest.id, contracts.id] });
|
|
3532
|
+
finalCaseReview.runIf = reviseCases.runIf;
|
|
3533
|
+
const caseGate = buildReviewBackendCasesGateNode(sources);
|
|
3534
|
+
caseGate.depends_on = [reviewCases.id, finalCaseReview.id];
|
|
3535
|
+
caseGate.shell.verdictGate = { fromNodeId: finalCaseReview.id, fallbackFromNodeIds: [reviewCases.id], accept: ["VERDICT: pass"], label: "backend case effective review", lineMode: "first-verdict-line" };
|
|
3536
|
+
const generatePytest = buildGenerateBackendPytestNode(sources);
|
|
3537
|
+
generatePytest.depends_on = [caseGate.id, contracts.id];
|
|
3538
|
+
const semanticReview = buildBackendTestSemanticReviewNode(sources, { dependsOn: [generatePytest.id, contracts.id, manifest.id, finalManifest.id] });
|
|
3539
|
+
const semanticInitial = { id: "validate-semantic-review-and-traceability-shell", depends_on: [semanticReview.id, manifest.id, finalManifest.id], dependsPolicy: "all-or-condition-skip", role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Materialize initial semantic review and validate pytest traceability.", subtask_prompt: "Validate initial semantic review and traceability fail-closed.", shell: { commands: [], backendTestPipeline: "semantic-initial", cwd: ".", timeoutMs: 60000 } };
|
|
3540
|
+
const revisePytest = buildReviseGeneratedBackendPytestNode(sources);
|
|
3541
|
+
revisePytest.depends_on = [semanticInitial.id];
|
|
3542
|
+
revisePytest.runIf = "$.nodes['validate-semantic-review-and-traceability-shell'].json.verdict == 'request-revision'";
|
|
3543
|
+
const finalSemanticReview = buildBackendTestSemanticReviewNode(sources, { id: "review-generated-backend-pytest-final-pi", dependsOn: [revisePytest.id], final: true });
|
|
3544
|
+
finalSemanticReview.runIf = revisePytest.runIf;
|
|
3545
|
+
const semanticFinal = { id: "backend-test-semantic-final-gate-shell", depends_on: [semanticInitial.id, finalSemanticReview.id], dependsPolicy: "all-or-condition-skip", role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Pass-only effective semantic review gate with final traceability after revision.", subtask_prompt: "Accept initial semantic pass or validate the single final review and traceability.", shell: { commands: [['test -n "${HARNESS_DAG_RUN_DIR:-}" || exit 2', 'node -e \'const fs=require("fs"),path=require("path");const r=process.env.HARNESS_DAG_RUN_DIR;const f=path.join(r,"contracts","backend-test-semantic-review-final.json");const i=path.join(r,"contracts","backend-test-semantic-review.json");const v=JSON.parse(fs.readFileSync(fs.existsSync(f)?f:i,"utf8"));if(v.verdict!=="pass")throw new Error("backend pytest semantic review did not pass");\''].join("; ")], cwd: ".", timeoutMs: 60000 } };
|
|
3546
|
+
const finalSemanticMaterialize = { id: "materialize-final-semantic-review-shell", depends_on: [finalSemanticReview.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), runIf: revisePytest.runIf, outputContract: "Materialize final semantic review and re-check traceability.", subtask_prompt: "Validate final semantic review and traceability.", shell: { commands: [], backendTestPipeline: "semantic-final", cwd: ".", timeoutMs: 60000 } };
|
|
3547
|
+
semanticFinal.depends_on = [semanticInitial.id, finalSemanticMaterialize.id];
|
|
3548
|
+
const executeInitial = buildExecuteBackendPytestNode(sources, { id: "execute-and-parse-backend-pytest-initial-shell", dependsOn: [semanticFinal.id, contracts.id], reportStem: "backend-test-initial" });
|
|
3549
|
+
executeInitial.shell.backendTestPipeline = "execute-parse-initial";
|
|
3550
|
+
const classify = buildClassifyBackendTestResultNode(sources);
|
|
3551
|
+
classify.depends_on = [executeInitial.id];
|
|
3552
|
+
const classifyEligibility = { id: "materialize-classification-and-eligibility-shell", depends_on: [classify.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Materialize Classification v1 and repair eligibility JSON.", subtask_prompt: "Validate classification and determine bounded repair eligibility.", shell: { commands: [], backendTestPipeline: "classification-eligibility", cwd: ".", timeoutMs: 60000 } };
|
|
3553
|
+
const repair = buildRepairBackendPytestNode(sources);
|
|
3554
|
+
repair.depends_on = [classifyEligibility.id];
|
|
3555
|
+
repair.runIf = "$.nodes['materialize-classification-and-eligibility-shell'].json.eligible == true";
|
|
3556
|
+
const repairVerify = { id: "validate-repair-safety-and-traceability-shell", depends_on: [repair.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: ["testcase/**"], forbiddenPaths: commonForbiddenPaths(sources), runIf: repair.runIf, outputContract: "Validate repair safety and final traceability.", subtask_prompt: "Reject manufactured success and revalidate mapping.", shell: { commands: [], backendTestPipeline: "repair-safety-traceability", cwd: ".", timeoutMs: 60000 } };
|
|
3557
|
+
const finalize = buildExecuteBackendPytestNode(sources, { id: "finalize-effective-backend-test-result-shell", dependsOn: [classifyEligibility.id, repairVerify.id, contracts.id], reportStem: "backend-test-final" });
|
|
3558
|
+
finalize.dependsPolicy = "all-or-condition-skip";
|
|
3559
|
+
finalize.shell.backendTestPipeline = "finalize-effective-result";
|
|
3560
|
+
finalize.outputContract = "If repaired, execute/parse final pytest; always materialize contracts/backend-test-result.json from final or initial result.";
|
|
3561
|
+
const retrospect = buildTestRetrospectNode(sources);
|
|
3562
|
+
retrospect.depends_on = [finalize.id];
|
|
3563
|
+
const outcome = buildBackendTestOutcomeGateNode(sources);
|
|
3564
|
+
const tasks = [analyze, contracts, generateCases, manifest, reviewCases, reviseCases, finalManifest, finalCaseReview, caseGate, generatePytest, semanticReview, semanticInitial, revisePytest, finalSemanticReview, finalSemanticMaterialize, semanticFinal, executeInitial, classify, classifyEligibility, repair, repairVerify, finalize, retrospect, outcome];
|
|
3565
|
+
const spec = { version: 3, title: `Backend test DAG: ${taskConfig.title}`, runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT, outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE, objective: extractObjective(sources.requirementMarkdown, taskConfig.title), successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId), globalConstraints, defaults: { ...BACKEND_TEST_DEFAULTS, contextProfile: taskConfig.contextProfile }, skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE, executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS, tasks };
|
|
3094
3566
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
3095
3567
|
parseDagSpec(spec);
|
|
3096
3568
|
assertValidDagSpec(spec);
|