@tea-agent/loop-agent 0.38.0 → 0.38.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init-upgrade.js +97 -19
- package/dist/executors/dag-pi-executor.js +42 -4
- package/dist/executors/shell-executor.js +269 -15
- package/dist/infrastructure/harness/atomic-write.js +2 -1
- package/dist/shared/openspec-spec.js +76 -0
- package/dist/shared/operator/capabilities.js +149 -27
- package/dist/task/config-types.js +43 -0
- package/dist/task/frontend-project-capability.js +292 -38
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +477 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +137 -0
- package/dist/worker/console/operator-actions.js +128 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +8 -1
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/security.js +20 -1
- package/dist/worker/console/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
- package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +39 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-routing.js +9 -4
- package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +479 -28
- package/dist/workflows/dag/init-hybrid.js +920 -110
- package/dist/workflows/dag/lifecycle.js +4 -0
- package/dist/workflows/dag/node-execution.js +77 -8
- package/dist/workflows/dag/prompt.js +45 -0
- package/dist/workflows/dag/report.js +6 -0
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +93 -27
- package/dist/workflows/dag/rerun-run.js +3 -0
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +33 -0
- package/dist/workflows/dag/types.js +148 -4
- package/dist/workflows/dag/validate.js +7 -2
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-design-contract.md +40 -14
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- package/docs/templates/frontend-task-constraints.md +40 -13
- package/docs/templates/frontend-task-requirement.md +54 -20
- package/harness.json +3 -1
- package/package.json +1 -1
- package/skills/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +15 -11
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
|
@@ -6,6 +6,7 @@ import { createWriteStream } from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
|
|
9
|
+
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
9
10
|
import { parseJacocoXml, } from "./backend-test-coverage-contract.js";
|
|
10
11
|
import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
@@ -14,10 +15,22 @@ const execFileAsync = promisify(execFile);
|
|
|
14
15
|
// heading width cannot collapse the whole manifest/report to zero cases.
|
|
15
16
|
const CASE_ID = /\bBE-[A-Z0-9_-]+-\d{2,3}\b/g;
|
|
16
17
|
const CANONICAL_CASE_ID = /\bBE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}\b/;
|
|
18
|
+
const SHARED_SETUP_REF_LINE = /^\s*引用前置\s*:\s*(\S+)\s*#\s*([A-Za-z0-9_.\-\u4e00-\u9fa5]+)\s*$/;
|
|
19
|
+
const sharedSetupAnchorRe = /^SS-[A-Z0-9][A-Z0-9._-]*$/i;
|
|
20
|
+
export function extractBackendSharedSetupRefs(markdown) {
|
|
21
|
+
const refs = [];
|
|
22
|
+
for (const line of markdown.split("\n")) {
|
|
23
|
+
const match = SHARED_SETUP_REF_LINE.exec(line);
|
|
24
|
+
if (match) {
|
|
25
|
+
refs.push({ raw: match[0].trim(), path: match[1], anchor: match[2] });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return refs;
|
|
29
|
+
}
|
|
17
30
|
const AC_ID = /\bAC-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
|
|
18
31
|
const SECRET = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization)\s*[:=]\s*\S+/i;
|
|
19
32
|
const SECRET_ASSIGNMENT = /\b(password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization|cookie|set-cookie|credential)\b\s*[:=]\s*([^\r\n,;]+)/gi;
|
|
20
|
-
const SECRET_VALUE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:ghp|github_pat|xox[baprs]|AKIA
|
|
33
|
+
const SECRET_VALUE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b|\bsk[-_][-_A-Za-z0-9]{10,}\b/i;
|
|
21
34
|
const PLACEHOLDER_SECRET_VALUE = /^(?:<[^>]+>|\$\{[^}]+\}|\*+|x+|redacted|masked|example(?:[-_].*)?|sample(?:[-_].*)?|dummy(?:[-_].*)?|test(?:[-_].*)?|your[-_].*)$/i;
|
|
22
35
|
const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|后续补充|暂不实现|接口正常|结果正确|按实际情况处理/i;
|
|
23
36
|
const CASE_SECTION_ALIASES = {
|
|
@@ -187,8 +200,9 @@ export async function inspectBackendTestEnvironment(input) {
|
|
|
187
200
|
serverEntries,
|
|
188
201
|
};
|
|
189
202
|
}
|
|
190
|
-
async function markdownFiles(root) {
|
|
191
|
-
const
|
|
203
|
+
export async function markdownFiles(root, layout) {
|
|
204
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
205
|
+
const directory = path.join(root, ...resolved.markdownDir.split("/"));
|
|
192
206
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
193
207
|
return entries
|
|
194
208
|
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
|
|
@@ -303,6 +317,8 @@ function isCleanupStatement(value) {
|
|
|
303
317
|
return false;
|
|
304
318
|
if (/\b(?:assert|pytest\.fail|raise)\b/.test(trimmed))
|
|
305
319
|
return false;
|
|
320
|
+
if (/\b[A-Za-z_][A-Za-z0-9_.]*request\s*\(\s*["']DELETE["']/i.test(trimmed))
|
|
321
|
+
return true;
|
|
306
322
|
return /(?:^|[._])(?:cleanup|clean_up|teardown|delete|remove|close|release|rollback|disconnect|dispose|unlink)(?:[_(.]|$)/i.test(trimmed);
|
|
307
323
|
}
|
|
308
324
|
function findUnsafeSwallowedException(source) {
|
|
@@ -326,7 +342,18 @@ function findUnsafeSwallowedException(source) {
|
|
|
326
342
|
continue;
|
|
327
343
|
}
|
|
328
344
|
const exceptionType = exceptMatch[2]?.trim() || "bare except";
|
|
329
|
-
|
|
345
|
+
const narrowDecodeTypes = exceptionType
|
|
346
|
+
.replace(/[()]/g, "")
|
|
347
|
+
.split(",")
|
|
348
|
+
.map((value) => value.trim())
|
|
349
|
+
.filter(Boolean);
|
|
350
|
+
const precedingDecodeRegion = lines.slice(Math.max(0, index - 8), index).join("\n");
|
|
351
|
+
if (narrowDecodeTypes.length > 0 &&
|
|
352
|
+
narrowDecodeTypes.every((value) => value === "UnicodeDecodeError" || value === "json.JSONDecodeError") &&
|
|
353
|
+
/\.json\s*\(\s*\)/.test(precedingDecodeRegion)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (!new Set(["requests.RequestException", "urllib.error.URLError", "httpx.RequestError"]).has(exceptionType)) {
|
|
330
357
|
return exceptionType;
|
|
331
358
|
}
|
|
332
359
|
let tryIndex = index - 1;
|
|
@@ -366,9 +393,18 @@ function httpLoggingFindings(relativeScript, source) {
|
|
|
366
393
|
if (!hasHttpRequest)
|
|
367
394
|
return [];
|
|
368
395
|
const findings = [];
|
|
369
|
-
const hasRequestLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:request|请求)[^\n]*(?:method|url|path|params|query|json|body|payload|参数)/i.test(source)
|
|
370
|
-
|
|
371
|
-
|
|
396
|
+
const hasRequestLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:request|请求)[^\n]*(?:method|url|path|params|query|json|body|payload|参数)/i.test(source)
|
|
397
|
+
|| /["']record["']\s*:\s*["']request["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
|
|
398
|
+
|| /\b[A-Za-z_][A-Za-z0-9_]*log[A-Za-z0-9_]*\s*\(\s*["'](?:HTTP|API)_REQUEST["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
|
|
399
|
+
|| /print\s*\(\s*f?["'](?:HTTP|API)(?:_|\s+)REQUEST\b/i.test(source) && /["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
|
|
400
|
+
|| /\b[A-Za-z_][A-Za-z0-9_]*log\s*\(\s*["']REQUEST["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source);
|
|
401
|
+
const hasResponseLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:response|响应)[^\n]*(?:status|status_code|body|json|text|result|结果)/i.test(source)
|
|
402
|
+
|| /["']record["']\s*:\s*["']response["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
|
|
403
|
+
|| /\b[A-Za-z_][A-Za-z0-9_]*log[A-Za-z0-9_]*\s*\(\s*["'](?:HTTP|API)_RESPONSE["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
|
|
404
|
+
|| /print\s*\(\s*f?["'](?:HTTP|API)(?:_|\s+)RESPONSE\b/i.test(source) && /["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
|
|
405
|
+
|| /\b[A-Za-z_][A-Za-z0-9_]*log\s*\(\s*["']RESPONSE["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source);
|
|
406
|
+
const hasSanitizer = /\b(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(|\b[A-Za-z0-9_]*(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(/i.test(source)
|
|
407
|
+
|| /(?:SENSITIVE_KEYS|sensitive_keys)[\s\S]{0,1600}["']<redacted>["']/i.test(source);
|
|
372
408
|
const hasBoundedOutput = /(?:MAX_[A-Z_]*(?:LOG|BODY|RESPONSE)|max_(?:log|body|response)|truncate|truncat|\[:\s*\d{3,}\s*\])/i.test(source);
|
|
373
409
|
if (!hasRequestLog)
|
|
374
410
|
findings.push(`request logging evidence is missing in mapped backend pytest: ${relativeScript}`);
|
|
@@ -403,22 +439,23 @@ export function hasBlockingBackendMarkdownModuleStemFindings(report) {
|
|
|
403
439
|
return /^## Module Stem Status\s*\r?\n\s*BLOCKED\s*$/m.test(report);
|
|
404
440
|
}
|
|
405
441
|
export async function validateBackendMarkdownCases(input) {
|
|
442
|
+
const layout = input.layout ?? resolveBackendTestLayout(undefined);
|
|
406
443
|
const findings = [];
|
|
407
444
|
const safetyFindings = [];
|
|
408
445
|
const moduleStemFindings = [];
|
|
409
446
|
let files = [];
|
|
410
447
|
try {
|
|
411
|
-
files = await markdownFiles(input.workspaceRoot);
|
|
448
|
+
files = await markdownFiles(input.workspaceRoot, layout);
|
|
412
449
|
}
|
|
413
450
|
catch {
|
|
414
|
-
findings.push(
|
|
451
|
+
findings.push(`missing ${layout.markdownDir} directory`);
|
|
415
452
|
}
|
|
416
|
-
const readme = path.join(input.workspaceRoot, "
|
|
453
|
+
const readme = path.join(input.workspaceRoot, ...layout.readmePath.split("/"));
|
|
417
454
|
if (!(await exists(readme)))
|
|
418
|
-
findings.push(
|
|
455
|
+
findings.push(`missing ${layout.readmePath}`);
|
|
419
456
|
const moduleFiles = files.filter((file) => path.basename(file).toLowerCase() !== "readme.md");
|
|
420
457
|
if (moduleFiles.length === 0)
|
|
421
|
-
findings.push(
|
|
458
|
+
findings.push(`no ${layout.markdownDir} module case file found`);
|
|
422
459
|
const seen = new Map();
|
|
423
460
|
const coveredAc = new Set();
|
|
424
461
|
let caseCount = 0;
|
|
@@ -436,6 +473,18 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
436
473
|
if (PLACEHOLDER.test(markdown)) {
|
|
437
474
|
findings.push(`placeholder or non-assertable wording in ${relativeFile}`);
|
|
438
475
|
}
|
|
476
|
+
const sharedSetupRefs = extractBackendSharedSetupRefs(markdown);
|
|
477
|
+
for (const ref of sharedSetupRefs) {
|
|
478
|
+
if (!input.sharedSetup) {
|
|
479
|
+
findings.push(`shared setup reference without a bound document: ${ref.raw} in ${relativeFile}`);
|
|
480
|
+
}
|
|
481
|
+
else if (ref.path !== input.sharedSetup.path) {
|
|
482
|
+
findings.push(`shared setup reference cites unbound path ${ref.path} (bound: ${input.sharedSetup.path}) in ${relativeFile}`);
|
|
483
|
+
}
|
|
484
|
+
else if (ref.anchor !== "SS-DEFAULT" && !sharedSetupAnchorRe.test(ref.anchor)) {
|
|
485
|
+
findings.push(`shared setup reference anchor is not a stable SS ID and not SS-DEFAULT: ${ref.raw} in ${relativeFile}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
439
488
|
const cases = splitCases(markdown);
|
|
440
489
|
if (cases.length === 0) {
|
|
441
490
|
findings.push(`no BE-<MODULE>-<NNN> case heading in ${relativeFile}`);
|
|
@@ -459,6 +508,15 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
459
508
|
findings.push(`${testCase.id} missing section: ${sectionAliases.join(" or ")}`);
|
|
460
509
|
}
|
|
461
510
|
}
|
|
511
|
+
const bindingFinding = markdownTestPointBindingFinding(testCase.id, testCase.body);
|
|
512
|
+
if (bindingFinding)
|
|
513
|
+
findings.push(bindingFinding);
|
|
514
|
+
const payloadLabelFinding = markdownPayloadLabelFinding(testCase.id, testCase.body);
|
|
515
|
+
if (payloadLabelFinding)
|
|
516
|
+
findings.push(payloadLabelFinding);
|
|
517
|
+
const scenarioIntentFinding = markdownScenarioIntentFinding(testCase.id, testCase.body);
|
|
518
|
+
if (scenarioIntentFinding)
|
|
519
|
+
findings.push(scenarioIntentFinding);
|
|
462
520
|
const acIds = unique(testCase.body.match(AC_ID) ?? []);
|
|
463
521
|
acIds.forEach((id) => coveredAc.add(id));
|
|
464
522
|
const steps = sectionBody(testCase.body, CASE_SECTION_ALIASES.steps);
|
|
@@ -469,9 +527,12 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
469
527
|
if (!hasAssertableExpectedResult(expected)) {
|
|
470
528
|
findings.push(`${testCase.id} has no structured expected result`);
|
|
471
529
|
}
|
|
472
|
-
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
530
|
+
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile, layout);
|
|
473
531
|
const mappedScripts = extractMappedPytestScripts(testCase.body);
|
|
474
|
-
if (
|
|
532
|
+
if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
|
|
533
|
+
// Evidence-only meta cases intentionally declare 脚本/primary symbol as 无 and keep only assertion/cross-cutting TPs.
|
|
534
|
+
}
|
|
535
|
+
else if (mappedScripts.length === 0) {
|
|
475
536
|
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
|
|
476
537
|
}
|
|
477
538
|
else {
|
|
@@ -549,8 +610,9 @@ export function normalizeBackendTestModuleStem(moduleFileName) {
|
|
|
549
610
|
return stem;
|
|
550
611
|
}
|
|
551
612
|
/** Expected pytest script path for a Markdown module file (basename or relative path). */
|
|
552
|
-
export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName) {
|
|
553
|
-
|
|
613
|
+
export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName, layout) {
|
|
614
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
615
|
+
return `${resolved.scriptDir}/test_${normalizeBackendTestModuleStem(moduleFileName)}.py`;
|
|
554
616
|
}
|
|
555
617
|
function extractMappedPytestScripts(testCaseBody) {
|
|
556
618
|
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
@@ -560,12 +622,79 @@ function extractMappedPytestScripts(testCaseBody) {
|
|
|
560
622
|
.map((match) => match[1].replaceAll("\\", "/"))
|
|
561
623
|
.filter((value) => !value.includes("..")));
|
|
562
624
|
}
|
|
563
|
-
function
|
|
625
|
+
function markdownScenarioIntentFinding(testCaseId, testCaseBody) {
|
|
626
|
+
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
627
|
+
if (!automation.trim())
|
|
628
|
+
return null;
|
|
629
|
+
const variantLine = /^(?:\s*[-*+]\s*)?(?:变体测试点|Variant Test Points)\s*[::]\s*(.*)$/im.exec(automation)?.[1] ?? "";
|
|
630
|
+
const variants = unique(variantLine.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
|
|
631
|
+
if (variants.length === 0)
|
|
632
|
+
return null;
|
|
633
|
+
const localIntentIds = unique([...testCaseBody.matchAll(/(?:场景意图|Scenario Intent)\s*[::]\s*(TP-[A-Z0-9-]+)/gi)].map((match) => match[1].toUpperCase()));
|
|
634
|
+
const missing = variants.filter((id) => !localIntentIds.includes(id.toUpperCase()));
|
|
635
|
+
return missing.length > 0
|
|
636
|
+
? `${testCaseId} variant Test Points require a local Scenario Intent line inside the same Case: missing=${missing.join(", ")}`
|
|
637
|
+
: null;
|
|
638
|
+
}
|
|
639
|
+
function markdownPayloadLabelFinding(testCaseId, testCaseBody) {
|
|
640
|
+
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
641
|
+
if (!automation.trim())
|
|
642
|
+
return null;
|
|
643
|
+
const payloadLabel = /Payload (?:Contract|Required Paths|Allowed Paths|Enum)\s*[::]/gi;
|
|
644
|
+
for (const line of automation.split(/\r?\n/)) {
|
|
645
|
+
const matches = line.match(payloadLabel) ?? [];
|
|
646
|
+
if (matches.length === 0)
|
|
647
|
+
continue;
|
|
648
|
+
if (matches.length !== 1 || !/^\s*[-*+]\s*Payload (?:Contract|Required Paths|Allowed Paths|Enum)\s*[::]/i.test(line)) {
|
|
649
|
+
return `${testCaseId} payload contract labels must each occupy one independent machine-readable list line`;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
function markdownTestPointBindingFinding(testCaseId, testCaseBody) {
|
|
655
|
+
const declaredSection = sectionBody(testCaseBody, ["测试点", "Test Points"]);
|
|
656
|
+
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
657
|
+
if (!declaredSection.trim() || !automation.trim())
|
|
658
|
+
return null;
|
|
659
|
+
const declared = unique(declaredSection.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
|
|
660
|
+
const bindingLines = [
|
|
661
|
+
/^(?:\s*[-*+]\s*)?(?:变体测试点|Variant Test Points)\s*[::]\s*(.*)$/gim,
|
|
662
|
+
/^(?:\s*[-*+]\s*)?(?:场景断言测试点|Assertion Test Points)\s*[::]\s*(.*)$/gim,
|
|
663
|
+
/^(?:\s*[-*+]\s*)?(?:横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(.*)$/gim,
|
|
664
|
+
];
|
|
665
|
+
const bound = unique(bindingLines.flatMap((pattern) => [...automation.matchAll(pattern)].flatMap((match) => match[1]?.match(/\bTP-[A-Z0-9-]+\b/g) ?? [])));
|
|
666
|
+
const missingBindings = declared.filter((id) => !bound.includes(id));
|
|
667
|
+
const undeclaredBindings = bound.filter((id) => !declared.includes(id));
|
|
668
|
+
if (missingBindings.length === 0 && undeclaredBindings.length === 0)
|
|
669
|
+
return null;
|
|
670
|
+
return `${testCaseId} Test Point set must exactly equal variant/assertion/cross-cutting binding union: unclassified=${missingBindings.join(", ") || "none"}; undeclared-bindings=${undeclaredBindings.join(", ") || "none"}`;
|
|
671
|
+
}
|
|
672
|
+
function isEvidenceOnlyMarkdownCaseBody(testCaseBody) {
|
|
673
|
+
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
674
|
+
if (!automation.trim())
|
|
675
|
+
return false;
|
|
676
|
+
const scriptLine = /^\s*[-*+]\s*(?:脚本|script)\s*[::]\s*(.*)$/im.exec(automation)?.[1]?.replaceAll("`", "").trim() ??
|
|
677
|
+
"";
|
|
678
|
+
const primaryLine = /^\s*[-*+]\s*(?:primary symbol|Primary Symbol|主测试符号)\s*[::]\s*(.*)$/im.exec(automation)?.[1]
|
|
679
|
+
?.replaceAll("`", "")
|
|
680
|
+
.trim() ?? "";
|
|
681
|
+
const noneLike = /^(?:none|无|n\/a|not-applicable)(?:\s*[((].*[))])?$/i;
|
|
682
|
+
const scriptIsNone = noneLike.test(scriptLine) || scriptLine.length === 0;
|
|
683
|
+
const primaryIsNone = noneLike.test(primaryLine) || primaryLine.length === 0;
|
|
684
|
+
const hasMappedScript = extractMappedPytestScripts(testCaseBody).length > 0;
|
|
685
|
+
const hasPrimary = /\btest_[A-Za-z0-9_]+\b/.test(primaryLine);
|
|
686
|
+
const hasVariant = /(?:变体测试点|Variant Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
|
|
687
|
+
const hasAssertionOrCross = /(?:场景断言测试点|Assertion Test Points|横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
|
|
688
|
+
return scriptIsNone && primaryIsNone && !hasMappedScript && !hasPrimary && !hasVariant && hasAssertionOrCross;
|
|
689
|
+
}
|
|
690
|
+
function isSafeBackendPytestScript(script, workspaceRoot, layout) {
|
|
691
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
564
692
|
const absolute = path.resolve(workspaceRoot, script);
|
|
565
693
|
const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
|
|
694
|
+
const allowedRoots = [`${resolved.testRoot}/`];
|
|
566
695
|
return (!relative.startsWith("..") &&
|
|
567
696
|
!path.isAbsolute(relative) &&
|
|
568
|
-
script.startsWith(
|
|
697
|
+
allowedRoots.some((prefix) => script.startsWith(prefix)) &&
|
|
569
698
|
!script.includes("..") &&
|
|
570
699
|
/^test_.*\.py$/i.test(path.basename(script)));
|
|
571
700
|
}
|
|
@@ -609,8 +738,8 @@ function symbolCaseId(symbol) {
|
|
|
609
738
|
? canonicalBackendCaseId(match[1].replaceAll("_", "-"))
|
|
610
739
|
: undefined;
|
|
611
740
|
}
|
|
612
|
-
export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
613
|
-
const files = await markdownFiles(workspaceRoot);
|
|
741
|
+
export async function validateBackendMarkdownTraceability(workspaceRoot, layout) {
|
|
742
|
+
const files = await markdownFiles(workspaceRoot, layout);
|
|
614
743
|
const markdownIds = new Set();
|
|
615
744
|
const expectedIdsByScript = new Map();
|
|
616
745
|
const findings = [];
|
|
@@ -619,8 +748,11 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
619
748
|
continue;
|
|
620
749
|
const content = await readFile(file, "utf8");
|
|
621
750
|
for (const testCase of splitCases(content)) {
|
|
622
|
-
markdownIds.add(testCase.id);
|
|
623
751
|
const scripts = extractMappedPytestScripts(testCase.body);
|
|
752
|
+
if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
markdownIds.add(testCase.id);
|
|
624
756
|
if (scripts.length === 0) {
|
|
625
757
|
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射`);
|
|
626
758
|
continue;
|
|
@@ -632,6 +764,7 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
632
764
|
}
|
|
633
765
|
}
|
|
634
766
|
}
|
|
767
|
+
const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
|
|
635
768
|
const mappings = new Map();
|
|
636
769
|
const automationIds = new Set();
|
|
637
770
|
let requestResponseLoggingFindingCount = 0;
|
|
@@ -640,7 +773,7 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
640
773
|
const relative = path.relative(workspaceRoot, absolute);
|
|
641
774
|
if (relative.startsWith("..") ||
|
|
642
775
|
path.isAbsolute(relative) ||
|
|
643
|
-
!relativeScript.startsWith(
|
|
776
|
+
!relativeScript.startsWith(`${resolvedLayout.testRoot}/`) ||
|
|
644
777
|
!/^test_.*\.py$/i.test(path.basename(relativeScript))) {
|
|
645
778
|
findings.push(`unsafe mapped pytest script: ${relativeScript}`);
|
|
646
779
|
continue;
|
|
@@ -713,8 +846,9 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
713
846
|
...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
|
|
714
847
|
].join("\n") + "\n");
|
|
715
848
|
}
|
|
716
|
-
export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
717
|
-
const
|
|
849
|
+
export async function resolveBackendTestMappedPytestScripts(workspaceRoot, layout) {
|
|
850
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
851
|
+
const files = await markdownFiles(workspaceRoot, resolved);
|
|
718
852
|
const expectedScripts = new Set();
|
|
719
853
|
const existingScripts = new Set();
|
|
720
854
|
const missingScripts = new Set();
|
|
@@ -735,7 +869,7 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
735
869
|
if (path.basename(file).toLowerCase() === "readme.md")
|
|
736
870
|
continue;
|
|
737
871
|
const relativeFile = path.relative(workspaceRoot, file).replaceAll(path.sep, "/");
|
|
738
|
-
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
872
|
+
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile, resolved);
|
|
739
873
|
const markdown = await readFile(file, "utf8");
|
|
740
874
|
const mappedFromModule = new Set();
|
|
741
875
|
for (const testCase of splitCases(markdown)) {
|
|
@@ -755,14 +889,14 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
755
889
|
continue;
|
|
756
890
|
}
|
|
757
891
|
for (const script of mappedFromModule) {
|
|
758
|
-
if (!isSafeBackendPytestScript(script, workspaceRoot)) {
|
|
892
|
+
if (!isSafeBackendPytestScript(script, workspaceRoot, resolved)) {
|
|
759
893
|
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
760
894
|
}
|
|
761
895
|
if (await mappedScriptExists(path.resolve(workspaceRoot, script))) {
|
|
762
896
|
recordScript(script, relativeFile, true);
|
|
763
897
|
continue;
|
|
764
898
|
}
|
|
765
|
-
if (!isSafeBackendPytestScript(expectedScript, workspaceRoot)) {
|
|
899
|
+
if (!isSafeBackendPytestScript(expectedScript, workspaceRoot, resolved)) {
|
|
766
900
|
throw new Error(`unsafe mapped pytest script: ${expectedScript}`);
|
|
767
901
|
}
|
|
768
902
|
recordScript(expectedScript, relativeFile, await mappedScriptExists(path.resolve(workspaceRoot, expectedScript)));
|
|
@@ -777,8 +911,8 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
777
911
|
missingScripts: unique(missingScripts),
|
|
778
912
|
};
|
|
779
913
|
}
|
|
780
|
-
export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
|
|
781
|
-
const resolution = await resolveBackendTestMappedPytestScripts(workspaceRoot);
|
|
914
|
+
export async function collectBackendTestMappedPytestScripts(workspaceRoot, layout) {
|
|
915
|
+
const resolution = await resolveBackendTestMappedPytestScripts(workspaceRoot, layout);
|
|
782
916
|
if (resolution.missingScripts.length > 0) {
|
|
783
917
|
throw new Error(`mapped pytest script is missing: ${resolution.missingScripts[0]}`);
|
|
784
918
|
}
|
|
@@ -791,8 +925,8 @@ function cleanCaseTitle(rawHeading, id) {
|
|
|
791
925
|
.replace(/^BE-[A-Z0-9_-]+-\d{2,3}\s*(?:[||—–-]\s*)?/i, "")
|
|
792
926
|
.trim() || id;
|
|
793
927
|
}
|
|
794
|
-
export async function collectBackendTestHumanCaseCatalog(workspaceRoot) {
|
|
795
|
-
const files = await markdownFiles(workspaceRoot);
|
|
928
|
+
export async function collectBackendTestHumanCaseCatalog(workspaceRoot, layout) {
|
|
929
|
+
const files = await markdownFiles(workspaceRoot, layout);
|
|
796
930
|
const catalog = new Map();
|
|
797
931
|
for (const file of files) {
|
|
798
932
|
if (path.basename(file).toLowerCase() === "readme.md")
|
|
@@ -814,7 +948,8 @@ export async function collectBackendTestHumanCaseCatalog(workspaceRoot) {
|
|
|
814
948
|
});
|
|
815
949
|
}
|
|
816
950
|
}
|
|
817
|
-
const
|
|
951
|
+
const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
|
|
952
|
+
const testcaseRoot = path.join(workspaceRoot, ...resolvedLayout.testRoot.split("/"));
|
|
818
953
|
const pythonFiles = [];
|
|
819
954
|
async function walk(directory) {
|
|
820
955
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
@@ -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);
|