@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -17
- package/dist/application/task-lifecycle/advance.js +254 -4
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/shell-executor.js +4 -91
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +72 -42
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +180 -16
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/model-resolver.js +17 -0
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +1007 -188
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +495 -157
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/session-store.js +39 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/operator-actions.js +559 -68
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
- package/dist/worker/console/static/assets/index-CMHovlqG.js +32 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
- package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
- package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
- package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +34 -46
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/backend-test-dag.json +29 -32
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/local-jacoco-coverage/SKILL.md +281 -0
- package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
- package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
|
@@ -149,10 +149,6 @@ export const backendTestCaseManifestSchema = z
|
|
|
149
149
|
crossCuttingTestPointCount: z.number().int().min(0),
|
|
150
150
|
unclassifiedTestPointCount: z.number().int().min(0),
|
|
151
151
|
duplicateBindingTestPointCount: z.number().int().min(0),
|
|
152
|
-
payloadSafeCount: z.number().int().min(0),
|
|
153
|
-
payloadUnsafeCount: z.number().int().min(0),
|
|
154
|
-
payloadUnavailableCount: z.number().int().min(0),
|
|
155
|
-
duplicateExecutionSignatureCount: z.number().int().min(0),
|
|
156
152
|
}).strict().optional(),
|
|
157
153
|
artifactRefs: z.object({
|
|
158
154
|
caseCoverageFacts: z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/).optional() }).strict(),
|
|
@@ -471,10 +471,7 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
471
471
|
}
|
|
472
472
|
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
473
473
|
const mappedScripts = extractMappedPytestScripts(testCase.body);
|
|
474
|
-
if (
|
|
475
|
-
// Evidence-only meta cases intentionally declare 脚本/primary symbol as 无 and keep only assertion/cross-cutting TPs.
|
|
476
|
-
}
|
|
477
|
-
else if (mappedScripts.length === 0) {
|
|
474
|
+
if (mappedScripts.length === 0) {
|
|
478
475
|
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
|
|
479
476
|
}
|
|
480
477
|
else {
|
|
@@ -563,24 +560,6 @@ function extractMappedPytestScripts(testCaseBody) {
|
|
|
563
560
|
.map((match) => match[1].replaceAll("\\", "/"))
|
|
564
561
|
.filter((value) => !value.includes("..")));
|
|
565
562
|
}
|
|
566
|
-
function isEvidenceOnlyMarkdownCaseBody(testCaseBody) {
|
|
567
|
-
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
568
|
-
if (!automation.trim())
|
|
569
|
-
return false;
|
|
570
|
-
const scriptLine = /^\s*[-*+]\s*(?:脚本|script)\s*[::]\s*(.*)$/im.exec(automation)?.[1]?.replaceAll("`", "").trim() ??
|
|
571
|
-
"";
|
|
572
|
-
const primaryLine = /^\s*[-*+]\s*(?:primary symbol|Primary Symbol|主测试符号)\s*[::]\s*(.*)$/im.exec(automation)?.[1]
|
|
573
|
-
?.replaceAll("`", "")
|
|
574
|
-
.trim() ?? "";
|
|
575
|
-
const noneLike = /^(?:none|无|n\/a|not-applicable)(?:\s*[((].*[))])?$/i;
|
|
576
|
-
const scriptIsNone = noneLike.test(scriptLine) || scriptLine.length === 0;
|
|
577
|
-
const primaryIsNone = noneLike.test(primaryLine) || primaryLine.length === 0;
|
|
578
|
-
const hasMappedScript = extractMappedPytestScripts(testCaseBody).length > 0;
|
|
579
|
-
const hasPrimary = /\btest_[A-Za-z0-9_]+\b/.test(primaryLine);
|
|
580
|
-
const hasVariant = /(?:变体测试点|Variant Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
|
|
581
|
-
const hasAssertionOrCross = /(?:场景断言测试点|Assertion Test Points|横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
|
|
582
|
-
return scriptIsNone && primaryIsNone && !hasMappedScript && !hasPrimary && !hasVariant && hasAssertionOrCross;
|
|
583
|
-
}
|
|
584
563
|
function isSafeBackendPytestScript(script, workspaceRoot) {
|
|
585
564
|
const absolute = path.resolve(workspaceRoot, script);
|
|
586
565
|
const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
|
|
@@ -642,9 +621,6 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
642
621
|
for (const testCase of splitCases(content)) {
|
|
643
622
|
markdownIds.add(testCase.id);
|
|
644
623
|
const scripts = extractMappedPytestScripts(testCase.body);
|
|
645
|
-
if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
624
|
if (scripts.length === 0) {
|
|
649
625
|
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射`);
|
|
650
626
|
continue;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
const PRIORITY_ONLY_MODULE_STEMS = new Set(["p0", "p1", "p2"]);
|
|
3
|
-
const OPAQUE_HASH_MODULE_STEM = /^[a-f][a-f0-9]{6,63}$/;
|
|
4
|
-
export const BACKEND_TEST_MAX_MODULE_COUNT = 8;
|
|
5
3
|
export function normalizeBackendTestModuleStemCandidate(raw) {
|
|
6
4
|
return path.basename(raw)
|
|
7
5
|
.replace(/\.(?:md|py)$/i, "")
|
|
@@ -13,9 +11,6 @@ export function normalizeBackendTestModuleStemCandidate(raw) {
|
|
|
13
11
|
export function isPriorityOnlyBackendTestModuleStem(raw) {
|
|
14
12
|
return PRIORITY_ONLY_MODULE_STEMS.has(normalizeBackendTestModuleStemCandidate(raw));
|
|
15
13
|
}
|
|
16
|
-
export function isOpaqueHashBackendTestModuleStem(raw) {
|
|
17
|
-
return OPAQUE_HASH_MODULE_STEM.test(normalizeBackendTestModuleStemCandidate(raw));
|
|
18
|
-
}
|
|
19
14
|
export function isPriorityOnlyBackendPytestScript(script) {
|
|
20
15
|
const basename = path.posix.basename(script.replaceAll("\\", "/"));
|
|
21
16
|
const match = /^test_(.+)\.py$/i.exec(basename);
|
|
@@ -5,15 +5,6 @@ import { z } from "zod";
|
|
|
5
5
|
import { priorityOnlyBackendPytestScripts } from "./backend-test-module-stem.js";
|
|
6
6
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
7
7
|
const MAX_DIAGNOSTIC_CHARS = 12_000;
|
|
8
|
-
/** Upper bound for assertBackendTestExecutionReadinessFresh Error.message (not collection excerpts). */
|
|
9
|
-
export const MAX_READINESS_ERROR_CHARS = 2_048;
|
|
10
|
-
const READINESS_EXCLUSION_CATEGORIES = [
|
|
11
|
-
"correspondence-missing",
|
|
12
|
-
"correspondence-status",
|
|
13
|
-
"payload-contract-status",
|
|
14
|
-
"scenario-param-field",
|
|
15
|
-
"other",
|
|
16
|
-
];
|
|
17
8
|
export const backendPytestCollectionFindingSchema = z.object({
|
|
18
9
|
kind: z.string().min(1),
|
|
19
10
|
classification: z.literal("test-asset-defect"),
|
|
@@ -36,8 +27,6 @@ export const backendPytestCollectionFactsSchema = z.object({
|
|
|
36
27
|
missingMappedScripts: z.array(z.string()),
|
|
37
28
|
assetFiles: z.array(z.string()),
|
|
38
29
|
inputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
39
|
-
hazardOccurrenceCounts: z.record(z.string(), z.record(z.string(), z.number().int().min(0))),
|
|
40
|
-
hazardOccurrences: z.record(z.string(), z.record(z.string(), z.array(z.string()))).optional(),
|
|
41
30
|
pytestExitCode: z.number().int().nullable(),
|
|
42
31
|
collectedItemCount: z.number().int().min(0),
|
|
43
32
|
collectedItemIds: z.array(z.string()),
|
|
@@ -60,14 +49,8 @@ export const backendPytestCollectionFactsSchema = z.object({
|
|
|
60
49
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "backend pytest fixture resolution without an attempt cannot have an exit code" });
|
|
61
50
|
}
|
|
62
51
|
});
|
|
63
|
-
const backendTestExcludedItemSchema = z.object({
|
|
64
|
-
itemId: z.string().min(1),
|
|
65
|
-
caseId: z.string().optional(),
|
|
66
|
-
symbol: z.string().optional(),
|
|
67
|
-
reasons: z.array(z.string().min(1)).min(1),
|
|
68
|
-
}).strict();
|
|
69
52
|
export const backendTestExecutionReadinessSchema = z.object({
|
|
70
|
-
schemaId: z.literal("backend-test-execution-readiness-
|
|
53
|
+
schemaId: z.literal("backend-test-execution-readiness-v1"),
|
|
71
54
|
status: z.enum(["PASS", "PARTIAL", "BLOCKED"]),
|
|
72
55
|
collectionStatus: z.literal("PASS"),
|
|
73
56
|
fixtureResolutionStatus: z.literal("PASS"),
|
|
@@ -78,94 +61,9 @@ export const backendTestExecutionReadinessSchema = z.object({
|
|
|
78
61
|
}).strict(),
|
|
79
62
|
mappedScripts: z.array(z.string()).min(1),
|
|
80
63
|
collectedItemIds: z.array(z.string()),
|
|
81
|
-
eligibleItemIds: z.array(z.string()),
|
|
82
|
-
excludedItems: z.array(backendTestExcludedItemSchema),
|
|
83
64
|
fixtureIssues: z.array(z.string()),
|
|
84
65
|
assetHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
85
|
-
eligibilityInputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
86
66
|
}).strict();
|
|
87
|
-
const REPAIR_HAZARD_PATTERNS = [
|
|
88
|
-
{ label: "skip/xfail", source: "pytest\\.mark\\.(?:skip|skipif|xfail)\\b|pytest\\.(?:skip|xfail)\\s*\\(" },
|
|
89
|
-
{ label: "sys.path/PYTHONPATH mutation", source: "sys\\.path\\s*\\.|PYTHONPATH" },
|
|
90
|
-
{ label: "ImportError fallback", source: "except\\s+ImportError\\b" },
|
|
91
|
-
{ label: "broad exception swallowing", source: "except\\s+(?:Exception|BaseException)\\b[\\s\\S]{0,160}?\\bpass\\b" },
|
|
92
|
-
];
|
|
93
|
-
function normalizeSourceNewlines(source) {
|
|
94
|
-
return source.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Normalize a hazard occurrence identity by stripping trailing Python `#` comments
|
|
98
|
-
* while preserving `#` characters inside string literals. Fail closed: if quote
|
|
99
|
-
* state is ambiguous, keep the original trimmed line rather than over-stripping.
|
|
100
|
-
*/
|
|
101
|
-
function normalizeHazardOccurrenceIdentity(line) {
|
|
102
|
-
const trimmed = line.trimEnd().trim();
|
|
103
|
-
if (!trimmed.includes("#"))
|
|
104
|
-
return trimmed;
|
|
105
|
-
let inSingle = false;
|
|
106
|
-
let inDouble = false;
|
|
107
|
-
let escaped = false;
|
|
108
|
-
for (let i = 0; i < trimmed.length; i += 1) {
|
|
109
|
-
const ch = trimmed[i];
|
|
110
|
-
if (escaped) {
|
|
111
|
-
escaped = false;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if ((inSingle || inDouble) && ch === "\\") {
|
|
115
|
-
escaped = true;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (!inDouble && ch === "'") {
|
|
119
|
-
inSingle = !inSingle;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (!inSingle && ch === '"') {
|
|
123
|
-
inDouble = !inDouble;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (!inSingle && !inDouble && ch === "#") {
|
|
127
|
-
return trimmed.slice(0, i).trimEnd();
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// Unclosed quote or no out-of-string `#`: do not invent a comment boundary.
|
|
131
|
-
return trimmed;
|
|
132
|
-
}
|
|
133
|
-
function listPatternOccurrences(source, patternSource, flags = "") {
|
|
134
|
-
const normalized = normalizeSourceNewlines(source);
|
|
135
|
-
const pattern = new RegExp(patternSource, flags.includes("g") ? flags : `${flags}g`);
|
|
136
|
-
const identities = [];
|
|
137
|
-
for (const match of normalized.matchAll(pattern)) {
|
|
138
|
-
const index = match.index ?? 0;
|
|
139
|
-
const lineStart = normalized.lastIndexOf("\n", Math.max(0, index - 1)) + 1;
|
|
140
|
-
const lineEnd = normalized.indexOf("\n", index);
|
|
141
|
-
const line = normalized.slice(lineStart, lineEnd === -1 ? normalized.length : lineEnd).trimEnd();
|
|
142
|
-
identities.push(normalizeHazardOccurrenceIdentity(line));
|
|
143
|
-
}
|
|
144
|
-
return identities;
|
|
145
|
-
}
|
|
146
|
-
function scanRepairHazardOccurrenceLists(source) {
|
|
147
|
-
const lists = {};
|
|
148
|
-
for (const rule of REPAIR_HAZARD_PATTERNS) {
|
|
149
|
-
lists[rule.label] = listPatternOccurrences(source, rule.source, rule.flags);
|
|
150
|
-
}
|
|
151
|
-
return lists;
|
|
152
|
-
}
|
|
153
|
-
function multisetCounts(identities) {
|
|
154
|
-
const counts = new Map();
|
|
155
|
-
for (const identity of identities) {
|
|
156
|
-
counts.set(identity, (counts.get(identity) ?? 0) + 1);
|
|
157
|
-
}
|
|
158
|
-
return counts;
|
|
159
|
-
}
|
|
160
|
-
function hasMultisetIncrement(initialIdentities, finalIdentities) {
|
|
161
|
-
const initial = multisetCounts(initialIdentities);
|
|
162
|
-
const final = multisetCounts(finalIdentities);
|
|
163
|
-
for (const [identity, finalCount] of final) {
|
|
164
|
-
if (finalCount > (initial.get(identity) ?? 0))
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
67
|
function repoRef(workspaceRoot, absolutePath) {
|
|
170
68
|
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, "/");
|
|
171
69
|
}
|
|
@@ -214,17 +112,9 @@ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScri
|
|
|
214
112
|
const pythonFiles = await walkPythonFiles(path.resolve(workspaceRoot, "testcase"));
|
|
215
113
|
const assetFiles = [...new Set(pythonFiles.map((file) => repoRef(workspaceRoot, file)))].sort();
|
|
216
114
|
const inputHashes = {};
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const bytes = await readFile(path.resolve(workspaceRoot, file));
|
|
221
|
-
const source = bytes.toString("utf8");
|
|
222
|
-
inputHashes[file] = sha256(bytes);
|
|
223
|
-
const lists = scanRepairHazardOccurrenceLists(source);
|
|
224
|
-
hazardOccurrences[file] = lists;
|
|
225
|
-
hazardOccurrenceCounts[file] = Object.fromEntries(Object.entries(lists).map(([label, identities]) => [label, identities.length]));
|
|
226
|
-
}
|
|
227
|
-
return { mappedScripts: normalizedScripts, assetFiles, inputHashes, hazardOccurrenceCounts, hazardOccurrences };
|
|
115
|
+
for (const file of assetFiles)
|
|
116
|
+
inputHashes[file] = sha256(await readFile(path.resolve(workspaceRoot, file)));
|
|
117
|
+
return { mappedScripts: normalizedScripts, assetFiles, inputHashes };
|
|
228
118
|
}
|
|
229
119
|
function bounded(value) {
|
|
230
120
|
const normalized = value.replaceAll(/(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+/gi, "$1 [REDACTED]");
|
|
@@ -248,10 +138,6 @@ function classifyFixtureResolutionFailure(output) {
|
|
|
248
138
|
const normalized = output.replaceAll("\\", "/");
|
|
249
139
|
const paths = testcasePythonPaths(normalized);
|
|
250
140
|
const generatedProviderPaths = paths.filter((item) => /testcase\/(?:helpers|factories)\//.test(item));
|
|
251
|
-
const generatedTestModulePaths = paths.filter((item) => /testcase\/(?:[A-Za-z0-9_./-]+\/)?test_[A-Za-z0-9_.-]+\.py$/i.test(item));
|
|
252
|
-
const onlyGeneratedTestcasePaths = paths.length > 0 && paths.every((item) => item.startsWith("testcase/"));
|
|
253
|
-
const mentionsProduction = /(?:apps|src|server)\/[A-Za-z0-9_./-]+\.py/i.test(normalized) ||
|
|
254
|
-
/No module named ['"](?!testcase(?:\.|['"]))/i.test(normalized);
|
|
255
141
|
if (/fixture ['"][^'"]+['"] not found/i.test(normalized) && generatedProviderPaths.length > 0) {
|
|
256
142
|
return {
|
|
257
143
|
status: "REPAIRABLE",
|
|
@@ -260,33 +146,6 @@ function classifyFixtureResolutionFailure(output) {
|
|
|
260
146
|
repairPaths: generatedProviderPaths,
|
|
261
147
|
};
|
|
262
148
|
}
|
|
263
|
-
// ScopeMismatch and other setup failures fully attributed to generated test modules are
|
|
264
|
-
// writer-local and may be fixed by one bounded repair (align scopes / define fixtures).
|
|
265
|
-
if (!mentionsProduction &&
|
|
266
|
-
onlyGeneratedTestcasePaths &&
|
|
267
|
-
generatedTestModulePaths.length > 0 &&
|
|
268
|
-
/ScopeMismatch|scope mismatch|function scoped fixture|module scoped request object|module scoped fixture|ERROR at setup of/i.test(normalized)) {
|
|
269
|
-
const isScope = /ScopeMismatch|scope mismatch|function scoped fixture|module scoped request object|module scoped fixture/i.test(normalized);
|
|
270
|
-
return {
|
|
271
|
-
status: "REPAIRABLE",
|
|
272
|
-
kind: isScope ? "generated-fixture-scope-mismatch" : "generated-fixture-setup-defect",
|
|
273
|
-
detail: isScope
|
|
274
|
-
? "generated pytest fixture scopes are inconsistent inside mapped test modules"
|
|
275
|
-
: "generated pytest fixture setup failed inside mapped test modules",
|
|
276
|
-
repairPaths: [...new Set([...generatedTestModulePaths, ...generatedProviderPaths])].sort(),
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
if (!mentionsProduction &&
|
|
280
|
-
onlyGeneratedTestcasePaths &&
|
|
281
|
-
generatedTestModulePaths.length > 0 &&
|
|
282
|
-
/fixture ['"][^'"]+['"] not found/i.test(normalized)) {
|
|
283
|
-
return {
|
|
284
|
-
status: "REPAIRABLE",
|
|
285
|
-
kind: "missing-generated-fixture",
|
|
286
|
-
detail: "mapped pytest module requests a fixture not defined in generated testcase assets",
|
|
287
|
-
repairPaths: [...new Set([...generatedTestModulePaths, ...generatedProviderPaths])].sort(),
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
149
|
return {
|
|
291
150
|
status: "BLOCKED",
|
|
292
151
|
kind: "unresolved-fixture-dependency",
|
|
@@ -340,8 +199,6 @@ export function assessBackendPytestCollection(input) {
|
|
|
340
199
|
missingMappedScripts: [],
|
|
341
200
|
assetFiles: input.inventory.assetFiles,
|
|
342
201
|
inputHashes: input.inventory.inputHashes,
|
|
343
|
-
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
344
|
-
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
345
202
|
pytestExitCode: input.exitCode,
|
|
346
203
|
collectedItemCount: items.length,
|
|
347
204
|
collectedItemIds: items,
|
|
@@ -370,8 +227,6 @@ export function assessBackendPytestCollection(input) {
|
|
|
370
227
|
missingMappedScripts: [],
|
|
371
228
|
assetFiles: input.inventory.assetFiles,
|
|
372
229
|
inputHashes: input.inventory.inputHashes,
|
|
373
|
-
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
374
|
-
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
375
230
|
pytestExitCode: input.exitCode,
|
|
376
231
|
collectedItemCount: items.length,
|
|
377
232
|
collectedItemIds: items,
|
|
@@ -404,8 +259,6 @@ export function assessBackendPytestCollection(input) {
|
|
|
404
259
|
missingMappedScripts: [],
|
|
405
260
|
assetFiles: input.inventory.assetFiles,
|
|
406
261
|
inputHashes: input.inventory.inputHashes,
|
|
407
|
-
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
408
|
-
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
409
262
|
pytestExitCode: input.exitCode,
|
|
410
263
|
collectedItemCount: items.length,
|
|
411
264
|
collectedItemIds: items,
|
|
@@ -441,8 +294,6 @@ export function assessPriorityOnlyBackendPytestModules(input) {
|
|
|
441
294
|
missingMappedScripts: [],
|
|
442
295
|
assetFiles: input.inventory.assetFiles,
|
|
443
296
|
inputHashes: input.inventory.inputHashes,
|
|
444
|
-
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
445
|
-
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
446
297
|
pytestExitCode: null,
|
|
447
298
|
collectedItemCount: 0,
|
|
448
299
|
collectedItemIds: [],
|
|
@@ -475,8 +326,6 @@ export function assessMissingBackendPytestScripts(input) {
|
|
|
475
326
|
missingMappedScripts: [...input.missingMappedScripts],
|
|
476
327
|
assetFiles: [...input.assetFiles],
|
|
477
328
|
inputHashes: input.inputHashes,
|
|
478
|
-
hazardOccurrenceCounts: input.hazardOccurrenceCounts ?? Object.fromEntries(input.assetFiles.map((file) => [file, {}])),
|
|
479
|
-
hazardOccurrences: input.hazardOccurrences ?? Object.fromEntries(input.assetFiles.map((file) => [file, {}])),
|
|
480
329
|
pytestExitCode: null,
|
|
481
330
|
collectedItemCount: 0,
|
|
482
331
|
collectedItemIds: [],
|
|
@@ -566,21 +415,17 @@ async function assertBackendPytestRepairSafety(workspaceRoot, initial, final) {
|
|
|
566
415
|
if (JSON.stringify(initial.mappedScripts) !== JSON.stringify(final.mappedScripts)) {
|
|
567
416
|
throw new Error("backend pytest repair changed mapped script scope");
|
|
568
417
|
}
|
|
418
|
+
const forbidden = [
|
|
419
|
+
[/pytest\.mark\.(?:skip|skipif|xfail)\b|pytest\.(?:skip|xfail)\s*\(/, "skip/xfail"],
|
|
420
|
+
[/sys\.path\s*\.|PYTHONPATH/, "sys.path/PYTHONPATH mutation"],
|
|
421
|
+
[/except\s+ImportError\b/, "ImportError fallback"],
|
|
422
|
+
[/except\s+(?:Exception|BaseException)\b[\s\S]{0,160}?\bpass\b/, "broad exception swallowing"],
|
|
423
|
+
];
|
|
569
424
|
for (const file of final.assetFiles) {
|
|
570
425
|
const source = await readFile(path.resolve(workspaceRoot, file), "utf8");
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
if (initial.inputHashes[file] === final.inputHashes[file])
|
|
576
|
-
continue;
|
|
577
|
-
const finalLists = scanRepairHazardOccurrenceLists(source);
|
|
578
|
-
for (const rule of REPAIR_HAZARD_PATTERNS) {
|
|
579
|
-
const baseline = initial.hazardOccurrences?.[file]?.[rule.label] ?? [];
|
|
580
|
-
const finalIdentities = finalLists[rule.label] ?? [];
|
|
581
|
-
if (hasMultisetIncrement(baseline, finalIdentities)) {
|
|
582
|
-
throw new Error(`backend pytest repair safety blocked ${rule.label}: ${file}`);
|
|
583
|
-
}
|
|
426
|
+
for (const [pattern, label] of forbidden) {
|
|
427
|
+
if (pattern.test(source))
|
|
428
|
+
throw new Error(`backend pytest repair safety blocked ${label}: ${file}`);
|
|
584
429
|
}
|
|
585
430
|
}
|
|
586
431
|
}
|
|
@@ -594,65 +439,23 @@ function assertSameInventory(expected, actual) {
|
|
|
594
439
|
throw new Error(`backend pytest collection hash drift: ${file}`);
|
|
595
440
|
}
|
|
596
441
|
}
|
|
597
|
-
export function buildBackendTestItemEligibility(collectedItemIds, input) {
|
|
598
|
-
const eligibleItemIds = [];
|
|
599
|
-
const excludedItems = [];
|
|
600
|
-
for (const itemId of collectedItemIds) {
|
|
601
|
-
const segments = itemId.split("::");
|
|
602
|
-
const symbolSegment = [...segments].reverse().find((segment) => /^test_/.test(segment));
|
|
603
|
-
const symbol = symbolSegment?.replace(/\[.*$/, "");
|
|
604
|
-
const parameterTokens = orderedUnique((itemId.match(/TP-[A-Z0-9-]+/g) ?? []));
|
|
605
|
-
const mapping = input.correspondenceEntries.find((entry) => symbol && (entry.declaredPrimarySymbol === symbol || entry.pytestSymbols.includes(symbol)));
|
|
606
|
-
const reasons = [];
|
|
607
|
-
if (!mapping)
|
|
608
|
-
reasons.push("no exact Markdown Case/primary-symbol correspondence");
|
|
609
|
-
if (mapping && mapping.status !== "EXACT_1_TO_1")
|
|
610
|
-
reasons.push(`correspondence status is ${mapping.status}`);
|
|
611
|
-
if (mapping && mapping.payloadAssessment.status !== "SAFE")
|
|
612
|
-
reasons.push(`payload contract status is ${mapping.payloadAssessment.status}`);
|
|
613
|
-
const scenarioEntries = mapping?.caseId ? input.scenarioEntries.filter((entry) => entry.caseId === mapping.caseId) : [];
|
|
614
|
-
const relevantScenarioEntries = parameterTokens.length > 0
|
|
615
|
-
? scenarioEntries.filter((entry) => parameterTokens.includes(entry.tpId))
|
|
616
|
-
: scenarioEntries;
|
|
617
|
-
const payloadPaths = orderedUnique([
|
|
618
|
-
...(mapping?.payloadAssessment.requiredPaths ?? []),
|
|
619
|
-
...(mapping?.payloadAssessment.allowedPaths ?? []),
|
|
620
|
-
...(mapping?.payloadAssessment.observedPaths ?? []),
|
|
621
|
-
]).map((item) => item.toLowerCase());
|
|
622
|
-
for (const entry of relevantScenarioEntries) {
|
|
623
|
-
// Field names heuristically inferred only from a TP token (for example
|
|
624
|
-
// TP-HEALTH-HTTP-200 -> hEALTH) are not execution-safety evidence. Exclude
|
|
625
|
-
// only when the field is also present in the deterministic payload contract
|
|
626
|
-
// or observed Python payload shape.
|
|
627
|
-
const normalizedField = entry.field?.toLowerCase();
|
|
628
|
-
const fieldIsPayloadBound = Boolean(normalizedField && payloadPaths.some((item) => item === normalizedField || item.endsWith(`.${normalizedField}`)));
|
|
629
|
-
if (fieldIsPayloadBound && entry.status !== "MATCH")
|
|
630
|
-
reasons.push(`scenario-param ${entry.tpId} field ${entry.field} is ${entry.status}`);
|
|
631
|
-
}
|
|
632
|
-
if (reasons.length === 0)
|
|
633
|
-
eligibleItemIds.push(itemId);
|
|
634
|
-
else
|
|
635
|
-
excludedItems.push({ itemId, ...(mapping?.caseId ? { caseId: mapping.caseId } : {}), ...(symbol ? { symbol } : {}), reasons: orderedUnique(reasons) });
|
|
636
|
-
}
|
|
637
|
-
return { eligibleItemIds, excludedItems };
|
|
638
|
-
}
|
|
639
|
-
function orderedUnique(values) {
|
|
640
|
-
return [...new Set(values)];
|
|
641
|
-
}
|
|
642
442
|
export async function materializeBackendTestExecutionReadiness(input) {
|
|
643
443
|
if (input.effective.phase !== "effective" || input.effective.status !== "PASS" || input.effective.fixtureResolutionStatus !== "PASS") {
|
|
644
444
|
throw new Error("backend-test execution readiness requires effective collection and fixture-resolution PASS");
|
|
645
445
|
}
|
|
646
446
|
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.effective.mappedScripts);
|
|
447
|
+
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
448
|
+
if (priorityOnly.length > 0) {
|
|
449
|
+
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
450
|
+
}
|
|
647
451
|
assertSameInventory(input.effective, current);
|
|
648
|
-
const
|
|
649
|
-
const status = eligibility.eligibleItemIds.length === 0
|
|
452
|
+
const status = input.scenarioParamStatus === "FAIL"
|
|
650
453
|
? "BLOCKED"
|
|
651
|
-
: input.scenarioParamStatus === "PASS"
|
|
454
|
+
: input.scenarioParamStatus === "PASS"
|
|
652
455
|
? "PASS"
|
|
653
456
|
: "PARTIAL";
|
|
654
457
|
const readiness = backendTestExecutionReadinessSchema.parse({
|
|
655
|
-
schemaId: "backend-test-execution-readiness-
|
|
458
|
+
schemaId: "backend-test-execution-readiness-v1",
|
|
656
459
|
status,
|
|
657
460
|
collectionStatus: "PASS",
|
|
658
461
|
fixtureResolutionStatus: "PASS",
|
|
@@ -663,11 +466,8 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
663
466
|
},
|
|
664
467
|
mappedScripts: input.effective.mappedScripts,
|
|
665
468
|
collectedItemIds: input.effective.collectedItemIds,
|
|
666
|
-
eligibleItemIds: eligibility.eligibleItemIds,
|
|
667
|
-
excludedItems: eligibility.excludedItems,
|
|
668
469
|
fixtureIssues: input.effective.findings.filter((item) => /fixture/i.test(item.kind)).map((item) => item.detail),
|
|
669
470
|
assetHashes: input.effective.inputHashes,
|
|
670
|
-
eligibilityInputHashes: input.eligibilityInputHashes ?? {},
|
|
671
471
|
});
|
|
672
472
|
const contractsDir = path.join(input.runDir, "contracts");
|
|
673
473
|
await mkdir(contractsDir, { recursive: true });
|
|
@@ -677,145 +477,27 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
677
477
|
export async function readBackendTestExecutionReadiness(filePath) {
|
|
678
478
|
return backendTestExecutionReadinessSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
679
479
|
}
|
|
680
|
-
function formatReadinessCounts(readiness) {
|
|
681
|
-
return `mapped=${readiness.mappedScripts.length} collected=${readiness.collectedItemIds.length} eligible=${readiness.eligibleItemIds.length} excluded=${readiness.excludedItems.length}`;
|
|
682
|
-
}
|
|
683
|
-
export function classifyExclusionReason(reason) {
|
|
684
|
-
if (reason === "no exact Markdown Case/primary-symbol correspondence")
|
|
685
|
-
return "correspondence-missing";
|
|
686
|
-
if (/^correspondence status is\b/.test(reason))
|
|
687
|
-
return "correspondence-status";
|
|
688
|
-
if (/^payload contract status is\b/.test(reason))
|
|
689
|
-
return "payload-contract-status";
|
|
690
|
-
if (/^scenario-param\b.*\bfield\b/.test(reason))
|
|
691
|
-
return "scenario-param-field";
|
|
692
|
-
return "other";
|
|
693
|
-
}
|
|
694
|
-
/** Fixed-category tally for readiness Error messages — never echoes raw reason text. */
|
|
695
|
-
function tallyExclusionReasonCategories(excludedItems) {
|
|
696
|
-
const counts = new Map();
|
|
697
|
-
for (const item of excludedItems) {
|
|
698
|
-
for (const reason of item.reasons) {
|
|
699
|
-
const category = classifyExclusionReason(reason);
|
|
700
|
-
counts.set(category, (counts.get(category) ?? 0) + 1);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
const parts = READINESS_EXCLUSION_CATEGORIES
|
|
704
|
-
.filter((category) => (counts.get(category) ?? 0) > 0)
|
|
705
|
-
.map((category) => `${category}=${counts.get(category)}`);
|
|
706
|
-
return parts.length > 0 ? parts.join("; ") : "none";
|
|
707
|
-
}
|
|
708
|
-
const READINESS_ERROR_TRUNCATION_SUFFIX = "...[truncated]";
|
|
709
|
-
export function boundReadinessErrorMessage(message) {
|
|
710
|
-
if (message.length <= MAX_READINESS_ERROR_CHARS)
|
|
711
|
-
return message;
|
|
712
|
-
// Truncate to leave room for the suffix so the final message never exceeds
|
|
713
|
-
// MAX_READINESS_ERROR_CHARS. Never append beyond the declared limit.
|
|
714
|
-
const limit = MAX_READINESS_ERROR_CHARS - READINESS_ERROR_TRUNCATION_SUFFIX.length;
|
|
715
|
-
return `${message.slice(0, Math.max(0, limit))}${READINESS_ERROR_TRUNCATION_SUFFIX}`;
|
|
716
|
-
}
|
|
717
|
-
function throwReadinessError(message) {
|
|
718
|
-
throw new Error(boundReadinessErrorMessage(message));
|
|
719
|
-
}
|
|
720
|
-
function firstInventoryDeltaPath(expectedHashes, actualHashes) {
|
|
721
|
-
const expectedFiles = Object.keys(expectedHashes).sort();
|
|
722
|
-
const actualFiles = Object.keys(actualHashes).sort();
|
|
723
|
-
const actualSet = new Set(actualFiles);
|
|
724
|
-
const expectedSet = new Set(expectedFiles);
|
|
725
|
-
const onlyExpected = expectedFiles.find((file) => !actualSet.has(file));
|
|
726
|
-
if (onlyExpected)
|
|
727
|
-
return onlyExpected;
|
|
728
|
-
return actualFiles.find((file) => !expectedSet.has(file));
|
|
729
|
-
}
|
|
730
|
-
async function firstUnreadableAssetPath(workspaceRoot, readiness) {
|
|
731
|
-
const candidatePaths = [...new Set([
|
|
732
|
-
...Object.keys(readiness.assetHashes),
|
|
733
|
-
...readiness.mappedScripts,
|
|
734
|
-
])].sort();
|
|
735
|
-
for (const relativePath of candidatePaths) {
|
|
736
|
-
try {
|
|
737
|
-
await readFile(path.resolve(workspaceRoot, relativePath));
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
return relativePath;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
return undefined;
|
|
744
|
-
}
|
|
745
480
|
export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness) {
|
|
746
481
|
if (!["PASS", "PARTIAL"].includes(readiness.status)) {
|
|
747
|
-
|
|
748
|
-
const reasonTally = readiness.eligibleItemIds.length === 0 && readiness.excludedItems.length > 0
|
|
749
|
-
? `; exclusionReasons: ${tallyExclusionReasonCategories(readiness.excludedItems)}`
|
|
750
|
-
: "";
|
|
751
|
-
throwReadinessError(`backend pytest execution readiness is ${readiness.status}: ${counts}${reasonTally}`);
|
|
752
|
-
}
|
|
753
|
-
if (readiness.eligibleItemIds.length === 0) {
|
|
754
|
-
throwReadinessError(`backend pytest execution readiness authorizes zero eligible items: ${tallyExclusionReasonCategories(readiness.excludedItems)}`);
|
|
755
|
-
}
|
|
756
|
-
const missingEligibleCount = readiness.eligibleItemIds.filter((itemId) => !readiness.collectedItemIds.includes(itemId)).length;
|
|
757
|
-
if (missingEligibleCount > 0) {
|
|
758
|
-
// Do not replay raw pytest item IDs: they may contain TP-*, field, payload,
|
|
759
|
-
// secret-like or parameter markers. Report only the fixed category + count.
|
|
760
|
-
throwReadinessError(`backend pytest readiness has ${missingEligibleCount} eligible item(s) absent from collected items: ${formatReadinessCounts(readiness)}`);
|
|
761
|
-
}
|
|
762
|
-
const unreadableAsset = await firstUnreadableAssetPath(workspaceRoot, readiness);
|
|
763
|
-
if (unreadableAsset) {
|
|
764
|
-
throwReadinessError(`backend pytest collection asset inventory drift: firstPath=${unreadableAsset}`);
|
|
765
|
-
}
|
|
766
|
-
let current;
|
|
767
|
-
try {
|
|
768
|
-
current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts);
|
|
769
|
-
}
|
|
770
|
-
catch {
|
|
771
|
-
const fallbackPath = [...readiness.mappedScripts].sort()[0] ?? Object.keys(readiness.assetHashes).sort()[0] ?? "unknown";
|
|
772
|
-
throwReadinessError(`backend pytest collection asset inventory drift: firstPath=${fallbackPath}`);
|
|
482
|
+
throw new Error(`backend pytest execution readiness is ${readiness.status}`);
|
|
773
483
|
}
|
|
484
|
+
const current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts);
|
|
774
485
|
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
775
486
|
if (priorityOnly.length > 0) {
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
const expectedHashes = readiness.assetHashes;
|
|
779
|
-
const inventoryDelta = firstInventoryDeltaPath(expectedHashes, current.inputHashes);
|
|
780
|
-
if (inventoryDelta) {
|
|
781
|
-
throwReadinessError(`backend pytest collection asset inventory drift: firstPath=${inventoryDelta}`);
|
|
782
|
-
}
|
|
783
|
-
for (const file of Object.keys(expectedHashes).sort()) {
|
|
784
|
-
if (expectedHashes[file] !== current.inputHashes[file]) {
|
|
785
|
-
throwReadinessError(`backend pytest collection hash drift: ${file}`);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
for (const relativePath of Object.keys(readiness.eligibilityInputHashes).sort()) {
|
|
789
|
-
const expectedHash = readiness.eligibilityInputHashes[relativePath];
|
|
790
|
-
const absolute = path.resolve(workspaceRoot, relativePath);
|
|
791
|
-
let actualHash;
|
|
792
|
-
try {
|
|
793
|
-
actualHash = sha256(await readFile(absolute));
|
|
794
|
-
}
|
|
795
|
-
catch {
|
|
796
|
-
throwReadinessError(`backend pytest eligibility input hash drift: firstPath=${relativePath}`);
|
|
797
|
-
}
|
|
798
|
-
if (actualHash !== expectedHash) {
|
|
799
|
-
throwReadinessError(`backend pytest eligibility input hash drift: ${relativePath}`);
|
|
800
|
-
}
|
|
487
|
+
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
801
488
|
}
|
|
489
|
+
assertSameInventory({ mappedScripts: readiness.mappedScripts, assetFiles: Object.keys(readiness.assetHashes), inputHashes: readiness.assetHashes }, current);
|
|
802
490
|
}
|
|
803
491
|
export async function assertBackendPytestCollectionFresh(workspaceRoot, effective) {
|
|
804
492
|
if (effective.phase !== "effective" || effective.status !== "PASS") {
|
|
805
493
|
throw new Error("backend pytest execution requires effective collection PASS facts");
|
|
806
494
|
}
|
|
807
495
|
const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts);
|
|
808
|
-
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
809
|
-
if (priorityOnly.length > 0)
|
|
810
|
-
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
811
496
|
assertSameInventory(effective, current);
|
|
812
497
|
}
|
|
813
498
|
export async function materializeEffectiveBackendPytestCollection(input) {
|
|
814
499
|
if (input.initial.status === "PASS") {
|
|
815
500
|
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts);
|
|
816
|
-
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
817
|
-
if (priorityOnly.length > 0)
|
|
818
|
-
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
819
501
|
assertSameInventory(input.initial, current);
|
|
820
502
|
return backendPytestCollectionFactsSchema.parse({
|
|
821
503
|
...input.initial,
|
|
@@ -831,9 +513,6 @@ export async function materializeEffectiveBackendPytestCollection(input) {
|
|
|
831
513
|
throw new Error("backend pytest collection still fails after bounded repair");
|
|
832
514
|
await assertBackendPytestRepairSafety(input.workspaceRoot, input.initial, input.final);
|
|
833
515
|
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts);
|
|
834
|
-
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
835
|
-
if (priorityOnly.length > 0)
|
|
836
|
-
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
837
516
|
assertSameInventory(input.final, current);
|
|
838
517
|
return backendPytestCollectionFactsSchema.parse({
|
|
839
518
|
...input.final,
|