@tea-agent/loop-agent 0.40.0-next.8 → 0.40.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/AGENTS.md +1 -1
- package/CHANGELOG.md +21 -20
- package/README.md +2 -2
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +9 -5
- package/dist/cli/program.js +38 -3
- package/dist/commands/client-recovery.js +8 -36
- package/dist/commands/init-model-catalog.js +22 -0
- package/dist/commands/task-advance.js +23 -1
- package/dist/commands/task-source-prepare.js +5 -0
- package/dist/commands/task-status.js +1 -0
- package/dist/executors/dag-pi-executor.js +3 -0
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +79 -6
- package/dist/executors/shell-executor.js +94 -20
- package/dist/governance/checks.js +1 -0
- package/dist/shared/operator/capabilities.js +9 -1
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- package/dist/worker/console/chat/assistant-content.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +82 -11
- package/dist/worker/console/chat/goal-round-driver.js +72 -0
- package/dist/worker/console/chat/goals.js +300 -0
- package/dist/worker/console/chat/pi-runtime.js +285 -8
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/repo-browser.js +94 -12
- package/dist/worker/console/chat/routes.js +495 -30
- package/dist/worker/console/chat/session-catalog.js +48 -0
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +27 -18
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/chat/turn-process.js +42 -5
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/operator-actions.js +20 -5
- package/dist/worker/console/operator-selection.js +6 -2
- package/dist/worker/console/pi-plugins.catalog.json +73658 -0
- package/dist/worker/console/pi-plugins.js +179 -0
- package/dist/worker/console/routes.js +246 -6
- package/dist/worker/console/server.js +173 -42
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BYBmDfPW.js → abnfDiagram-N423BO3Z-Dy71QJp3.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BKMmxpGF.js → arc-M25fNMTE.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DJpwxr1d.js → architectureDiagram-T3A2C74G-CUWlVl1a.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BaPj_ZlY.js → blockDiagram-VBNYF7ZC-Dd0Whpx2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8iGmHlm.js → c4Diagram-5PPSVZJV-BdfeCkI5.js} +1 -1
- package/dist/worker/console/static/assets/channel-Ba0AKiEZ.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DaxXQayc.js → chunk-2GRJ4B5K-D_1sUkyD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DqQKGA7q.js → chunk-2Q5K7J3B-Cxnxf36E.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CgMKpJBB.js → chunk-5RXB4S5H-D9RG3G_x.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpToUlaW.js → chunk-5VM5RSS4-Zz9jgjLX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BsEWOHxv.js → chunk-6Q2QTUOP-BCggDRCH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CKGSCe71.js → chunk-GF5L2VYU-CFg-5iAS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Bm1U5299.js → chunk-JWPE2WC7-DRQTE8x_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BI0ov870.js → chunk-KBJHAD2P-C_Dw7uZ8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BmnQo0iJ.js → chunk-RYQCIY6F-BjIiApsk.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJBqa6Xc.js → chunk-XXDRQBXY-B5XQD4KG.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BU3IjVEe.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BU3IjVEe.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BuN9-7AR.js → cose-bilkent-JH36ORCC-huHqH6WQ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bw22jc2k.js → cynefin-VYW2F7L2-BSi6yZqP.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BHnSH8Dr.js → cynefinDiagram-MW4NZA55-Bs_Jp31y.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BoXXIegj.js → dagre-VZM6K2ZE-CYxavikT.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CU8qyPhT.js → diagram-7IWD3JNH-B6TErl71.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-lrjhF0S8.js → diagram-B4RE2ZJO-CvH4J2ii.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DL0QDEpo.js → diagram-LBJQPF4R-v1NBAqXS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DxVMJMBH.js → diagram-Q27KOJAE-CUNHVAn2.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CFg9FzvG.js → diagram-UB23O5K3-Dzz1gUu-.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CQgP9fTG.js → ebnfDiagram-BXEA7PRR-CCF8_LjK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-A5vQXx9U.js → erDiagram-JOGREHBK-DObdPVLa.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-ytXnLyXD.js → flowDiagram-UKHOOZJN-DlN8Pxrj.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CYuksfnS.js → ganttDiagram-PKOTCBZU-BJKIPTGX.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-f_jW3zgU.js → gitGraphDiagram-DS77QQ5N-XcSSnObi.js} +1 -1
- package/dist/worker/console/static/assets/index-CIYzS16i.css +1 -0
- package/dist/worker/console/static/assets/index-CS3QOTfl.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C3HLE-xV.js → infoDiagram-6WML65LV-BKrRKMww.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DwpgejS6.js → ishikawaDiagram-WSZJBQD7-CN5dk-yb.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ll0YWX1y.js → journeyDiagram-NVQOT4AX-B4P4KZ-Y.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Hq3rchvU.js → kanban-definition-27J2QSJJ-BRlteP32.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BaGjJx8z.js → linear-CG8D9fuL.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CKGQHWX_.js → mermaid.core-BuB6P2rX.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DM1FA6Dr.js → mindmap-definition-FAOFIHXS-DYXGl_DR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Xa9fHkE2.js → pegDiagram-VL7TDLO6-BM48yEVG.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-pCd4PDqJ.js → pieDiagram-7S7Q4E2Y-mT8NL-Ow.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-KqjpEBLk.js → quadrantDiagram-CIZ2JOQS-C4pZ6DH_.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DgvR7LZD.js → railroadDiagram-AXF67PYL-BKAcr70m.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bys1UCWA.js → requirementDiagram-LRYGKXZP-CRxcKpoO.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Ms9npnZt.js → sankeyDiagram-W5VNT64P-Ds4LxQDI.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DEIWrtQZ.js → sequenceDiagram-SI44F4Z6-CPLwPf_p.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DELJ0AhZ.js → sizeCapture-X5ZJPWSS-CL65uij0.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BpHt9wKh.js → stateDiagram-OKZ733FA-CO0Mgd1W.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BUewAVYS.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-GKRsBluT.js → swimlanes-SLNWSIFB-Cxv3TSNw.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bhe67Wjl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cwsq1DxL.js → timeline-definition-Z64GVDOM-Dw85f9L2.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CfaqtKx0.js → vennDiagram-T6HMQDX7-B4QLCqCy.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BlEUtK_g.js → wardleyDiagram-T6FBY63Y-B_yKCWwn.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CHSRXPvO.js → xychartDiagram-ELKLHX3M-nniBEGv2.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
- package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
- package/dist/worker/console/workspace-context.js +28 -3
- package/dist/worker/console/workspace-registry.js +333 -60
- package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
- package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
- package/dist/worker/observe/static/inspect-workspace.js +54 -10
- package/dist/worker/observe/static/operator-chrome.css +23 -4
- package/dist/worker/observe/static/operator-chrome.js +7 -2
- package/dist/worker/observe/static/styles.css +87 -25
- package/dist/worker/observe/static/views/dag-inspector.js +42 -1
- package/dist/worker/observe/static/views/dags.js +7 -5
- package/dist/worker/observe/static/views/session-timeline.js +135 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +5 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +18 -12
- package/dist/workflows/dag/init-hybrid.js +32 -9
- package/dist/workflows/dag/types.js +13 -0
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +27 -9
- package/docs/templates/init-managed-agents.md +1 -1
- package/package.json +3 -2
- package/skills/loop-agent/references/command-reference.md +15 -1
- package/dist/worker/console/static/assets/channel-CrzAa3FA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-B7QwCCb4.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-B7QwCCb4.js +0 -1
- package/dist/worker/console/static/assets/index-BB6KxwT_.css +0 -1
- package/dist/worker/console/static/assets/index-D_wxbadT.js +0 -407
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BEeSswlv.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B2m_wca8.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -20,7 +20,8 @@ const factsSchema = z
|
|
|
20
20
|
tpId: z.string(),
|
|
21
21
|
field: z.string().optional(),
|
|
22
22
|
intent: z.string(),
|
|
23
|
-
observed: z.string(),
|
|
23
|
+
observed: z.string().max(200),
|
|
24
|
+
observedLength: z.number().int().min(0).optional(),
|
|
24
25
|
status: z.enum(["MATCH", "MISMATCH", "UNDETERMINED"]),
|
|
25
26
|
repairability: z.enum([
|
|
26
27
|
"repairable",
|
|
@@ -28,6 +29,12 @@ const factsSchema = z
|
|
|
28
29
|
"none",
|
|
29
30
|
"undetermined-no-repair",
|
|
30
31
|
]),
|
|
32
|
+
reasonCode: z.enum(["SCENARIO_PARAM_MATCH", "SCENARIO_PARAM_MISMATCH", "SCENARIO_PARAM_UNDETERMINED"]),
|
|
33
|
+
expectedRaw: z.string().max(512).optional(),
|
|
34
|
+
observedRaw: z.string().max(512),
|
|
35
|
+
expectedNormalized: z.string().max(512).optional(),
|
|
36
|
+
observedNormalized: z.string().max(512).optional(),
|
|
37
|
+
sourceLocation: z.string().max(512).optional(),
|
|
31
38
|
suggestedFix: z.string().optional(),
|
|
32
39
|
scriptPath: z.string().optional(),
|
|
33
40
|
bound: z.number().optional(),
|
|
@@ -435,8 +442,16 @@ function isWhitespacePaddedString(value) {
|
|
|
435
442
|
}
|
|
436
443
|
function normalizeScenarioLiteralText(value) {
|
|
437
444
|
// Writers sometimes emit visible-space placeholders (U+2420), NBSP variants,
|
|
438
|
-
//
|
|
445
|
+
// URL-encoded spaces, or explicit Python/JSON whitespace escapes in
|
|
446
|
+
// machine-readable Markdown. Decode only escapes that resolve to whitespace;
|
|
447
|
+
// ordinary business characters such as `\\u0041` remain literal and fail closed.
|
|
448
|
+
const decodeWhitespaceEscape = (token, hex) => {
|
|
449
|
+
const decoded = String.fromCodePoint(Number.parseInt(hex, 16));
|
|
450
|
+
return /^\s$/u.test(decoded) || /[ ]/u.test(decoded) ? decoded : token;
|
|
451
|
+
};
|
|
439
452
|
const normalized = value
|
|
453
|
+
.replace(/\\u([0-9a-f]{4})/gi, decodeWhitespaceEscape)
|
|
454
|
+
.replace(/\\x([0-9a-f]{2})/gi, decodeWhitespaceEscape)
|
|
440
455
|
.replace(/␠/g, " ")
|
|
441
456
|
.replace(/ /g, " ")
|
|
442
457
|
.replace(/ /g, " ")
|
|
@@ -677,6 +692,50 @@ export function extractPytestParamBlock(source, tpId) {
|
|
|
677
692
|
}
|
|
678
693
|
return undefined;
|
|
679
694
|
}
|
|
695
|
+
function extractPytestParamTopLevelArgs(block) {
|
|
696
|
+
if (!block)
|
|
697
|
+
return [];
|
|
698
|
+
const open = block.indexOf("(");
|
|
699
|
+
const close = block.lastIndexOf(")");
|
|
700
|
+
if (open < 0 || close <= open)
|
|
701
|
+
return [];
|
|
702
|
+
const input = block.slice(open + 1, close);
|
|
703
|
+
const args = [];
|
|
704
|
+
let start = 0;
|
|
705
|
+
let depth = 0;
|
|
706
|
+
let quote = "";
|
|
707
|
+
let escaped = false;
|
|
708
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
709
|
+
const ch = input[index];
|
|
710
|
+
if (escaped) {
|
|
711
|
+
escaped = false;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (ch === "\\") {
|
|
715
|
+
escaped = true;
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (quote) {
|
|
719
|
+
if (ch === quote)
|
|
720
|
+
quote = "";
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (ch === '"' || ch === "'") {
|
|
724
|
+
quote = ch;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (ch === "[" || ch === "{" || ch === "(")
|
|
728
|
+
depth += 1;
|
|
729
|
+
else if (ch === "]" || ch === "}" || ch === ")")
|
|
730
|
+
depth -= 1;
|
|
731
|
+
else if (ch === "," && depth === 0) {
|
|
732
|
+
args.push(input.slice(start, index).trim());
|
|
733
|
+
start = index + 1;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
args.push(input.slice(start).trim());
|
|
737
|
+
return args.filter((arg) => arg && !/^(?:id|marks)\s*=/.test(arg));
|
|
738
|
+
}
|
|
680
739
|
function extractPytestParamParameterNames(source, tpId) {
|
|
681
740
|
const block = extractPytestParamBlock(source, tpId);
|
|
682
741
|
if (!block)
|
|
@@ -688,8 +747,11 @@ function extractPytestParamParameterNames(source, tpId) {
|
|
|
688
747
|
if (decoratorIndex < 0)
|
|
689
748
|
return [];
|
|
690
749
|
const prefix = source.slice(decoratorIndex, blockIndex);
|
|
691
|
-
const
|
|
692
|
-
|
|
750
|
+
const scalar = /@pytest\.mark\.parametrize\s*\(\s*["']([^"']+)["']/s.exec(prefix)?.[1];
|
|
751
|
+
if (scalar)
|
|
752
|
+
return scalar.split(",").map((name) => name.trim()).filter(Boolean);
|
|
753
|
+
const tuple = /@pytest\.mark\.parametrize\s*\(\s*\(\s*((?:["'][^"']+["']\s*,?\s*)+)\)/s.exec(prefix)?.[1];
|
|
754
|
+
return tuple ? [...tuple.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]).filter(Boolean) : [];
|
|
693
755
|
}
|
|
694
756
|
export function observeParamFeatures(block, field, constants, parameterNames) {
|
|
695
757
|
if (!block)
|
|
@@ -749,48 +811,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
|
|
|
749
811
|
}
|
|
750
812
|
// Parse top-level positional args before the legacy token scanner so list
|
|
751
813
|
// cardinality and field/value rows remain statically visible.
|
|
752
|
-
const topLevelArgs = (
|
|
753
|
-
const open = block.indexOf("(");
|
|
754
|
-
const close = block.lastIndexOf(")");
|
|
755
|
-
if (open < 0 || close <= open)
|
|
756
|
-
return [];
|
|
757
|
-
const input = block.slice(open + 1, close);
|
|
758
|
-
const args = [];
|
|
759
|
-
let start = 0;
|
|
760
|
-
let depth = 0;
|
|
761
|
-
let quote = "";
|
|
762
|
-
let escaped = false;
|
|
763
|
-
for (let i = 0; i < input.length; i += 1) {
|
|
764
|
-
const ch = input[i];
|
|
765
|
-
if (escaped) {
|
|
766
|
-
escaped = false;
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
if (ch === "\\") {
|
|
770
|
-
escaped = true;
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
773
|
-
if (quote) {
|
|
774
|
-
if (ch === quote)
|
|
775
|
-
quote = "";
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
if (ch === '"' || ch === "'") {
|
|
779
|
-
quote = ch;
|
|
780
|
-
continue;
|
|
781
|
-
}
|
|
782
|
-
if (ch === "[" || ch === "{" || ch === "(")
|
|
783
|
-
depth += 1;
|
|
784
|
-
else if (ch === "]" || ch === "}" || ch === ")")
|
|
785
|
-
depth -= 1;
|
|
786
|
-
else if (ch === "," && depth === 0) {
|
|
787
|
-
args.push(input.slice(start, i).trim());
|
|
788
|
-
start = i + 1;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
args.push(input.slice(start).trim());
|
|
792
|
-
return args.filter((arg) => arg && !/^(?:id|marks)\s*=/.test(arg));
|
|
793
|
-
})();
|
|
814
|
+
const topLevelArgs = extractPytestParamTopLevelArgs(block);
|
|
794
815
|
if (topLevelArgs.length > 0) {
|
|
795
816
|
const idTp = /\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/i.exec(block)?.[1]?.toUpperCase();
|
|
796
817
|
const meaningfulArgs = topLevelArgs.filter((arg) => {
|
|
@@ -1161,11 +1182,13 @@ function suggestedFixFor(intent, field, bound, example) {
|
|
|
1161
1182
|
: undefined;
|
|
1162
1183
|
default:
|
|
1163
1184
|
if (intent.startsWith("custom-literal:")) {
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1185
|
+
const expectedRaw = intent.slice("custom-literal:".length);
|
|
1186
|
+
const expected = normalizeCustomLiteralExpected(expectedRaw);
|
|
1187
|
+
if (isWhitespaceSemanticLiteral(expectedRaw) || isWhitespaceOnlyString(expected)) {
|
|
1188
|
+
const length = isWhitespaceOnlyString(expected) ? expected.length : 3;
|
|
1189
|
+
return field ? `set ${field} to ${length} literal spaces` : `set value to ${length} literal spaces`;
|
|
1167
1190
|
}
|
|
1168
|
-
if (isWhitespacePaddedSemanticLiteral(
|
|
1191
|
+
if (isWhitespacePaddedSemanticLiteral(expectedRaw)) {
|
|
1169
1192
|
return field
|
|
1170
1193
|
? `set ${field}=" ${field}-value "`
|
|
1171
1194
|
: 'set value=" value "';
|
|
@@ -1177,6 +1200,200 @@ function suggestedFixFor(intent, field, bound, example) {
|
|
|
1177
1200
|
return undefined;
|
|
1178
1201
|
}
|
|
1179
1202
|
}
|
|
1203
|
+
const MAX_PERSISTED_SCENARIO_TEXT = 192;
|
|
1204
|
+
function boundedScenarioText(value, logicalLength) {
|
|
1205
|
+
if (value.length <= MAX_PERSISTED_SCENARIO_TEXT)
|
|
1206
|
+
return value;
|
|
1207
|
+
const suffix = `<truncated length=${logicalLength ?? value.length}>`;
|
|
1208
|
+
return `${value.slice(0, Math.max(0, MAX_PERSISTED_SCENARIO_TEXT - suffix.length - 3))}...${suffix}`;
|
|
1209
|
+
}
|
|
1210
|
+
function localPythonFunctionRegion(source, functionName) {
|
|
1211
|
+
const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1212
|
+
const startMatch = new RegExp(`^def\\s+${escaped}\\s*\\(`, "m").exec(source);
|
|
1213
|
+
if (!startMatch || startMatch.index === undefined)
|
|
1214
|
+
return undefined;
|
|
1215
|
+
const start = startMatch.index;
|
|
1216
|
+
const remainder = source.slice(start + startMatch[0].length);
|
|
1217
|
+
const nextTopLevel = /^(?:def|class|@pytest\b|@(?:[A-Za-z_][A-Za-z0-9_.]*))\s*/m.exec(remainder);
|
|
1218
|
+
return source.slice(start, nextTopLevel?.index === undefined ? source.length : start + startMatch[0].length + nextTopLevel.index);
|
|
1219
|
+
}
|
|
1220
|
+
function splitPythonTopLevelArguments(input) {
|
|
1221
|
+
const args = [];
|
|
1222
|
+
let start = 0;
|
|
1223
|
+
let depth = 0;
|
|
1224
|
+
let quote = "";
|
|
1225
|
+
let escaped = false;
|
|
1226
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1227
|
+
const ch = input[index];
|
|
1228
|
+
if (escaped) {
|
|
1229
|
+
escaped = false;
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (ch === "\\") {
|
|
1233
|
+
escaped = true;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (quote) {
|
|
1237
|
+
if (ch === quote)
|
|
1238
|
+
quote = "";
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (ch === '"' || ch === "'") {
|
|
1242
|
+
quote = ch;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (ch === "[" || ch === "{" || ch === "(")
|
|
1246
|
+
depth += 1;
|
|
1247
|
+
else if (ch === "]" || ch === "}" || ch === ")")
|
|
1248
|
+
depth -= 1;
|
|
1249
|
+
else if (ch === "," && depth === 0) {
|
|
1250
|
+
args.push(input.slice(start, index).trim());
|
|
1251
|
+
start = index + 1;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
args.push(input.slice(start).trim());
|
|
1255
|
+
return args.filter(Boolean);
|
|
1256
|
+
}
|
|
1257
|
+
function parseSimplePythonCall(expression) {
|
|
1258
|
+
const call = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*)\)$/.exec(expression.trim());
|
|
1259
|
+
if (!call)
|
|
1260
|
+
return undefined;
|
|
1261
|
+
const positional = [];
|
|
1262
|
+
const keywords = new Map();
|
|
1263
|
+
for (const argument of splitPythonTopLevelArguments(call[2] ?? "")) {
|
|
1264
|
+
const keyword = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]+)$/.exec(argument);
|
|
1265
|
+
if (keyword) {
|
|
1266
|
+
if (keywords.has(keyword[1]))
|
|
1267
|
+
return undefined;
|
|
1268
|
+
keywords.set(keyword[1], keyword[2].trim());
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
if (keywords.size > 0 || /^\*{1,2}/.test(argument))
|
|
1272
|
+
return undefined;
|
|
1273
|
+
positional.push(argument);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return { name: call[1], positional, keywords };
|
|
1277
|
+
}
|
|
1278
|
+
function safeSameModuleDictHelper(source, helperName) {
|
|
1279
|
+
if (!helperName.startsWith("_"))
|
|
1280
|
+
return undefined;
|
|
1281
|
+
const region = localPythonFunctionRegion(source, helperName);
|
|
1282
|
+
if (!region)
|
|
1283
|
+
return undefined;
|
|
1284
|
+
const functionBody = region.slice(region.indexOf(":") + 1);
|
|
1285
|
+
const escapedName = helperName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1286
|
+
if (new RegExp(`\\b${escapedName}\\s*\\(`).test(functionBody))
|
|
1287
|
+
return undefined;
|
|
1288
|
+
if (/\b(?:Faker|fake|random|randint|uuid4|environ|getenv|requests|httpx|urlopen|subprocess|Popen|socket|open)\b/i.test(region))
|
|
1289
|
+
return undefined;
|
|
1290
|
+
const signature = new RegExp(`^def\\s+${escapedName}\\s*\\(([\\s\\S]*?)\\)\\s*(?:->[^:]+)?\\s*:`, "m").exec(region);
|
|
1291
|
+
if (!signature)
|
|
1292
|
+
return undefined;
|
|
1293
|
+
const parameters = [];
|
|
1294
|
+
for (const rawParameter of splitPythonTopLevelArguments(signature[1] ?? "")) {
|
|
1295
|
+
if (rawParameter === "*" || rawParameter === "/")
|
|
1296
|
+
continue;
|
|
1297
|
+
if (/^\*{1,2}/.test(rawParameter))
|
|
1298
|
+
return undefined;
|
|
1299
|
+
const parameter = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s*:[^=]+)?(?:\s*=\s*([\s\S]+))?$/.exec(rawParameter);
|
|
1300
|
+
if (!parameter)
|
|
1301
|
+
return undefined;
|
|
1302
|
+
parameters.push({ name: parameter[1], defaultExpression: parameter[2]?.trim() });
|
|
1303
|
+
}
|
|
1304
|
+
const directReturn = /\breturn\s*(\{[\s\S]*?\})/.exec(functionBody)?.[1];
|
|
1305
|
+
const assignment = /\b([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=\n]+)?=\s*(\{[\s\S]*?\})/.exec(functionBody);
|
|
1306
|
+
const assignedReturn = assignment && new RegExp(`\\breturn\\s+${assignment[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(functionBody)
|
|
1307
|
+
? assignment[2]
|
|
1308
|
+
: undefined;
|
|
1309
|
+
const dictExpression = directReturn ?? assignedReturn;
|
|
1310
|
+
if (!dictExpression)
|
|
1311
|
+
return undefined;
|
|
1312
|
+
const fieldBindings = new Map();
|
|
1313
|
+
for (const item of splitPythonTopLevelArguments(dictExpression.slice(1, -1))) {
|
|
1314
|
+
const binding = /^(?:["']([^"']+)["'])\s*:\s*([A-Za-z_][A-Za-z0-9_]*)$/.exec(item);
|
|
1315
|
+
if (!binding || !parameters.some((parameter) => parameter.name === binding[2]))
|
|
1316
|
+
return undefined;
|
|
1317
|
+
fieldBindings.set(binding[1], binding[2]);
|
|
1318
|
+
}
|
|
1319
|
+
return { parameters, fieldBindings };
|
|
1320
|
+
}
|
|
1321
|
+
function observeSameModulePayloadHelperField(input) {
|
|
1322
|
+
const payloadIndex = input.parameterNames.findIndex((name) => /^(?:payload|body|data|json)$/i.test(name));
|
|
1323
|
+
if (payloadIndex < 0)
|
|
1324
|
+
return undefined;
|
|
1325
|
+
let expression = extractPytestParamTopLevelArgs(input.block)[payloadIndex]?.trim() ?? "";
|
|
1326
|
+
const wrapper = parseSimplePythonCall(expression);
|
|
1327
|
+
if (wrapper?.name === "_without") {
|
|
1328
|
+
if (wrapper.positional.length !== 2 || wrapper.keywords.size > 0)
|
|
1329
|
+
return undefined;
|
|
1330
|
+
const removed = observeLiteralToken(wrapper.positional[1], input.field, input.constants);
|
|
1331
|
+
if (removed?.kind !== "string" || removed.literal !== input.field)
|
|
1332
|
+
return undefined;
|
|
1333
|
+
expression = wrapper.positional[0];
|
|
1334
|
+
const inner = parseSimplePythonCall(expression);
|
|
1335
|
+
if (!inner || !safeSameModuleDictHelper(input.source, inner.name))
|
|
1336
|
+
return undefined;
|
|
1337
|
+
return { kind: "missing-key", text: `missing:${input.field}` };
|
|
1338
|
+
}
|
|
1339
|
+
const call = parseSimplePythonCall(expression);
|
|
1340
|
+
if (!call)
|
|
1341
|
+
return undefined;
|
|
1342
|
+
const helper = safeSameModuleDictHelper(input.source, call.name);
|
|
1343
|
+
if (!helper)
|
|
1344
|
+
return undefined;
|
|
1345
|
+
const parameterName = helper.fieldBindings.get(input.field);
|
|
1346
|
+
if (!parameterName)
|
|
1347
|
+
return { kind: "missing-key", text: `missing:${input.field}` };
|
|
1348
|
+
const parameterIndex = helper.parameters.findIndex((parameter) => parameter.name === parameterName);
|
|
1349
|
+
if (parameterIndex < 0 || call.positional.length > helper.parameters.length)
|
|
1350
|
+
return undefined;
|
|
1351
|
+
const expressionForField = call.keywords.get(parameterName) ?? call.positional[parameterIndex] ?? helper.parameters[parameterIndex]?.defaultExpression;
|
|
1352
|
+
return expressionForField
|
|
1353
|
+
? observeLiteralToken(expressionForField, input.field, input.constants)
|
|
1354
|
+
: undefined;
|
|
1355
|
+
}
|
|
1356
|
+
function hasSafeSameModulePayloadHelper(source, block, parameterNames) {
|
|
1357
|
+
const payloadIndex = parameterNames.findIndex((name) => /^(?:payload|body|data|json)$/i.test(name));
|
|
1358
|
+
if (payloadIndex < 0)
|
|
1359
|
+
return false;
|
|
1360
|
+
const expression = extractPytestParamTopLevelArgs(block)[payloadIndex]?.trim() ?? "";
|
|
1361
|
+
const call = parseSimplePythonCall(expression);
|
|
1362
|
+
if (!call)
|
|
1363
|
+
return false;
|
|
1364
|
+
if (/\b(?:Faker|fake|random|randint|uuid4|environ|getenv|requests|httpx|urlopen|subprocess|Popen|socket|open)\b/i.test(expression))
|
|
1365
|
+
return false;
|
|
1366
|
+
return Boolean(safeSameModuleDictHelper(source, call.name));
|
|
1367
|
+
}
|
|
1368
|
+
function observeDeterministicFunctionFieldOverride(input) {
|
|
1369
|
+
const region = scenarioFunctionRegion(input.source, input.tpId);
|
|
1370
|
+
if (!region)
|
|
1371
|
+
return undefined;
|
|
1372
|
+
const escapedField = input.field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1373
|
+
const assignment = new RegExp(`^([ \\t]*)([A-Za-z_][A-Za-z0-9_]*)\\s*\\[\\s*["']${escapedField}["']\\s*\\]\\s*=\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*$`, "m").exec(region);
|
|
1374
|
+
if (!assignment)
|
|
1375
|
+
return undefined;
|
|
1376
|
+
const indentation = assignment[1]?.length ?? 0;
|
|
1377
|
+
const valueParameter = assignment[3];
|
|
1378
|
+
const valueIndex = input.parameterNames.indexOf(valueParameter);
|
|
1379
|
+
if (valueIndex < 0)
|
|
1380
|
+
return undefined;
|
|
1381
|
+
const beforeAssignment = region.slice(0, assignment.index);
|
|
1382
|
+
const guardMatches = [...beforeAssignment.matchAll(/^([ \\t]*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/gm)];
|
|
1383
|
+
const nearestGuard = guardMatches.at(-1);
|
|
1384
|
+
if (nearestGuard && (nearestGuard[1]?.length ?? 0) < indentation) {
|
|
1385
|
+
const guardIndex = input.parameterNames.indexOf(nearestGuard[2]);
|
|
1386
|
+
if (guardIndex < 0)
|
|
1387
|
+
return undefined;
|
|
1388
|
+
const guardExpression = extractPytestParamTopLevelArgs(input.block)[guardIndex]?.trim();
|
|
1389
|
+
if (guardExpression !== "True")
|
|
1390
|
+
return undefined;
|
|
1391
|
+
}
|
|
1392
|
+
const valueExpression = extractPytestParamTopLevelArgs(input.block)[valueIndex]?.trim();
|
|
1393
|
+
return valueExpression
|
|
1394
|
+
? observeLiteralToken(valueExpression, input.field, input.constants)
|
|
1395
|
+
: undefined;
|
|
1396
|
+
}
|
|
1180
1397
|
function scenarioFunctionRegion(source, tpId) {
|
|
1181
1398
|
const idIndex = source.indexOf(`id="${tpId}"`) >= 0
|
|
1182
1399
|
? source.indexOf(`id="${tpId}"`)
|
|
@@ -1202,6 +1419,43 @@ function hasCreateDeleteDerivedJourney(source, tpId) {
|
|
|
1202
1419
|
(/\b_create[A-Za-z0-9_]*\s*\(/.test(region) &&
|
|
1203
1420
|
(/\b_delete[A-Za-z0-9_]*\s*\(/.test(region) || /\.delete\s*\(/.test(region) || /\.request\s*\(\s*["']DELETE["']/i.test(region)));
|
|
1204
1421
|
}
|
|
1422
|
+
function hasConditionalFieldOmission(source, tpId, field, parameterNames) {
|
|
1423
|
+
if (!field)
|
|
1424
|
+
return false;
|
|
1425
|
+
const region = scenarioFunctionRegion(source, tpId);
|
|
1426
|
+
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1427
|
+
const candidateNames = parameterNames.filter((name) => {
|
|
1428
|
+
const normalized = name.toLowerCase();
|
|
1429
|
+
const normalizedField = field.toLowerCase();
|
|
1430
|
+
return normalized === normalizedField || normalized.startsWith(`${normalizedField}_`) || normalized.endsWith(`_${normalizedField}`);
|
|
1431
|
+
});
|
|
1432
|
+
return candidateNames.some((name) => {
|
|
1433
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1434
|
+
return new RegExp(`\\{\\s*\\}\\s*if\\s+${escapedName}\\s+is\\s+None\\s+else\\s+\\{[\\s\\S]{0,200}?["']${escapedField}["']\\s*:\\s*${escapedName}\\b`).test(region) ||
|
|
1435
|
+
new RegExp(`\\{[\\s\\S]{0,200}?["']${escapedField}["']\\s*:\\s*${escapedName}\\b[\\s\\S]{0,200}?\\}\\s*if\\s+${escapedName}\\s+is\\s+not\\s+None\\s+else\\s+\\{\\s*\\}`).test(region) ||
|
|
1436
|
+
new RegExp(`if\\s+${escapedName}\\s+is\\s+not\\s+None\\s*:[\\s\\S]{0,200}?["']${escapedField}["']\\s*\\]\\s*=\\s*${escapedName}\\b`).test(region);
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
function scenarioParamSourceLocation(scriptPath, source, tpId) {
|
|
1440
|
+
const block = extractPytestParamBlock(source, tpId);
|
|
1441
|
+
if (!block)
|
|
1442
|
+
return undefined;
|
|
1443
|
+
const index = source.indexOf(block);
|
|
1444
|
+
if (index < 0)
|
|
1445
|
+
return undefined;
|
|
1446
|
+
return `${scriptPath}:${source.slice(0, index).split("\n").length}`;
|
|
1447
|
+
}
|
|
1448
|
+
function scenarioParamDiagnosticValues(intent, observed) {
|
|
1449
|
+
const expectedRaw = intent.startsWith("custom-literal:")
|
|
1450
|
+
? intent.slice("custom-literal:".length)
|
|
1451
|
+
: intent === "unknown" ? undefined : intent;
|
|
1452
|
+
return {
|
|
1453
|
+
expectedRaw,
|
|
1454
|
+
observedRaw: boundedScenarioText(observed.text, observed.length),
|
|
1455
|
+
expectedNormalized: expectedRaw === undefined ? undefined : normalizeCustomLiteralExpected(expectedRaw).slice(0, 512),
|
|
1456
|
+
observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), observed.length),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1205
1459
|
export async function assessBackendScenarioParamConsistency(input) {
|
|
1206
1460
|
const cases = await listMarkdownCases(input.workspaceRoot, input.layout);
|
|
1207
1461
|
const entries = [];
|
|
@@ -1223,7 +1477,26 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1223
1477
|
const constants = source ? collectPythonStringConstants(source) : undefined;
|
|
1224
1478
|
const block = source ? extractPytestParamBlock(source, tpId) : undefined;
|
|
1225
1479
|
const parameterNames = source ? extractPytestParamParameterNames(source, tpId) : [];
|
|
1226
|
-
const
|
|
1480
|
+
const functionFieldOverride = source && inferred.field
|
|
1481
|
+
? observeDeterministicFunctionFieldOverride({
|
|
1482
|
+
source,
|
|
1483
|
+
tpId,
|
|
1484
|
+
block,
|
|
1485
|
+
parameterNames,
|
|
1486
|
+
field: inferred.field,
|
|
1487
|
+
constants,
|
|
1488
|
+
})
|
|
1489
|
+
: undefined;
|
|
1490
|
+
const helperFieldObserved = source && inferred.field
|
|
1491
|
+
? observeSameModulePayloadHelperField({
|
|
1492
|
+
source,
|
|
1493
|
+
block,
|
|
1494
|
+
parameterNames,
|
|
1495
|
+
field: inferred.field,
|
|
1496
|
+
constants,
|
|
1497
|
+
})
|
|
1498
|
+
: undefined;
|
|
1499
|
+
const observed = functionFieldOverride ?? helperFieldObserved ?? observeParamFeatures(block, inferred.field, constants, parameterNames);
|
|
1227
1500
|
// Request-level / health checks often have no pytest.param payload row,
|
|
1228
1501
|
// or only a TP-id label positional (no field value).
|
|
1229
1502
|
const requestLevelNoParam = Boolean(source) &&
|
|
@@ -1242,9 +1515,17 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1242
1515
|
hasCreateDeleteDerivedJourney(source, tpId)) ||
|
|
1243
1516
|
(/^custom-literal:(?:create-derived(?:-active-id|-id)?|active-existing-derived)$/i.test(inferred.intent) &&
|
|
1244
1517
|
hasCreateDerivedJourney(source, tpId)));
|
|
1518
|
+
const conditionalOmissionMatch = Boolean(source) &&
|
|
1519
|
+
inferred.intent === "missing" &&
|
|
1520
|
+
observed.kind === "none" &&
|
|
1521
|
+
hasConditionalFieldOmission(source, tpId, inferred.field, parameterNames);
|
|
1522
|
+
const localPayloadHelperMatch = Boolean(source) &&
|
|
1523
|
+
inferred.intent === "nominal" &&
|
|
1524
|
+
!inferred.field &&
|
|
1525
|
+
hasSafeSameModulePayloadHelper(source, block, parameterNames);
|
|
1245
1526
|
const status = !source
|
|
1246
1527
|
? "UNDETERMINED"
|
|
1247
|
-
: derivedJourneyMatch
|
|
1528
|
+
: derivedJourneyMatch || conditionalOmissionMatch || localPayloadHelperMatch
|
|
1248
1529
|
? "MATCH"
|
|
1249
1530
|
: requestLevelNoParam
|
|
1250
1531
|
? "MATCH"
|
|
@@ -1252,14 +1533,19 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1252
1533
|
? "UNDETERMINED"
|
|
1253
1534
|
: compareIntent(inferred.intent, observed, inferred.bound, inferred.example, inferred.field);
|
|
1254
1535
|
const repairability = repairabilityFor(status, inferred.intent, observed, inferred.example);
|
|
1536
|
+
const diagnostics = scenarioParamDiagnosticValues(inferred.intent, observed);
|
|
1255
1537
|
entries.push({
|
|
1256
1538
|
caseId: testCase.caseId,
|
|
1257
1539
|
tpId,
|
|
1258
1540
|
field: inferred.field,
|
|
1259
1541
|
intent: inferred.intent,
|
|
1260
|
-
observed: observed.text,
|
|
1542
|
+
observed: boundedScenarioText(observed.text, observed.length),
|
|
1543
|
+
observedLength: observed.length,
|
|
1261
1544
|
status,
|
|
1262
1545
|
repairability,
|
|
1546
|
+
reasonCode: status === "MATCH" ? "SCENARIO_PARAM_MATCH" : status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
|
|
1547
|
+
...diagnostics,
|
|
1548
|
+
sourceLocation: source ? scenarioParamSourceLocation(testCase.scriptPath, source, tpId) : undefined,
|
|
1263
1549
|
suggestedFix: fallbackSourced
|
|
1264
1550
|
? undefined
|
|
1265
1551
|
: suggestedFixFor(inferred.intent, inferred.field, inferred.bound, inferred.example),
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { BACKEND_TEST_MAX_MODULE_COUNT, isOpaqueHashBackendTestModuleStem, isPriorityOnlyBackendTestModuleStem, } from "./backend-test-module-stem.js";
|
|
6
6
|
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
7
|
+
import { inspectBackendTestMarkdownTestPointBindings } from "./backend-test-case-coverage-analysis.js";
|
|
7
8
|
/**
|
|
8
9
|
* Maps a backend-test generation writer task id to its writer-progress role.
|
|
9
10
|
* Returns undefined for nodes that are not completeness-gated generators.
|
|
@@ -276,6 +277,16 @@ function moduleStructurallyComplete(markdown) {
|
|
|
276
277
|
reasons: [`missing required sections: [${missing.join(", ")}]`],
|
|
277
278
|
};
|
|
278
279
|
}
|
|
280
|
+
const bindingReasons = inspectBackendTestMarkdownTestPointBindings(markdown).flatMap((entry) => [
|
|
281
|
+
...(entry.unclassifiedTestPoints.length > 0
|
|
282
|
+
? [`${entry.caseId} has unclassified Test Points: ${entry.unclassifiedTestPoints.join(", ")}`]
|
|
283
|
+
: []),
|
|
284
|
+
...(entry.duplicateBindingTestPoints.length > 0
|
|
285
|
+
? [`${entry.caseId} has duplicate Test Point bindings: ${entry.duplicateBindingTestPoints.join(", ")}`]
|
|
286
|
+
: []),
|
|
287
|
+
]);
|
|
288
|
+
if (bindingReasons.length > 0)
|
|
289
|
+
return { ok: false, reasons: bindingReasons };
|
|
279
290
|
return { ok: true, reasons: [] };
|
|
280
291
|
}
|
|
281
292
|
function moduleStructuralDetail(result) {
|
|
@@ -884,28 +895,23 @@ export function isBackendTestShardChildTask(task) {
|
|
|
884
895
|
task.id?.startsWith("generate-backend-pytest-case-"));
|
|
885
896
|
}
|
|
886
897
|
/**
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
* must NOT require module files to exist at plan time.
|
|
898
|
+
* Historical classifier for the Markdown plan node. Current backend-test N2
|
|
899
|
+
* is read-only and persists a run-owned plan.md, so writer completeness never
|
|
900
|
+
* invokes the workspace README assessment below for newly generated DAGs.
|
|
891
901
|
*/
|
|
892
902
|
export function isBackendTestMdPlanTask(task) {
|
|
893
903
|
return task.id === "generate-backend-md-plan-pi";
|
|
894
904
|
}
|
|
895
905
|
/**
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
* scripts are produced by the downstream map_agent children, so the Gate
|
|
899
|
-
* must NOT require mapped pytest scripts to exist at plan time.
|
|
906
|
+
* Historical classifier for the former shared-resources pytest writer. Current
|
|
907
|
+
* N7 is static and every map child writes one self-contained pytest module.
|
|
900
908
|
*/
|
|
901
909
|
export function isBackendTestPytestPlanTask(task) {
|
|
902
910
|
return task.id === "generate-backend-pytest-plan-pi";
|
|
903
911
|
}
|
|
904
912
|
/**
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
* index), WITHOUT requiring the indexed module files to exist yet — those are
|
|
908
|
-
* written by the downstream map_agent shard children.
|
|
913
|
+
* Legacy workspace-plan completeness helper retained for isolated historical
|
|
914
|
+
* fixtures. New DAGs validate the run-owned plan artifact in N3 instead.
|
|
909
915
|
*/
|
|
910
916
|
export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout) {
|
|
911
917
|
const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);
|