@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -23
- package/dist/application/task-lifecycle/advance.js +4 -254
- package/dist/application/task-lifecycle/gates.js +0 -50
- package/dist/application/task-lifecycle/observe.js +2 -11
- package/dist/commands/init-upgrade.js +1 -32
- package/dist/commands/init.js +3 -94
- package/dist/executors/shell-executor.js +91 -4
- package/dist/executors/shell-write-guard.js +8 -26
- package/dist/shared/operator/capabilities.js +42 -72
- package/dist/task/source-prepare/index.js +0 -2
- package/dist/task/source-prepare/parse-intent.js +10 -58
- package/dist/task/source-prepare/prepare.js +16 -180
- package/dist/task/source-prepare/reference-integrity.js +2 -18
- package/dist/worker/console/app-data.js +0 -2
- package/dist/worker/console/chat/chat-event-store.js +25 -190
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/pi-console-config.js +32 -250
- package/dist/worker/console/chat/pi-runtime.js +71 -625
- package/dist/worker/console/chat/resource-loader.js +4 -5
- package/dist/worker/console/chat/routes.js +146 -324
- package/dist/worker/console/chat/runtime-context.js +12 -48
- package/dist/worker/console/chat/runtime-selection.js +0 -59
- package/dist/worker/console/chat/shortcuts.js +0 -1
- package/dist/worker/console/chat/tool-adapter.js +3 -9
- package/dist/worker/console/chat/tools.js +1 -5
- package/dist/worker/console/operator-actions.js +68 -559
- package/dist/worker/console/server.js +15 -8
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
- package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
- package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
- package/dist/worker/delivery/final-verification.js +5 -13
- package/dist/worker/delivery/package.js +19 -31
- package/dist/worker/delivery/verification-bundle.js +4 -6
- package/dist/worker/observe/static/operator-chrome.css +2 -5
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/styles.css +9 -39
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +269 -82
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
- package/dist/workflows/dag/init-hybrid.js +46 -34
- package/dist/workflows/dag/types.js +7 -0
- package/dist/workflows/dag/workspace-checkpoint.js +27 -8
- package/docs/templates/backend-test-dag.json +32 -29
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -3
- package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
- package/skills/loop-agent/references/task-workflow.md +0 -4
- package/dist/shared/resilient-git.js +0 -133
- package/dist/task/source-prepare/artifact-meta.js +0 -137
- package/dist/task/source-prepare/semantic-intake.js +0 -404
- package/dist/worker/console/dag-execution-receipt.js +0 -380
- package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
- package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +0 -196
|
@@ -149,6 +149,10 @@ 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),
|
|
152
156
|
}).strict().optional(),
|
|
153
157
|
artifactRefs: z.object({
|
|
154
158
|
caseCoverageFacts: z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/).optional() }).strict(),
|
|
@@ -471,7 +471,10 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
471
471
|
}
|
|
472
472
|
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
473
473
|
const mappedScripts = extractMappedPytestScripts(testCase.body);
|
|
474
|
-
if (
|
|
474
|
+
if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
|
|
475
|
+
// Evidence-only meta cases intentionally declare 脚本/primary symbol as 无 and keep only assertion/cross-cutting TPs.
|
|
476
|
+
}
|
|
477
|
+
else if (mappedScripts.length === 0) {
|
|
475
478
|
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
|
|
476
479
|
}
|
|
477
480
|
else {
|
|
@@ -560,6 +563,24 @@ function extractMappedPytestScripts(testCaseBody) {
|
|
|
560
563
|
.map((match) => match[1].replaceAll("\\", "/"))
|
|
561
564
|
.filter((value) => !value.includes("..")));
|
|
562
565
|
}
|
|
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
|
+
}
|
|
563
584
|
function isSafeBackendPytestScript(script, workspaceRoot) {
|
|
564
585
|
const absolute = path.resolve(workspaceRoot, script);
|
|
565
586
|
const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
|
|
@@ -621,6 +642,9 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
621
642
|
for (const testCase of splitCases(content)) {
|
|
622
643
|
markdownIds.add(testCase.id);
|
|
623
644
|
const scripts = extractMappedPytestScripts(testCase.body);
|
|
645
|
+
if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
624
648
|
if (scripts.length === 0) {
|
|
625
649
|
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射`);
|
|
626
650
|
continue;
|
|
@@ -1,5 +1,7 @@
|
|
|
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;
|
|
3
5
|
export function normalizeBackendTestModuleStemCandidate(raw) {
|
|
4
6
|
return path.basename(raw)
|
|
5
7
|
.replace(/\.(?:md|py)$/i, "")
|
|
@@ -11,6 +13,9 @@ export function normalizeBackendTestModuleStemCandidate(raw) {
|
|
|
11
13
|
export function isPriorityOnlyBackendTestModuleStem(raw) {
|
|
12
14
|
return PRIORITY_ONLY_MODULE_STEMS.has(normalizeBackendTestModuleStemCandidate(raw));
|
|
13
15
|
}
|
|
16
|
+
export function isOpaqueHashBackendTestModuleStem(raw) {
|
|
17
|
+
return OPAQUE_HASH_MODULE_STEM.test(normalizeBackendTestModuleStemCandidate(raw));
|
|
18
|
+
}
|
|
14
19
|
export function isPriorityOnlyBackendPytestScript(script) {
|
|
15
20
|
const basename = path.posix.basename(script.replaceAll("\\", "/"));
|
|
16
21
|
const match = /^test_(.+)\.py$/i.exec(basename);
|
|
@@ -5,6 +5,15 @@ 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
|
+
];
|
|
8
17
|
export const backendPytestCollectionFindingSchema = z.object({
|
|
9
18
|
kind: z.string().min(1),
|
|
10
19
|
classification: z.literal("test-asset-defect"),
|
|
@@ -27,6 +36,8 @@ export const backendPytestCollectionFactsSchema = z.object({
|
|
|
27
36
|
missingMappedScripts: z.array(z.string()),
|
|
28
37
|
assetFiles: z.array(z.string()),
|
|
29
38
|
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(),
|
|
30
41
|
pytestExitCode: z.number().int().nullable(),
|
|
31
42
|
collectedItemCount: z.number().int().min(0),
|
|
32
43
|
collectedItemIds: z.array(z.string()),
|
|
@@ -49,8 +60,14 @@ export const backendPytestCollectionFactsSchema = z.object({
|
|
|
49
60
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "backend pytest fixture resolution without an attempt cannot have an exit code" });
|
|
50
61
|
}
|
|
51
62
|
});
|
|
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();
|
|
52
69
|
export const backendTestExecutionReadinessSchema = z.object({
|
|
53
|
-
schemaId: z.literal("backend-test-execution-readiness-
|
|
70
|
+
schemaId: z.literal("backend-test-execution-readiness-v2"),
|
|
54
71
|
status: z.enum(["PASS", "PARTIAL", "BLOCKED"]),
|
|
55
72
|
collectionStatus: z.literal("PASS"),
|
|
56
73
|
fixtureResolutionStatus: z.literal("PASS"),
|
|
@@ -61,9 +78,94 @@ export const backendTestExecutionReadinessSchema = z.object({
|
|
|
61
78
|
}).strict(),
|
|
62
79
|
mappedScripts: z.array(z.string()).min(1),
|
|
63
80
|
collectedItemIds: z.array(z.string()),
|
|
81
|
+
eligibleItemIds: z.array(z.string()),
|
|
82
|
+
excludedItems: z.array(backendTestExcludedItemSchema),
|
|
64
83
|
fixtureIssues: z.array(z.string()),
|
|
65
84
|
assetHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
85
|
+
eligibilityInputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
66
86
|
}).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
|
+
}
|
|
67
169
|
function repoRef(workspaceRoot, absolutePath) {
|
|
68
170
|
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, "/");
|
|
69
171
|
}
|
|
@@ -112,9 +214,17 @@ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScri
|
|
|
112
214
|
const pythonFiles = await walkPythonFiles(path.resolve(workspaceRoot, "testcase"));
|
|
113
215
|
const assetFiles = [...new Set(pythonFiles.map((file) => repoRef(workspaceRoot, file)))].sort();
|
|
114
216
|
const inputHashes = {};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
217
|
+
const hazardOccurrenceCounts = {};
|
|
218
|
+
const hazardOccurrences = {};
|
|
219
|
+
for (const file of assetFiles) {
|
|
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 };
|
|
118
228
|
}
|
|
119
229
|
function bounded(value) {
|
|
120
230
|
const normalized = value.replaceAll(/(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+/gi, "$1 [REDACTED]");
|
|
@@ -138,6 +248,10 @@ function classifyFixtureResolutionFailure(output) {
|
|
|
138
248
|
const normalized = output.replaceAll("\\", "/");
|
|
139
249
|
const paths = testcasePythonPaths(normalized);
|
|
140
250
|
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);
|
|
141
255
|
if (/fixture ['"][^'"]+['"] not found/i.test(normalized) && generatedProviderPaths.length > 0) {
|
|
142
256
|
return {
|
|
143
257
|
status: "REPAIRABLE",
|
|
@@ -146,6 +260,33 @@ function classifyFixtureResolutionFailure(output) {
|
|
|
146
260
|
repairPaths: generatedProviderPaths,
|
|
147
261
|
};
|
|
148
262
|
}
|
|
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
|
+
}
|
|
149
290
|
return {
|
|
150
291
|
status: "BLOCKED",
|
|
151
292
|
kind: "unresolved-fixture-dependency",
|
|
@@ -199,6 +340,8 @@ export function assessBackendPytestCollection(input) {
|
|
|
199
340
|
missingMappedScripts: [],
|
|
200
341
|
assetFiles: input.inventory.assetFiles,
|
|
201
342
|
inputHashes: input.inventory.inputHashes,
|
|
343
|
+
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
344
|
+
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
202
345
|
pytestExitCode: input.exitCode,
|
|
203
346
|
collectedItemCount: items.length,
|
|
204
347
|
collectedItemIds: items,
|
|
@@ -227,6 +370,8 @@ export function assessBackendPytestCollection(input) {
|
|
|
227
370
|
missingMappedScripts: [],
|
|
228
371
|
assetFiles: input.inventory.assetFiles,
|
|
229
372
|
inputHashes: input.inventory.inputHashes,
|
|
373
|
+
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
374
|
+
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
230
375
|
pytestExitCode: input.exitCode,
|
|
231
376
|
collectedItemCount: items.length,
|
|
232
377
|
collectedItemIds: items,
|
|
@@ -259,6 +404,8 @@ export function assessBackendPytestCollection(input) {
|
|
|
259
404
|
missingMappedScripts: [],
|
|
260
405
|
assetFiles: input.inventory.assetFiles,
|
|
261
406
|
inputHashes: input.inventory.inputHashes,
|
|
407
|
+
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
408
|
+
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
262
409
|
pytestExitCode: input.exitCode,
|
|
263
410
|
collectedItemCount: items.length,
|
|
264
411
|
collectedItemIds: items,
|
|
@@ -294,6 +441,8 @@ export function assessPriorityOnlyBackendPytestModules(input) {
|
|
|
294
441
|
missingMappedScripts: [],
|
|
295
442
|
assetFiles: input.inventory.assetFiles,
|
|
296
443
|
inputHashes: input.inventory.inputHashes,
|
|
444
|
+
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
445
|
+
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
297
446
|
pytestExitCode: null,
|
|
298
447
|
collectedItemCount: 0,
|
|
299
448
|
collectedItemIds: [],
|
|
@@ -326,6 +475,8 @@ export function assessMissingBackendPytestScripts(input) {
|
|
|
326
475
|
missingMappedScripts: [...input.missingMappedScripts],
|
|
327
476
|
assetFiles: [...input.assetFiles],
|
|
328
477
|
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, {}])),
|
|
329
480
|
pytestExitCode: null,
|
|
330
481
|
collectedItemCount: 0,
|
|
331
482
|
collectedItemIds: [],
|
|
@@ -415,17 +566,21 @@ async function assertBackendPytestRepairSafety(workspaceRoot, initial, final) {
|
|
|
415
566
|
if (JSON.stringify(initial.mappedScripts) !== JSON.stringify(final.mappedScripts)) {
|
|
416
567
|
throw new Error("backend pytest repair changed mapped script scope");
|
|
417
568
|
}
|
|
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
|
-
];
|
|
424
569
|
for (const file of final.assetFiles) {
|
|
425
570
|
const source = await readFile(path.resolve(workspaceRoot, file), "utf8");
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
571
|
+
const liveHash = sha256(source);
|
|
572
|
+
if (final.inputHashes[file] && final.inputHashes[file] !== liveHash) {
|
|
573
|
+
throw new Error(`backend pytest collection hash drift: ${file}`);
|
|
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
|
+
}
|
|
429
584
|
}
|
|
430
585
|
}
|
|
431
586
|
}
|
|
@@ -439,23 +594,65 @@ function assertSameInventory(expected, actual) {
|
|
|
439
594
|
throw new Error(`backend pytest collection hash drift: ${file}`);
|
|
440
595
|
}
|
|
441
596
|
}
|
|
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
|
+
}
|
|
442
642
|
export async function materializeBackendTestExecutionReadiness(input) {
|
|
443
643
|
if (input.effective.phase !== "effective" || input.effective.status !== "PASS" || input.effective.fixtureResolutionStatus !== "PASS") {
|
|
444
644
|
throw new Error("backend-test execution readiness requires effective collection and fixture-resolution PASS");
|
|
445
645
|
}
|
|
446
646
|
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
|
-
}
|
|
451
647
|
assertSameInventory(input.effective, current);
|
|
452
|
-
const
|
|
648
|
+
const eligibility = input.eligibility ?? { eligibleItemIds: input.effective.collectedItemIds, excludedItems: [] };
|
|
649
|
+
const status = eligibility.eligibleItemIds.length === 0
|
|
453
650
|
? "BLOCKED"
|
|
454
|
-
: input.scenarioParamStatus === "PASS"
|
|
651
|
+
: input.scenarioParamStatus === "PASS" && eligibility.excludedItems.length === 0
|
|
455
652
|
? "PASS"
|
|
456
653
|
: "PARTIAL";
|
|
457
654
|
const readiness = backendTestExecutionReadinessSchema.parse({
|
|
458
|
-
schemaId: "backend-test-execution-readiness-
|
|
655
|
+
schemaId: "backend-test-execution-readiness-v2",
|
|
459
656
|
status,
|
|
460
657
|
collectionStatus: "PASS",
|
|
461
658
|
fixtureResolutionStatus: "PASS",
|
|
@@ -466,8 +663,11 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
466
663
|
},
|
|
467
664
|
mappedScripts: input.effective.mappedScripts,
|
|
468
665
|
collectedItemIds: input.effective.collectedItemIds,
|
|
666
|
+
eligibleItemIds: eligibility.eligibleItemIds,
|
|
667
|
+
excludedItems: eligibility.excludedItems,
|
|
469
668
|
fixtureIssues: input.effective.findings.filter((item) => /fixture/i.test(item.kind)).map((item) => item.detail),
|
|
470
669
|
assetHashes: input.effective.inputHashes,
|
|
670
|
+
eligibilityInputHashes: input.eligibilityInputHashes ?? {},
|
|
471
671
|
});
|
|
472
672
|
const contractsDir = path.join(input.runDir, "contracts");
|
|
473
673
|
await mkdir(contractsDir, { recursive: true });
|
|
@@ -477,27 +677,145 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
477
677
|
export async function readBackendTestExecutionReadiness(filePath) {
|
|
478
678
|
return backendTestExecutionReadinessSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
479
679
|
}
|
|
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
|
+
}
|
|
480
745
|
export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness) {
|
|
481
746
|
if (!["PASS", "PARTIAL"].includes(readiness.status)) {
|
|
482
|
-
|
|
747
|
+
const counts = formatReadinessCounts(readiness);
|
|
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}`);
|
|
483
773
|
}
|
|
484
|
-
const current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts);
|
|
485
774
|
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
486
775
|
if (priorityOnly.length > 0) {
|
|
487
|
-
|
|
776
|
+
throwReadinessError(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
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
|
+
}
|
|
488
801
|
}
|
|
489
|
-
assertSameInventory({ mappedScripts: readiness.mappedScripts, assetFiles: Object.keys(readiness.assetHashes), inputHashes: readiness.assetHashes }, current);
|
|
490
802
|
}
|
|
491
803
|
export async function assertBackendPytestCollectionFresh(workspaceRoot, effective) {
|
|
492
804
|
if (effective.phase !== "effective" || effective.status !== "PASS") {
|
|
493
805
|
throw new Error("backend pytest execution requires effective collection PASS facts");
|
|
494
806
|
}
|
|
495
807
|
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(", ")}`);
|
|
496
811
|
assertSameInventory(effective, current);
|
|
497
812
|
}
|
|
498
813
|
export async function materializeEffectiveBackendPytestCollection(input) {
|
|
499
814
|
if (input.initial.status === "PASS") {
|
|
500
815
|
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(", ")}`);
|
|
501
819
|
assertSameInventory(input.initial, current);
|
|
502
820
|
return backendPytestCollectionFactsSchema.parse({
|
|
503
821
|
...input.initial,
|
|
@@ -513,6 +831,9 @@ export async function materializeEffectiveBackendPytestCollection(input) {
|
|
|
513
831
|
throw new Error("backend pytest collection still fails after bounded repair");
|
|
514
832
|
await assertBackendPytestRepairSafety(input.workspaceRoot, input.initial, input.final);
|
|
515
833
|
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(", ")}`);
|
|
516
837
|
assertSameInventory(input.final, current);
|
|
517
838
|
return backendPytestCollectionFactsSchema.parse({
|
|
518
839
|
...input.final,
|