@tea-agent/loop-agent 0.31.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/executors/dag-pi-executor.js +69 -2
- package/dist/executors/pi-sdk-executor.js +61 -0
- package/dist/executors/shell-executor.js +136 -79
- package/dist/workflows/dag/backend-test-pytest-collection.js +162 -7
- package/dist/workflows/dag/backend-test-result-contract.js +105 -67
- package/dist/workflows/dag/backend-test-scenario-param.js +92 -30
- package/dist/workflows/dag/backend-test-writer-completeness.js +55 -0
- package/dist/workflows/dag/init-hybrid.js +44 -47
- package/dist/workflows/dag/rerun-task.js +86 -0
- package/docs/templates/backend-test-dag.json +40 -60
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -8,6 +9,7 @@ const factsSchema = z
|
|
|
8
9
|
.object({
|
|
9
10
|
schemaId: z.literal(BACKEND_TEST_SCENARIO_PARAM_FACTS_SCHEMA_ID),
|
|
10
11
|
phase: z.enum(["initial", "final"]),
|
|
12
|
+
overallStatus: z.enum(["PASS", "PARTIAL", "FAIL", "UNAVAILABLE"]),
|
|
11
13
|
repairEligible: z.boolean(),
|
|
12
14
|
repairAttempt: z.number().int().min(0).max(1),
|
|
13
15
|
strictScenarioParamGate: z.boolean(),
|
|
@@ -115,23 +117,28 @@ async function listMarkdownCases(workspaceRoot) {
|
|
|
115
117
|
}
|
|
116
118
|
export function inferScenarioParamIntent(input) {
|
|
117
119
|
const { tpId, caseBody } = input;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
+
const preciseLine = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;,,]\s*operation\s*=\s*([^|;,,\n]+)\s*[|;,,]\s*target\s*=\s*([A-Za-z0-9_.-]+)\s*[|;,,]\s*intent\s*=\s*([a-z0-9+._:-]+)(?:\s*[|;,,]\s*bound\s*=\s*(\d+))?(?:\s*[|;,,]\s*example\s*=\s*([^|\n]+))?/i.exec(caseBody);
|
|
121
|
+
const legacyLine = /场景意图\s*[::]\s*([a-z0-9+._:-]+)(?:\s*[|;,,]\s*field\s*=\s*([A-Za-z0-9_.]+))?(?:\s*[|;,,]\s*bound\s*=\s*(\d+))?(?:\s*[|;,,]\s*example\s*=\s*([^|\n]+))?/i.exec(caseBody);
|
|
122
|
+
const fallbackLine = new RegExp(`${tpId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]{0,160}?intent\\s*[=:]\\s*([a-z0-9+._:-]+)`, "i").exec(caseBody);
|
|
123
|
+
const machineLine = preciseLine ?? legacyLine ?? fallbackLine;
|
|
120
124
|
if (machineLine) {
|
|
121
|
-
const rawIntent = (machineLine[1] ?? "unknown").toLowerCase();
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
125
|
+
const rawIntent = (preciseLine ? preciseLine[4] : machineLine[1] ?? "unknown").toLowerCase();
|
|
126
|
+
const target = preciseLine?.[3];
|
|
127
|
+
const field = target && target !== "request" ? target.split(".").at(-1) : legacyLine?.[2];
|
|
128
|
+
const boundRaw = preciseLine?.[5] ?? legacyLine?.[3];
|
|
129
|
+
const example = (preciseLine?.[6] ?? legacyLine?.[4])?.trim();
|
|
130
|
+
const bound = boundRaw ? Number(boundRaw) : undefined;
|
|
131
|
+
const normalizedIntent = rawIntent === "nominal-operation" ? "nominal" : rawIntent;
|
|
132
|
+
if (normalizedIntent.startsWith("custom-literal:")) {
|
|
126
133
|
return {
|
|
127
|
-
intent:
|
|
134
|
+
intent: normalizedIntent,
|
|
128
135
|
field,
|
|
129
136
|
bound,
|
|
130
137
|
example,
|
|
131
138
|
};
|
|
132
139
|
}
|
|
133
140
|
return {
|
|
134
|
-
intent: (CLOSED_SET.has(
|
|
141
|
+
intent: (CLOSED_SET.has(normalizedIntent) ? normalizedIntent : "unknown"),
|
|
135
142
|
field,
|
|
136
143
|
bound,
|
|
137
144
|
example,
|
|
@@ -240,6 +247,9 @@ export function observeParamFeatures(block, field) {
|
|
|
240
247
|
// Prefer dict-like payload extraction.
|
|
241
248
|
const dictMatch = /\{\s*([\s\S]*?)\s*\}/.exec(block) ??
|
|
242
249
|
/(?:payload|body|data|json)\s*=\s*(\{[\s\S]*?\})/.exec(block);
|
|
250
|
+
if (dictMatch && !field) {
|
|
251
|
+
return { kind: "dict", text: (dictMatch[0] ?? dictMatch[1] ?? "{}").replace(/\s+/g, " ").slice(0, 160) };
|
|
252
|
+
}
|
|
243
253
|
if (dictMatch && field) {
|
|
244
254
|
const dict = dictMatch[0] ?? dictMatch[1] ?? "";
|
|
245
255
|
const fieldPattern = new RegExp(`["']${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:`);
|
|
@@ -531,19 +541,29 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
531
541
|
});
|
|
532
542
|
}
|
|
533
543
|
const repairableCount = entries.filter((entry) => entry.repairability === "repairable" && entry.status === "MISMATCH").length;
|
|
544
|
+
const matchCount = entries.filter((entry) => entry.status === "MATCH").length;
|
|
545
|
+
const mismatchCount = entries.filter((entry) => entry.status === "MISMATCH").length;
|
|
546
|
+
const undeterminedCount = entries.filter((entry) => entry.status === "UNDETERMINED").length;
|
|
547
|
+
const overallStatus = mismatchCount > 0
|
|
548
|
+
? "FAIL"
|
|
549
|
+
: matchCount === 0 && undeterminedCount > 0
|
|
550
|
+
? "UNAVAILABLE"
|
|
551
|
+
: undeterminedCount > 0
|
|
552
|
+
? "PARTIAL"
|
|
553
|
+
: "PASS";
|
|
534
554
|
const facts = {
|
|
535
555
|
schemaId: BACKEND_TEST_SCENARIO_PARAM_FACTS_SCHEMA_ID,
|
|
536
556
|
phase: input.phase,
|
|
557
|
+
overallStatus,
|
|
537
558
|
repairEligible: input.phase === "initial" && repairableCount > 0,
|
|
538
559
|
repairAttempt: input.repairAttempt ?? (input.phase === "final" ? 1 : 0),
|
|
539
560
|
strictScenarioParamGate: input.strictScenarioParamGate === true,
|
|
540
561
|
entries,
|
|
541
562
|
assetHashes,
|
|
542
563
|
summary: {
|
|
543
|
-
matchCount
|
|
544
|
-
mismatchCount
|
|
545
|
-
|
|
546
|
-
undeterminedCount: entries.filter((entry) => entry.status === "UNDETERMINED").length,
|
|
564
|
+
matchCount,
|
|
565
|
+
mismatchCount,
|
|
566
|
+
undeterminedCount,
|
|
547
567
|
repairableCount,
|
|
548
568
|
blockedCount: entries.filter((entry) => entry.repairability === "blocked")
|
|
549
569
|
.length,
|
|
@@ -554,7 +574,7 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
554
574
|
"",
|
|
555
575
|
`## Status`,
|
|
556
576
|
"",
|
|
557
|
-
facts.
|
|
577
|
+
facts.overallStatus,
|
|
558
578
|
"",
|
|
559
579
|
`## Summary`,
|
|
560
580
|
"",
|
|
@@ -615,10 +635,12 @@ function replaceFieldInDictLiteral(dictLiteral, field, mode, valueLiteral) {
|
|
|
615
635
|
if (fieldPattern.test(dictLiteral)) {
|
|
616
636
|
return dictLiteral.replace(fieldPattern, `$1$2${valueLiteral}`);
|
|
617
637
|
}
|
|
618
|
-
// Insert before closing brace.
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
638
|
+
// Insert before closing brace. Normalize an existing trailing comma first so
|
|
639
|
+
// adding a missing field can never produce `,,` and corrupt Python syntax.
|
|
640
|
+
const withoutClosingBrace = dictLiteral.replace(/\s*\}$/, "");
|
|
641
|
+
const trimmed = withoutClosingBrace.replace(/,\s*$/, "").trimEnd();
|
|
642
|
+
const hasEntries = trimmed.trim() !== "{";
|
|
643
|
+
return `${trimmed}${hasEntries ? ", " : ""}${JSON.stringify(field)}: ${valueLiteral}}`;
|
|
622
644
|
}
|
|
623
645
|
export function deterministicRewriteScenarioParam(input) {
|
|
624
646
|
const { entry } = input;
|
|
@@ -693,6 +715,32 @@ export function deterministicRewriteScenarioParam(input) {
|
|
|
693
715
|
}
|
|
694
716
|
return { ok: true, source: nextSource, detail: "rewritten" };
|
|
695
717
|
}
|
|
718
|
+
function validatePythonSyntaxWithoutExecution(source) {
|
|
719
|
+
const result = spawnSync("python", ["-c", "import ast,sys; ast.parse(sys.stdin.read())"], {
|
|
720
|
+
input: source,
|
|
721
|
+
encoding: "utf8",
|
|
722
|
+
env: {
|
|
723
|
+
...process.env,
|
|
724
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
725
|
+
PYTHONUTF8: "1",
|
|
726
|
+
},
|
|
727
|
+
timeout: 15_000,
|
|
728
|
+
});
|
|
729
|
+
if (result.error) {
|
|
730
|
+
return {
|
|
731
|
+
ok: false,
|
|
732
|
+
detail: `python syntax validation unavailable: ${result.error.message}`,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (result.status !== 0) {
|
|
736
|
+
const detail = String(result.stderr || result.stdout || "invalid Python syntax")
|
|
737
|
+
.trim()
|
|
738
|
+
.replace(/\s+/g, " ")
|
|
739
|
+
.slice(0, 500);
|
|
740
|
+
return { ok: false, detail: `python syntax validation failed: ${detail}` };
|
|
741
|
+
}
|
|
742
|
+
return { ok: true, detail: "python ast.parse PASS" };
|
|
743
|
+
}
|
|
696
744
|
export async function applyDeterministicScenarioParamRepairs(input) {
|
|
697
745
|
const repairable = input.facts.entries.filter((entry) => entry.status === "MISMATCH" && entry.repairability === "repairable");
|
|
698
746
|
const byScript = new Map();
|
|
@@ -722,7 +770,7 @@ export async function applyDeterministicScenarioParamRepairs(input) {
|
|
|
722
770
|
continue;
|
|
723
771
|
}
|
|
724
772
|
let source = await readFile(absolute, "utf8");
|
|
725
|
-
|
|
773
|
+
const staged = [];
|
|
726
774
|
for (const entry of entries) {
|
|
727
775
|
const result = deterministicRewriteScenarioParam({ source, entry });
|
|
728
776
|
if (!result.ok) {
|
|
@@ -735,17 +783,31 @@ export async function applyDeterministicScenarioParamRepairs(input) {
|
|
|
735
783
|
continue;
|
|
736
784
|
}
|
|
737
785
|
source = result.source;
|
|
738
|
-
|
|
739
|
-
repaired.push(entry.tpId);
|
|
740
|
-
auditEntries.push({
|
|
741
|
-
tpId: entry.tpId,
|
|
742
|
-
result: "rewritten",
|
|
743
|
-
detail: result.detail,
|
|
744
|
-
});
|
|
786
|
+
staged.push({ entry, detail: result.detail });
|
|
745
787
|
}
|
|
746
|
-
if (
|
|
788
|
+
if (staged.length > 0) {
|
|
789
|
+
const syntax = validatePythonSyntaxWithoutExecution(source);
|
|
790
|
+
if (!syntax.ok) {
|
|
791
|
+
for (const item of staged) {
|
|
792
|
+
skipped.push({ tpId: item.entry.tpId, detail: syntax.detail });
|
|
793
|
+
auditEntries.push({
|
|
794
|
+
tpId: item.entry.tpId,
|
|
795
|
+
result: "rolled-back-invalid-python",
|
|
796
|
+
detail: syntax.detail,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
747
801
|
await writeFile(absolute, source, "utf8");
|
|
748
802
|
changedFiles.push(scriptPath);
|
|
803
|
+
for (const item of staged) {
|
|
804
|
+
repaired.push(item.entry.tpId);
|
|
805
|
+
auditEntries.push({
|
|
806
|
+
tpId: item.entry.tpId,
|
|
807
|
+
result: "rewritten",
|
|
808
|
+
detail: `${item.detail}; ${syntax.detail}`,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
749
811
|
}
|
|
750
812
|
}
|
|
751
813
|
const afterHashes = [];
|
|
@@ -777,15 +839,15 @@ export async function writeScenarioParamRepairAudit(input) {
|
|
|
777
839
|
return filePath;
|
|
778
840
|
}
|
|
779
841
|
export function renderBackendTestFailureAnalysis(input) {
|
|
780
|
-
const overviewNarrative = input.failed === 0
|
|
842
|
+
const overviewNarrative = input.failed + input.errors === 0
|
|
781
843
|
? "本轮无失败用例。报告仍保留场景-参数一致性与执行摘要,供审计。"
|
|
782
|
-
: `本轮共 ${input.failed}
|
|
844
|
+
: `本轮共 ${input.failed} 个失败用例、${input.errors} 个错误用例。分类优先参考场景-参数一致性 final facts,再结合断言与环境证据。`;
|
|
783
845
|
return [
|
|
784
846
|
"# 测试失败用例分析报告",
|
|
785
847
|
"",
|
|
786
848
|
`> 报告生成时间:${input.generatedAt}`,
|
|
787
849
|
`> 测试报告来源:${input.htmlReportPath}`,
|
|
788
|
-
`> 测试总计:${input.total} 个用例(${input.passed} Passed, ${input.failed} Failed)`,
|
|
850
|
+
`> 测试总计:${input.total} 个用例(${input.passed} Passed, ${input.failed} Failed, ${input.errors} Error)`,
|
|
789
851
|
`> 测试耗时:${input.durationLabel}`,
|
|
790
852
|
`> 测试环境:${input.environmentSummary.split("\n")[0] ?? "unavailable"}`,
|
|
791
853
|
`> 接口:${input.primaryOperation ?? "—"}`,
|
|
@@ -271,6 +271,39 @@ function pythonParseable(source) {
|
|
|
271
271
|
// legal multi-line definitions such as `def f(\n x,\n):` — removed.
|
|
272
272
|
return true;
|
|
273
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Forbidden generated shared helper/factory namespaces for the self-contained
|
|
276
|
+
* pytest module contract. A self-contained testcase/test_*.py child must not
|
|
277
|
+
* import from generated shared namespaces such as `testcase_helpers`,
|
|
278
|
+
* `testcase.helpers`, or `testcase.factories`; those belong to the pytest plan
|
|
279
|
+
* node's shared resources, not to a per-module pytest script. Rejecting such
|
|
280
|
+
* imports early (before pytest collection) gives an actionable, recoverable
|
|
281
|
+
* incomplete-write-set signal carrying the exact module target path.
|
|
282
|
+
*/
|
|
283
|
+
export const FORBIDDEN_GENERATED_IMPORT_NAMESPACES = [
|
|
284
|
+
"testcase_helpers",
|
|
285
|
+
"testcase.helpers",
|
|
286
|
+
"testcase.factories",
|
|
287
|
+
];
|
|
288
|
+
const FORBIDDEN_GENERATED_IMPORT_PATTERN = /^\s*(?:from\s+(testcase_helpers|testcase\.helpers|testcase\.factories)\b|import\s+(testcase_helpers|testcase\.helpers|testcase\.factories)\b)/gm;
|
|
289
|
+
/**
|
|
290
|
+
* Scan Python source for imports from forbidden generated shared namespaces.
|
|
291
|
+
* Anchored at line start with a word boundary so `testcase.helpers_other` or a
|
|
292
|
+
* substring inside a docstring sentence is not falsely flagged. Returns the
|
|
293
|
+
* list of offending namespace module names (deduplicated, order-preserving).
|
|
294
|
+
*/
|
|
295
|
+
export function findForbiddenGeneratedImports(source) {
|
|
296
|
+
const hits = [];
|
|
297
|
+
const seen = new Set();
|
|
298
|
+
for (const match of source.matchAll(FORBIDDEN_GENERATED_IMPORT_PATTERN)) {
|
|
299
|
+
const ns = (match[1] ?? match[2]);
|
|
300
|
+
if (!seen.has(ns)) {
|
|
301
|
+
seen.add(ns);
|
|
302
|
+
hits.push(ns);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return hits;
|
|
306
|
+
}
|
|
274
307
|
/**
|
|
275
308
|
* Looser Python validity check for shared helpers/factories emitted by the
|
|
276
309
|
* pytest plan node. Unlike pythonParseable, it does NOT require balanced
|
|
@@ -417,6 +450,17 @@ export async function assessBackendTestShardChildCompleteness(input) {
|
|
|
417
450
|
recoverable: true,
|
|
418
451
|
});
|
|
419
452
|
}
|
|
453
|
+
const forbidden = findForbiddenGeneratedImports(body);
|
|
454
|
+
if (forbidden.length > 0) {
|
|
455
|
+
if (!brokenPaths.includes(target))
|
|
456
|
+
brokenPaths.push(target);
|
|
457
|
+
issues.push({
|
|
458
|
+
code: "T5",
|
|
459
|
+
path: target,
|
|
460
|
+
detail: `forbidden import from ${forbidden.join(", ")} in ${target}; self-contained pytest module must not import generated shared helper/factory namespaces`,
|
|
461
|
+
recoverable: true,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
420
464
|
}
|
|
421
465
|
}
|
|
422
466
|
const recoverableTargets = orderedUnique([...missingPaths, ...brokenPaths]);
|
|
@@ -584,6 +628,17 @@ export async function assessBackendTestPytestWriterCompleteness(workspaceRoot) {
|
|
|
584
628
|
});
|
|
585
629
|
continue;
|
|
586
630
|
}
|
|
631
|
+
const forbidden = findForbiddenGeneratedImports(source);
|
|
632
|
+
if (forbidden.length > 0) {
|
|
633
|
+
brokenPaths.push(expected);
|
|
634
|
+
issues.push({
|
|
635
|
+
code: "T5",
|
|
636
|
+
path: expected,
|
|
637
|
+
detail: `forbidden import from ${forbidden.join(", ")} in ${expected}; self-contained pytest module must not import generated shared helper/factory namespaces`,
|
|
638
|
+
recoverable: true,
|
|
639
|
+
});
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
587
642
|
const markdown = await readFile(path.join(workspaceRoot, moduleRel), "utf8");
|
|
588
643
|
const caseIds = orderedUnique([...markdown.matchAll(/\bBE-[A-Z0-9_-]+-\d{2,3}\b/g)].map((item) => item[0]));
|
|
589
644
|
const primaryHints = orderedUnique([
|
|
@@ -3595,7 +3595,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3595
3595
|
"Each Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.",
|
|
3596
3596
|
"Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
|
|
3597
3597
|
"For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
|
|
3598
|
-
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
3598
|
+
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output budget, and keep the total module count at the smallest safe value. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
3599
3599
|
"Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3600
3600
|
intake.boundedSourceContext,
|
|
3601
3601
|
"## Authoritative reference index",
|
|
@@ -3641,7 +3641,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3641
3641
|
maxItems: 64,
|
|
3642
3642
|
maxExpandedNodes: 64,
|
|
3643
3643
|
childIdPrefix: "generate-backend-md-case",
|
|
3644
|
-
tokenBudget: { maxTokensPerCase: 16384 },
|
|
3644
|
+
tokenBudget: { maxTokensPerCase: 16384, maxTotalTokens: 600000 },
|
|
3645
3645
|
childTask: {
|
|
3646
3646
|
executor: "pi",
|
|
3647
3647
|
role: "implementer",
|
|
@@ -3680,18 +3680,17 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3680
3680
|
id: "review-and-revise-backend-md-cases-pi",
|
|
3681
3681
|
depends_on: [generateMdCasesMap.id],
|
|
3682
3682
|
role: "reviewer",
|
|
3683
|
-
executor: "
|
|
3684
|
-
|
|
3685
|
-
complexity: "MED",
|
|
3683
|
+
executor: "static",
|
|
3684
|
+
complexity: "LOW",
|
|
3686
3685
|
writePolicy: "exclusive",
|
|
3687
3686
|
writeSet: ["testcase/md/**"],
|
|
3688
3687
|
allowedPaths: ["testcase/md/**"],
|
|
3689
3688
|
forbiddenPaths: forbidden,
|
|
3690
|
-
outputContract: "
|
|
3689
|
+
outputContract: "Deterministic advisory handoff that reserves testcase/md/** in the approved DAG write set for upstream map children; no model invocation or writes.",
|
|
3691
3690
|
subtask_prompt: [
|
|
3692
3691
|
"Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.",
|
|
3693
3692
|
"Output budget protocol: default to local edit per file; never dump full Matrix/case bodies into assistant chat. Review order is README (Scope/Matrix) then one module file per turn. When adding omitted in-scope cases, write one file per tool call and keep every required section. Do not bulk-delete in-scope cases to save tokens.",
|
|
3694
|
-
"For every variant Test Point, ensure the Markdown scenario intent is machine-checkable
|
|
3693
|
+
"For every variant Test Point, ensure the Markdown scenario intent is machine-checkable with an exact transport target: `场景意图: <TP-ID>; operation=<METHOD /path>; target=<body.field|query.field|path.field|header.field|request>; intent=<empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal-operation|custom-literal:V>; bound=<n optional>; example=<optional>; expectedCode=<optional>`. Never use vague targets such as field=resource/health. Keep pytest params aligned to the exact target.",
|
|
3695
3694
|
"Treat the requirement document as the coverage baseline; scope is limited to operations/rules it (or its referenced API contract) describes, and API contract evidence supplements scenario dimensions. For every in-scope operation, check applicable lifecycle/uniqueness states (including deleted-existing when in scope), valid enum values, bounded invalid classes, min-1/min/nominal/max/max+1, allowed/forbidden format classes, required/null/missing/wrong-type semantics, status/error codes, auth and state transitions. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT. Directly add in-scope omissions; reject scope expansion to operations absent from the requirement document; undefined impact remains GAP/CONFLICT rather than invented behavior.",
|
|
3696
3695
|
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Require the exact `## Coverage Scope` Field/Value table with the `|---|---|` separator row, a valid classification-policy pair, non-empty Affected Operations/Rule Keys/Scope Evidence, and the classification-specific Regression Floor. Require the exact unnumbered `## Coverage Matrix` heading in `testcase/md/README.md`, exact headers, exactly 9 cells in every data row (including a non-empty Dimension), deterministic OpenAPI Rule Keys for every in-scope affected operation, exactly one Matrix row per Rule Key (merge multi-dimension product rows), and bidirectional Matrix Rule/Test Point ↔ Case bindings. Never describe affected-scope coverage as whole-API completeness. Every explicit AC ID must appear in at least one Case `验收标准`; every explicit in-scope AC/REQ/BR Rule Key cited by a Case must have exactly one Coverage Matrix row, and no Case may cite a source Rule Key omitted from the Matrix. Every Matrix Case ID must share at least one of that row's Required Test Points and the Case must cite that Rule Key. Perform an explicit execution-redundancy review: merge checkpoint-only parameter rows, repeated default/read-back assertions, DELETE status/body/follow-up-read checks, response schema/Content-Type checks, PUT full-update/timestamp checks, repeated list setup and identical null/empty inputs when endpoint, input partition, precondition state and expected outcome are the same. Preserve separate POST/PUT, boundary, enum, wrong-type, role/tenant and distinct business-state variants. Directly repair malformed headings/rows/keys and binding modes rather than merely commenting on them. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, a `### 操作步骤` section that contains only a table without any numbered executable line, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), dropped exact `### 操作步骤`/`### 预期结果` headings, and missing or drifted script/function mapping where it can be derived.",
|
|
3697
3696
|
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol, assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, ensure every binding-list Test Point is also present in that Case's `### 测试点`, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
@@ -3701,6 +3700,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3701
3700
|
JSON.stringify(intake.referenceIndex, null, 2),
|
|
3702
3701
|
"For each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.",
|
|
3703
3702
|
].join("\n\n"),
|
|
3703
|
+
static: {
|
|
3704
|
+
resultMarkdown: "Markdown module writers completed. Deterministic node 6 validation owns advisory structure/coverage findings; no model review or rewrite was invoked.",
|
|
3705
|
+
},
|
|
3704
3706
|
};
|
|
3705
3707
|
const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "Record advisory findings for Markdown structure and deterministically analyze the final README Coverage Scope and Coverage Matrix against final Case rule/test-point bindings. Validate the classification-policy pair, affected operations/rules, scope evidence and regression floor; require documented OpenAPI completeness only for declared affected operations, while all explicit AC/REQ/BR remain in scope. Detect missing in-scope product/API rules, enum values, invalid equivalence classes, boundaries, format classes, business lifecycle states, GAP/CONFLICT, bidirectional Matrix/Case drift, non-canonical Case IDs, unclassified Test Points, duplicate binding modes and non-cross-cutting Test Points bound by multiple Cases. Do not validate source-reference existence. Write human and machine evidence from the same facts. Keep quality findings advisory, but fail closed after writing the report when secret-shaped values are detected. Coverage FAIL stays advisory.", "Run-owned reports/backend-md-case-validation.md, reports/backend-test-case-coverage-analysis.md and contracts/backend-test-case-coverage-facts.json v3 with Coverage Scope plus PASS/FAIL/UNAVAILABLE advisory facts; downstream execution continues.");
|
|
3706
3708
|
// N5 line (sharded): pytest shared-asset plan → manifest shell → map_agent barrier.
|
|
@@ -3710,29 +3712,25 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3710
3712
|
id: "generate-backend-pytest-plan-pi",
|
|
3711
3713
|
depends_on: [validateCases.id],
|
|
3712
3714
|
role: "implementer",
|
|
3713
|
-
executor: "
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
writeSet: ["testcase/**/helpers/**", "testcase/**/factories/**"],
|
|
3718
|
-
allowedPaths: Array.from(new Set([...ro, "testcase/**/helpers/**", "testcase/**/factories/**"])),
|
|
3715
|
+
executor: "static",
|
|
3716
|
+
complexity: "LOW",
|
|
3717
|
+
writePolicy: "none",
|
|
3718
|
+
allowedPaths: [],
|
|
3719
3719
|
forbiddenPaths: forbidden,
|
|
3720
|
-
|
|
3721
|
-
writerOutcomePolicy: {
|
|
3722
|
-
type: "implementation-outcome-v1",
|
|
3723
|
-
requireChangedFiles: true,
|
|
3724
|
-
},
|
|
3725
|
-
outputContract: "Write the shared pytest support assets (HTTP logging/redaction helper, request factories, shared fixtures) under testcase/**/helpers/** and testcase/**/factories/** only. Do not write per-module test_*.py here; those are written by downstream sharded nodes. No JSON and no pytest execution.",
|
|
3720
|
+
outputContract: "Deterministic pytest generation handoff. Per-module map children write self-contained test_<module>.py files with bounded local fixtures, HTTP logging/redaction and payload builders; no model invocation, JSON, shared-asset writes or pytest execution.",
|
|
3726
3721
|
subtask_prompt: [
|
|
3727
3722
|
"Convert testcase/md/** into pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. This node writes ONLY the shared pytest support assets (HTTP logging/redaction helper, request factories, shared fixtures); each module's test_<module>.py is written by a downstream sharded node that imports these helpers. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
|
|
3728
3723
|
"Output budget protocol (hard, max output <=16K per turn): Write helpers/factories one file per write/edit tool call. Never paste full Python modules into assistant chat. Do not reduce params/assertions/skips semantics to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken shared files.",
|
|
3729
3724
|
"Align every variant pytest.param payload with the Markdown scenario intent (empty/missing/null/length/pattern/enum/wrong-type/nominal). Prefer literal payloads over Faker for intent-critical fields so pre-execution scenario-param checks can verify them.",
|
|
3730
|
-
"Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions.",
|
|
3725
|
+
"Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions. If shared pytest fixtures are generated, keep their dependency graph in one provider module and require each downstream test module to register that provider with an exact pytest_plugins tuple; importing only the outer fixture is insufficient and will be rejected by fixture-resolution preflight.",
|
|
3731
3726
|
"HTTP response header names are case-insensitive. If the helper stores a lower-case normalized header map, every Content-Type or other header assertion must query the lower-case key (for example `content-type`) or use an explicitly case-insensitive accessor; never call a case-sensitive plain dict with `Content-Type` when the stored key is lower-case. Preserve the actual media-type assertion rather than dropping it.",
|
|
3732
3727
|
"Compare timestamps and other semantically equivalent protocol values by parsed meaning, not byte-for-byte serialization. In particular, normalize valid ISO-8601 instants before equality/order assertions so differences such as omitted trailing fractional seconds do not create TestBug failures; preserve exact-string assertions only when the Markdown explicitly requires representation equality.",
|
|
3733
3728
|
"Before logging, recursively redact sensitive keys and header values including authorization, proxy-authorization, cookie, set-cookie, token, password, secret, api key and credentials. Never print full Authorization/Cookie values. Apply bounded truncation to serialized request and response bodies (with an explicit truncation marker) so large payloads cannot flood pytest or report artifacts.",
|
|
3734
3729
|
"Do not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
|
|
3735
3730
|
].join("\n\n"),
|
|
3731
|
+
static: {
|
|
3732
|
+
resultMarkdown: "Shared pytest plan is deterministic: each downstream module writer owns one self-contained test_<module>.py and must not depend on generated shared helper/factory files.",
|
|
3733
|
+
},
|
|
3736
3734
|
};
|
|
3737
3735
|
const materializePytestManifest = {
|
|
3738
3736
|
id: "materialize-backend-pytest-module-manifest-shell",
|
|
@@ -3760,7 +3758,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3760
3758
|
writePolicy: "none",
|
|
3761
3759
|
allowedPaths: [],
|
|
3762
3760
|
forbiddenPaths: forbidden,
|
|
3763
|
-
outputContract: "Serial aggregate of sharded pytest module writers. Each child writes exactly one testcase/test_<stem>.py with its own 16K Pi budget
|
|
3761
|
+
outputContract: "Serial aggregate of sharded pytest module writers. Each child writes exactly one self-contained testcase/test_<stem>.py with its own 16K Pi budget and no generated shared-asset dependency.",
|
|
3764
3762
|
subtask_prompt: "Expand the README module manifest into one sharded pytest writer child per module and run them serially. Child failures fail-close the map barrier.",
|
|
3765
3763
|
static: { resultMarkdown: "Backend-test pytest module map expansion barrier." },
|
|
3766
3764
|
dynamicExpansion: {
|
|
@@ -3771,13 +3769,13 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3771
3769
|
maxItems: 64,
|
|
3772
3770
|
maxExpandedNodes: 64,
|
|
3773
3771
|
childIdPrefix: "generate-backend-pytest-case",
|
|
3774
|
-
tokenBudget: { maxTokensPerCase: 16384 },
|
|
3772
|
+
tokenBudget: { maxTokensPerCase: 16384, maxTotalTokens: 600000 },
|
|
3775
3773
|
childTask: {
|
|
3776
3774
|
executor: "pi",
|
|
3777
3775
|
role: "implementer",
|
|
3778
3776
|
skills: BACKEND_TEST_SKILLS_BY_ROLE.implementer,
|
|
3779
3777
|
toolProfile: "write",
|
|
3780
|
-
complexity: "
|
|
3778
|
+
complexity: "MED",
|
|
3781
3779
|
writePolicy: "exclusive",
|
|
3782
3780
|
allowedPaths: ["testcase/test_{{item.stem}}.py"],
|
|
3783
3781
|
forbiddenPaths: Array.from(new Set([
|
|
@@ -3796,18 +3794,18 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3796
3794
|
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
3797
3795
|
outputContract: "Write exactly one pytest module file testcase/test_<stem>.py whose actual test function region contains the exact Case ID, preferably in the function name or docstring. testcase/md/<module>.md (excluding README.md) maps one-to-one to testcase/test_<module>.py; never merge or split modules. No JSON and no pytest execution.",
|
|
3798
3796
|
subtaskPromptTemplate: [
|
|
3799
|
-
"Convert the single Markdown module testcase/md/{{item.stem}}.md into
|
|
3797
|
+
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. After reading the module Markdown and the bounded pytest config/conftest, immediately use write tools to create the single file testcase/test_{{item.stem}}.py. Define any bounded HTTP client fixture, request logging/redaction/truncation helper and payload builders needed by this module inside that same file; do not import generated testcase/**/helpers/** or testcase/**/factories/** assets. Do not end after analysis or planning. Do not modify Markdown, conftest, helpers/factories, or any other module's pytest script.",
|
|
3800
3798
|
"Output budget protocol (hard, max output <=16K per turn): Write exactly one test_{{item.stem}}.py. Never paste full Python modules into assistant chat. Do not merge or split modules. Do not reduce params/assertions/skips to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken scripts.",
|
|
3801
3799
|
"Align every variant pytest.param payload with the Markdown scenario intent (empty/missing/null/length/pattern/enum/wrong-type/nominal). Prefer literal payloads over Faker for intent-critical fields so pre-execution scenario-param checks can verify them.",
|
|
3802
3800
|
'Ensure every final Markdown Case ID in this module appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id="TP-...")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. Use `pytest.param(..., id=...)` for every row; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task\'s explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.',
|
|
3803
3801
|
"Name the generated pytest file so it corresponds one-to-one with its source Markdown module file: this module stem `{{item.stem}}` maps to exactly one `testcase/test_{{item.stem}}.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `resource_notes` → `testcase/test_resource_notes.py`, `health` → `testcase/test_health.py`. If Markdown automation mapping names a different path than this module stem path, still write the module stem path and do not invent prefixes. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
|
|
3804
|
-
"
|
|
3802
|
+
"Keep this module self-contained: define module-local fixtures and helpers directly in testcase/test_{{item.stem}}.py, so pytest discovers every fixture dependency without external plugin registration. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions. Recursively redact sensitive values and apply bounded truncation before logging.",
|
|
3805
3803
|
"Do not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
|
|
3806
3804
|
].join("\n\n"),
|
|
3807
3805
|
},
|
|
3808
3806
|
},
|
|
3809
3807
|
};
|
|
3810
|
-
const collectionAssess = shellNode("assess-backend-pytest-collection-shell", [generatePytestCasesMap.id], "markdown-collection-assess", "Resolve final Markdown-mapped scripts before any business test body execution.
|
|
3808
|
+
const collectionAssess = shellNode("assess-backend-pytest-collection-shell", [generatePytestCasesMap.id], "markdown-collection-assess", "Resolve final Markdown-mapped scripts before any business test body execution. Run scenario-param assess/at-most-one deterministic repair first, then pytest --collect-only and a no-business-body pytest --setup-plan fixture-resolution preflight. A safe missing mapped script, generated-local syntax/import defect, or generated fixture dependency/plugin-registration defect is REPAIRABLE; dependency, third-party plugin, production-module, environment, safety and unknown failures remain blocked. Materialize hash-bound collection-v3 facts with repairPaths and fixtureResolutionStatus.", "Run-owned reports/backend-test-pytest-collection-initial.md and contracts/backend-test-pytest-collection-initial.json (collection-v3) with bounded collection/fixture diagnostics, repairPaths, asset hashes, collected item IDs and deterministic repair eligibility.", [], 120000);
|
|
3811
3809
|
const repairPytest = {
|
|
3812
3810
|
id: "repair-backend-pytest-collection-pi",
|
|
3813
3811
|
depends_on: [collectionAssess.id],
|
|
@@ -3835,7 +3833,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3835
3833
|
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked, followed by a concise repair summary. Modify only generated pytest scripts/helpers/factories and preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
|
|
3836
3834
|
subtask_prompt: [
|
|
3837
3835
|
"Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution.",
|
|
3838
|
-
"Fix only
|
|
3836
|
+
"Fix only readiness-proven generated testcase-local defects on initial facts repairPaths: create exact safe missing mapped test_*.py paths, repair syntax/import/symbol/decorator/parameterization, or close generated fixture dependencies and plugin registration. For fixture defects inspect both provider and importer; when a shared fixture depends on sibling fixtures, register the whole provider module through an exact pytest_plugins declaration rather than importing only the outer fixture. Do not create unrelated pytest scripts.",
|
|
3839
3837
|
"Preserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.",
|
|
3840
3838
|
"Use local edit only on assessment-listed paths; keep summaries short; never rewrite unrelated modules.",
|
|
3841
3839
|
"Do not read task source/** or reinterpret requirements. Do not modify Markdown, conftest, pytest config, production code or dependencies.",
|
|
@@ -3843,37 +3841,28 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3843
3841
|
"Do not execute pytest; the deterministic effective collection gate owns the final collection attempt.",
|
|
3844
3842
|
].join("\n\n"),
|
|
3845
3843
|
};
|
|
3846
|
-
const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection passed, verify unchanged asset hashes and reuse it
|
|
3844
|
+
const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection+fixture readiness passed, verify unchanged asset hashes and reuse it. If the single repair ran, rerun scenario-param preflight, final collection and no-business-body fixture resolution once. BLOCKED facts, repair failure, residual fixture failure or hash drift prevent business pytest execution. Materialize canonical backend-test-execution-readiness.json.", "Run-owned effective collection-v3 facts plus contracts/backend-test-execution-readiness.json proving exact final assets are collectable, fixture-resolvable and hash-bound; initial PASS is reused, repair path records attempt=1.", [], 120000);
|
|
3847
3845
|
collectionEffective.dependsPolicy = "all-or-condition-skip";
|
|
3848
|
-
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only Markdown-mapped pytest scripts
|
|
3846
|
+
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only final readiness-authorized Markdown-mapped pytest scripts. Produce bidirectional Markdown module/Case/Test Point ↔ pytest file/primary symbol correspondence and logging findings. scenario-param assessment/repair already ran before collection; consume and display its final PASS/PARTIAL/FAIL/UNAVAILABLE facts without modifying pytest assets after readiness was frozen. Correspondence findings remain advisory; Never block pytest solely on correspondence FAIL.", "Run-owned reports/backend-test-traceability.md, reports/backend-test-markdown-pytest-correspondence.md, contracts/backend-test-markdown-pytest-correspondence-facts.json, reports/backend-test-scenario-param-consistency.md and contracts/backend-test-scenario-param-consistency-facts.json (initial+final) with optional repair audit; PASS/FAIL/UNAVAILABLE correspondence facts bound after effective collection.");
|
|
3849
3847
|
const manifest = shellNode("backend-test-case-manifest-shell", [traceability.id], "markdown-manifest", "Materialize the canonical Backend Test Case Manifest only from contracts/backend-test-case-coverage-facts.json and contracts/backend-test-markdown-pytest-correspondence-facts.json. Validate schema, task binding, input hashes and freshness; never re-read source semantics, re-analyze Coverage Matrix, rescan pytest symbols or recompute a second set of metrics. Missing/stale/conflicting facts produce partial/unavailable diagnostics rather than fabricated zeros.", "Run-owned contracts/backend-test-case-manifest.json with materializationStatus, sourceFactsIssues, validated coverageScope, coverageSummary, ruleCoverageSummary and correspondenceSummary; this is the single machine input for L-5 and closeout.");
|
|
3850
3848
|
const pytestCommand = [
|
|
3851
3849
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3852
3850
|
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3853
3851
|
].join("; ");
|
|
3854
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "
|
|
3852
|
+
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "Read canonical contracts/backend-test-execution-readiness.json, verify final asset hashes, then resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论 and quality status; make node 6 Markdown validation + case coverage and node 13 traceability + Markdown-to-pytest correspondence expandable to their full escaped details; show each failure overview item with its original pytest message plus deterministic evidence-based reason analysis; list failure/error case cards before the remaining cases while preserving stable order. Each polished per-case result card includes concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3855
3853
|
if (execute.shell) {
|
|
3856
3854
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3857
3855
|
}
|
|
3858
|
-
const canWriteReport = taskAllowsBackendTestReportWrite(sources);
|
|
3859
3856
|
const report = {
|
|
3860
3857
|
id: "backend-test-report-and-l5-pi",
|
|
3861
3858
|
depends_on: [execute.id],
|
|
3862
3859
|
role: "closeout",
|
|
3863
|
-
executor: "
|
|
3864
|
-
complexity: "
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
toolProfile: "write",
|
|
3868
|
-
writePolicy: "exclusive",
|
|
3869
|
-
writeSet: ["docs/test-reports/**"],
|
|
3870
|
-
allowedPaths: ["docs/test-reports/**"],
|
|
3871
|
-
}
|
|
3872
|
-
: { writePolicy: "read-only", allowedPaths: ro }),
|
|
3860
|
+
executor: "static",
|
|
3861
|
+
complexity: "LOW",
|
|
3862
|
+
writePolicy: "none",
|
|
3863
|
+
allowedPaths: [],
|
|
3873
3864
|
forbiddenPaths: forbidden,
|
|
3874
|
-
outputContract:
|
|
3875
|
-
? "Final Markdown report and L-5 conclusion under docs/test-reports/**; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked, not re-rendered; no JSON."
|
|
3876
|
-
: "Final Markdown report and L-5 conclusion in assistant output; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked; no JSON or writes.",
|
|
3865
|
+
outputContract: "Deterministic evidence handoff linking the node 15 Markdown facts, HTML report, failure analysis and L-5 dashboard; no model invocation, JSON or writes.",
|
|
3877
3866
|
subtask_prompt: [
|
|
3878
3867
|
"Generate the final Markdown report only from authoritative run-owned artifacts. Read node 1 reports/backend-test-environment.md; node 6 backend-md-case-validation.md and backend-test-case-coverage-analysis.md; nodes 10/12 backend-test-pytest-collection initial/effective reports and facts; node 13 backend-test-traceability.md, backend-test-markdown-pytest-correspondence.md and backend-test-scenario-param-consistency.md; node 14 contracts/backend-test-case-manifest.json; and node 15 backend-test-result.json, backend-test-facts.md, backend-test-failure-analysis.md, pytest-html/HTML and L-5 dashboard. Do not use node 2/3/4/7/8/9 assistant prose as facts. Do not emit JSON.",
|
|
3879
3868
|
"Use this exact human-facing section order: 测试结论 → 执行概览 → 质量校验 → 失败分析 → 风险与建议 → 证据与 L-5. Put the decision and key numbers first, use compact tables/bullets, and keep headings concise. Do not paste entire upstream reports, duplicate per-case tables already present in facts, or repeat the same evidence in multiple sections; link to paths/hashes and quote only the findings needed for the conclusion. Prefer linking reports/backend-test-failure-analysis.md for structured failure analysis rather than inventing classifications.",
|
|
@@ -3882,10 +3871,18 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3882
3871
|
"Always state the exact Coverage Scope classification, policy, affected operations, regression floor, completeness claim, PASS/FAIL/UNAVAILABLE status and findings from node 6 case validation + coverage, plus node 13 traceability + correspondence. Affected-scope or affected-operations-full coverage must never be described as whole-API completeness unless every operation is explicitly listed. Their FAIL status does not block pytest, but it must remain visible and must never be rewritten as PASS.",
|
|
3883
3872
|
"Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage availability, and L-5 READY/NOT READY. Distinguish Markdown Case count, primary pytest symbol count, collected pytest item count, variant/assertion/cross-cutting Test Point counts and execution amplification; never describe pytest item count as the number of business scenarios.",
|
|
3884
3873
|
"Never override Shell/pytest-html facts. L-5 requires pass=100%, AC=100%, automation>=90%, line>=80%, branch>=70%, skipped=0 and no blocking Critical risk.",
|
|
3885
|
-
|
|
3886
|
-
? "Write only under docs/test-reports/**."
|
|
3887
|
-
: "Keep the full report in assistant output.",
|
|
3874
|
+
"Node 15 already rendered the authoritative Markdown and HTML outputs; this node only hands off their stable paths without recomputation.",
|
|
3888
3875
|
].join("\n\n"),
|
|
3876
|
+
static: {
|
|
3877
|
+
resultMarkdown: [
|
|
3878
|
+
"# Backend-test deterministic closeout",
|
|
3879
|
+
"",
|
|
3880
|
+
"Authoritative execution facts: `reports/backend-test.md` and `contracts/backend-test-result.json`.",
|
|
3881
|
+
"Failure analysis: `reports/backend-test-failure-analysis.md`.",
|
|
3882
|
+
"HTML report: `reports/backend-test.html`.",
|
|
3883
|
+
"L-5 dashboard: `reports/backend-test-l5-dashboard.html`.",
|
|
3884
|
+
].join("\n"),
|
|
3885
|
+
},
|
|
3889
3886
|
};
|
|
3890
3887
|
const spec = {
|
|
3891
3888
|
version: 3,
|