@tea-agent/loop-agent 0.40.0-next.9 → 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 -21
- 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/pi-sdk-executor.js +33 -5
- 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-Bs-CDAXM.js → abnfDiagram-N423BO3Z-Dy71QJp3.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-M25fNMTE.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-CUWlVl1a.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-Dd0Whpx2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.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-shmQKsFz.js → chunk-2GRJ4B5K-D_1sUkyD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-Cxnxf36E.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-D9RG3G_x.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-Zz9jgjLX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-BCggDRCH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-CFg-5iAS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-DRQTE8x_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-C_Dw7uZ8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-BjIiApsk.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.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-CkHG6W3s.js → cose-bilkent-JH36ORCC-huHqH6WQ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-BSi6yZqP.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Bs_Jp31y.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-CYxavikT.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-B6TErl71.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-CvH4J2ii.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-v1NBAqXS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-CUNHVAn2.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-Dzz1gUu-.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-CCF8_LjK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-DObdPVLa.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-DlN8Pxrj.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-BJKIPTGX.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.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-DYNx_IPv.js → infoDiagram-6WML65LV-BKrRKMww.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-CN5dk-yb.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-B4P4KZ-Y.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-BRlteP32.js} +1 -1
- package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-CG8D9fuL.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-BuB6P2rX.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-DYXGl_DR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-BM48yEVG.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-mT8NL-Ow.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-C4pZ6DH_.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-BKAcr70m.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-CRxcKpoO.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-Ds4LxQDI.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-CPLwPf_p.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-CL65uij0.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.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-YcF12FiU.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-ClnRcg2s.js → timeline-definition-Z64GVDOM-Dw85f9L2.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-B4QLCqCy.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-B_yKCWwn.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.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/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 +11 -0
- package/dist/workflows/dag/init-hybrid.js +5 -4
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/backend-test-dag.json +6 -5
- 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-amxUpk7o.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
|
@@ -8,6 +8,7 @@ import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
|
8
8
|
import { resolveBackendTestManifestModules, backendManifestStatusFinding } from "./backend-test-markdown-workflow.js";
|
|
9
9
|
import { applyOpenApiPartitionDomainPolicy, assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
|
|
10
10
|
import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
|
|
11
|
+
import { inferScenarioParamIntent } from "./backend-test-scenario-param.js";
|
|
11
12
|
const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
|
|
12
13
|
const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
|
|
13
14
|
const NON_CANONICAL_CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-(?:\d{2}|\d{2,3}[A-Z]+))\b.*$/gm;
|
|
@@ -281,6 +282,80 @@ export const backendTestMarkdownPytestCorrespondenceFactsSchema = z.object({
|
|
|
281
282
|
entries: z.array(correspondenceEntrySchema),
|
|
282
283
|
findings: z.array(z.string()),
|
|
283
284
|
}).strict();
|
|
285
|
+
function bindingCounts(entry) {
|
|
286
|
+
const counts = new Map();
|
|
287
|
+
for (const point of [...entry.variantTestPoints, ...entry.assertionTestPoints, ...entry.crossCuttingTestPoints]) {
|
|
288
|
+
counts.set(point, (counts.get(point) ?? 0) + 1);
|
|
289
|
+
}
|
|
290
|
+
return counts;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Classify correspondence defects by the only writer that can safely repair
|
|
294
|
+
* them. Markdown-owned contract defects must be fixed before pytest generation;
|
|
295
|
+
* pytest-owned generated-asset defects may enter the single bounded N11 repair.
|
|
296
|
+
*/
|
|
297
|
+
export function classifyBackendTestCorrespondenceRepair(facts, layout) {
|
|
298
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
299
|
+
const scriptRoot = resolved.scriptDir.replace(/\/$/, "");
|
|
300
|
+
const safePytestPath = (candidate) => {
|
|
301
|
+
const normalized = candidate.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
302
|
+
return normalized.endsWith(".py") && (normalized === scriptRoot || normalized.startsWith(`${scriptRoot}/`));
|
|
303
|
+
};
|
|
304
|
+
const markdownBlockingEntries = [];
|
|
305
|
+
const pytestRepairEntries = [];
|
|
306
|
+
for (const entry of facts.entries) {
|
|
307
|
+
const markdownReasons = [];
|
|
308
|
+
const pytestReasons = [];
|
|
309
|
+
const counts = bindingCounts(entry);
|
|
310
|
+
if (entry.testPoints.some((point) => (counts.get(point) ?? 0) !== 1) ||
|
|
311
|
+
[...counts.keys()].some((point) => !entry.testPoints.includes(point))) {
|
|
312
|
+
markdownReasons.push("markdown-test-point-binding-invalid");
|
|
313
|
+
}
|
|
314
|
+
if (entry.declaredScript && entry.declaredScript !== entry.expectedScript) {
|
|
315
|
+
markdownReasons.push("markdown-script-declaration-invalid");
|
|
316
|
+
}
|
|
317
|
+
if (entry.cardinality === "1:0")
|
|
318
|
+
pytestReasons.push("missing-pytest-symbol");
|
|
319
|
+
if (entry.cardinality === "1:N")
|
|
320
|
+
pytestReasons.push("multiple-pytest-symbols");
|
|
321
|
+
if (entry.cardinality === "0:1")
|
|
322
|
+
pytestReasons.push("extra-pytest-symbol");
|
|
323
|
+
if (entry.actualScripts.some((script) => script !== entry.expectedScript))
|
|
324
|
+
pytestReasons.push("pytest-script-mismatch");
|
|
325
|
+
if (entry.declaredPrimarySymbol && entry.pytestSymbols.some((symbol) => symbol !== entry.declaredPrimarySymbol)) {
|
|
326
|
+
pytestReasons.push("pytest-primary-symbol-mismatch");
|
|
327
|
+
}
|
|
328
|
+
if (entry.variantTestPoints.some((point) => !entry.parameterIds.includes(point)))
|
|
329
|
+
pytestReasons.push("variant-parameter-missing");
|
|
330
|
+
if (entry.assertionTestPoints.some((point) => !entry.mappedTestPoints.includes(point)))
|
|
331
|
+
pytestReasons.push("assertion-binding-missing");
|
|
332
|
+
if (entry.crossCuttingTestPoints.some((point) => !entry.mappedTestPoints.includes(point)))
|
|
333
|
+
pytestReasons.push("cross-cutting-binding-missing");
|
|
334
|
+
if (entry.parameterIds.some((point) => !entry.testPoints.includes(point)))
|
|
335
|
+
pytestReasons.push("pytest-test-point-binding-extra");
|
|
336
|
+
if (entry.payloadAssessment.status === "UNSAFE")
|
|
337
|
+
pytestReasons.push("pytest-payload-shape-mismatch");
|
|
338
|
+
if (entry.status !== "EXACT_1_TO_1" &&
|
|
339
|
+
!["TEST_POINT_BINDING_DUPLICATE", "PAYLOAD_CONTRACT_UNAVAILABLE"].includes(entry.status)) {
|
|
340
|
+
pytestReasons.push(`correspondence-${entry.status.toLowerCase().replaceAll("_", "-")}`);
|
|
341
|
+
}
|
|
342
|
+
if (entry.status === "PAYLOAD_CONTRACT_UNAVAILABLE") {
|
|
343
|
+
markdownReasons.push("markdown-payload-contract-unavailable");
|
|
344
|
+
}
|
|
345
|
+
const paths = orderedUnique([entry.expectedScript, ...entry.actualScripts].filter(safePytestPath));
|
|
346
|
+
if (markdownReasons.length > 0) {
|
|
347
|
+
markdownBlockingEntries.push({ ...(entry.caseId ? { caseId: entry.caseId } : {}), reasonCodes: orderedUnique(markdownReasons), paths });
|
|
348
|
+
}
|
|
349
|
+
if (pytestReasons.length > 0 && paths.length > 0) {
|
|
350
|
+
pytestRepairEntries.push({ ...(entry.caseId ? { caseId: entry.caseId } : {}), reasonCodes: orderedUnique(pytestReasons), paths });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
markdownBlockingEntries,
|
|
355
|
+
pytestRepairEntries,
|
|
356
|
+
pytestRepairPaths: orderedUnique(pytestRepairEntries.flatMap((entry) => entry.paths)).sort(),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
284
359
|
function isRecord(value) {
|
|
285
360
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
286
361
|
}
|
|
@@ -593,6 +668,19 @@ function automationLabelBody(body, labels) {
|
|
|
593
668
|
const match = new RegExp(`^\\s*[-*+]\\s*(?:${escaped})\\s*[::]\\s*(.*)$`, "mi").exec(automation);
|
|
594
669
|
return match?.[1]?.replaceAll("`", "").trim() ?? "";
|
|
595
670
|
}
|
|
671
|
+
export function inspectBackendTestMarkdownTestPointBindings(markdown) {
|
|
672
|
+
const headings = [...markdown.matchAll(CASE_HEADING)];
|
|
673
|
+
return headings.map((heading, index) => {
|
|
674
|
+
const body = markdown.slice(heading.index, headings[index + 1]?.index ?? markdown.length);
|
|
675
|
+
const testPoints = listTokens(body, CASE_SECTION_ALIASES.testPoints, TEST_POINT);
|
|
676
|
+
const bindings = testPointBindingFacts(body, testPoints);
|
|
677
|
+
return {
|
|
678
|
+
caseId: canonicalCaseId(heading[1]),
|
|
679
|
+
unclassifiedTestPoints: bindings.unclassifiedTestPoints,
|
|
680
|
+
duplicateBindingTestPoints: bindings.duplicateBindingTestPoints,
|
|
681
|
+
};
|
|
682
|
+
});
|
|
683
|
+
}
|
|
596
684
|
function testPointBindingFacts(body, testPoints) {
|
|
597
685
|
const bindings = TEST_POINT_BINDING_MODES.flatMap((mode) => {
|
|
598
686
|
const value = automationLabelBody(body, AUTOMATION_BINDING_LABELS[mode]);
|
|
@@ -627,7 +715,11 @@ function payloadContract(body) {
|
|
|
627
715
|
const requiredValue = automationLabelBody(body, ["Payload Required Paths", "载荷必需路径"]);
|
|
628
716
|
const allowedValue = automationLabelBody(body, ["Payload Allowed Paths", "载荷允许路径"]);
|
|
629
717
|
const enumValue = automationLabelBody(body, ["Payload Enum", "载荷枚举"]);
|
|
630
|
-
const tokens = (value) => orderedUnique(value
|
|
718
|
+
const tokens = (value) => orderedUnique(value
|
|
719
|
+
.split(/[,;,;]/)
|
|
720
|
+
.map((item) => item.trim())
|
|
721
|
+
.filter((item) => !/^(?:none|无|not-applicable|n\/a)$/i.test(item))
|
|
722
|
+
.filter((item) => /^[A-Za-z_][A-Za-z0-9_.]*$/.test(item)));
|
|
631
723
|
const enumValues = {};
|
|
632
724
|
for (const assignment of enumValue.split(/[,;,;]/).map((item) => item.trim()).filter(Boolean)) {
|
|
633
725
|
const match = /^([A-Za-z_][A-Za-z0-9_.]*)\s*(?:=|:)\s*(.+)$/.exec(assignment);
|
|
@@ -1404,6 +1496,11 @@ def payload_expr_nodes(expr, assigned, parameters, seen=None):
|
|
|
1404
1496
|
if isinstance(key,ast.Constant) and key.value == expr.slice.value:
|
|
1405
1497
|
result.extend(shape_only(item) for item in payload_expr_nodes(value,assigned,parameters,set(seen)))
|
|
1406
1498
|
return result
|
|
1499
|
+
if isinstance(expr,ast.IfExp):
|
|
1500
|
+
result=[]
|
|
1501
|
+
result.extend(payload_expr_nodes(expr.body,assigned,parameters,set(seen)))
|
|
1502
|
+
result.extend(payload_expr_nodes(expr.orelse,assigned,parameters,set(seen)))
|
|
1503
|
+
return result
|
|
1407
1504
|
if isinstance(expr,ast.Call):
|
|
1408
1505
|
result=[]
|
|
1409
1506
|
if isinstance(expr.func,ast.Name) and expr.func.id in FUNCTION_PAYLOADS:
|
|
@@ -1444,6 +1541,30 @@ def decorator_payload_nodes(parameters,assigned):
|
|
|
1444
1541
|
for value in values: result.extend(payload_expr_nodes(value,assigned,parameters))
|
|
1445
1542
|
return result
|
|
1446
1543
|
|
|
1544
|
+
def decorator_payload_nodes_by_tp(fn,assigned):
|
|
1545
|
+
result={}
|
|
1546
|
+
for dec in fn.decorator_list:
|
|
1547
|
+
if not isinstance(dec,ast.Call) or not isinstance(dec.func,ast.Attribute) or dec.func.attr != 'parametrize' or len(dec.args) < 2: continue
|
|
1548
|
+
if not isinstance(dec.args[0],ast.Constant) or not isinstance(dec.args[0].value,str): continue
|
|
1549
|
+
names=[item.strip() for item in dec.args[0].value.split(',')]
|
|
1550
|
+
rows=dec.args[1]
|
|
1551
|
+
if isinstance(rows,ast.Name) and rows.id in GLOBAL_VALUES: rows=GLOBAL_VALUES[rows.id]
|
|
1552
|
+
for node in ast.walk(rows):
|
|
1553
|
+
if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute) or node.func.attr != 'param': continue
|
|
1554
|
+
tp=None
|
|
1555
|
+
for keyword in node.keywords:
|
|
1556
|
+
if keyword.arg == 'id' and isinstance(keyword.value,ast.Constant) and isinstance(keyword.value.value,str) and keyword.value.value.startswith('TP-'):
|
|
1557
|
+
tp=keyword.value.value
|
|
1558
|
+
if not tp: continue
|
|
1559
|
+
parameters={}
|
|
1560
|
+
for index,name in enumerate(names):
|
|
1561
|
+
if index < len(node.args): parameters.setdefault(name,[]).append(node.args[index])
|
|
1562
|
+
payload_nodes=decorator_payload_nodes(parameters,assigned)
|
|
1563
|
+
paths=set(); values={}
|
|
1564
|
+
for payload_node in payload_nodes: collect_dict(payload_node,'',paths,values)
|
|
1565
|
+
result[tp]={key:sorted(items) for key,items in sorted(values.items())}
|
|
1566
|
+
return result
|
|
1567
|
+
|
|
1447
1568
|
def fallback_payload_nodes(fn):
|
|
1448
1569
|
result=[]
|
|
1449
1570
|
assigned={}
|
|
@@ -1473,7 +1594,7 @@ def fallback_payload_nodes(fn):
|
|
|
1473
1594
|
|
|
1474
1595
|
def is_direct_transport_call(node):
|
|
1475
1596
|
if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute): return False
|
|
1476
|
-
if node.func.attr.lower() not in {'get','post','put','patch','delete','request'}: return False
|
|
1597
|
+
if node.func.attr.lower() not in {'get','post','put','patch','delete','request','call'}: return False
|
|
1477
1598
|
receiver_tokens=[]
|
|
1478
1599
|
current=node.func.value
|
|
1479
1600
|
while isinstance(current,ast.Attribute):
|
|
@@ -1521,6 +1642,7 @@ for fn in ast.walk(tree):
|
|
|
1521
1642
|
'values':{key:sorted(items) for key,items in sorted(values.items())},
|
|
1522
1643
|
'fallbackPaths':sorted(fallback_paths),
|
|
1523
1644
|
'fallbackValues':{key:sorted(items) for key,items in sorted(fallback_values.items())},
|
|
1645
|
+
'valuesByTestPoint':decorator_payload_nodes_by_tp(fn,{}),
|
|
1524
1646
|
}
|
|
1525
1647
|
print(json.dumps(out,ensure_ascii=True))
|
|
1526
1648
|
`;
|
|
@@ -1651,6 +1773,7 @@ function pytestSymbols(script, source) {
|
|
|
1651
1773
|
payloadValues: payloadShape.values,
|
|
1652
1774
|
fallbackPayloadPaths: payloadShape.fallbackPaths ?? [],
|
|
1653
1775
|
fallbackPayloadValues: payloadShape.fallbackValues ?? {},
|
|
1776
|
+
payloadValuesByTestPoint: payloadShape.valuesByTestPoint ?? {},
|
|
1654
1777
|
};
|
|
1655
1778
|
});
|
|
1656
1779
|
}
|
|
@@ -1664,6 +1787,33 @@ function executionSignature(testCase) {
|
|
|
1664
1787
|
.join("\n");
|
|
1665
1788
|
return createHash("sha256").update(normalized).digest("hex");
|
|
1666
1789
|
}
|
|
1790
|
+
function normalizePythonLiteralValue(value) {
|
|
1791
|
+
const trimmed = value.trim();
|
|
1792
|
+
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
1793
|
+
return trimmed.slice(1, -1);
|
|
1794
|
+
}
|
|
1795
|
+
return trimmed;
|
|
1796
|
+
}
|
|
1797
|
+
function intentionalInvalidEnumVariant(input) {
|
|
1798
|
+
if (!input.testCase.testPointBindings.some((binding) => binding.mode === "variant" && binding.testPoint === input.testPoint))
|
|
1799
|
+
return false;
|
|
1800
|
+
const inferred = inferScenarioParamIntent({ tpId: input.testPoint, caseBody: input.testCase.body });
|
|
1801
|
+
if (inferred.intentSource !== "machine-line" || inferred.field?.toLowerCase() !== input.field.toLowerCase())
|
|
1802
|
+
return false;
|
|
1803
|
+
const observed = normalizePythonLiteralValue(input.observedValue);
|
|
1804
|
+
if (input.allowed.includes(observed))
|
|
1805
|
+
return false;
|
|
1806
|
+
if (inferred.intent === "enum-invalid" || inferred.intent === "wrong-type")
|
|
1807
|
+
return true;
|
|
1808
|
+
if (inferred.intent === "null")
|
|
1809
|
+
return observed === "None" || observed === "null";
|
|
1810
|
+
if (inferred.intent === "empty")
|
|
1811
|
+
return observed === "";
|
|
1812
|
+
if (!inferred.intent.startsWith("custom-literal:"))
|
|
1813
|
+
return false;
|
|
1814
|
+
const expected = normalizePythonLiteralValue(inferred.example ?? inferred.intent.slice("custom-literal:".length));
|
|
1815
|
+
return observed === expected;
|
|
1816
|
+
}
|
|
1667
1817
|
function assessPayloadContract(testCase, refs) {
|
|
1668
1818
|
const requiredPaths = testCase.payloadContract.requiredPaths;
|
|
1669
1819
|
const directObservedPaths = orderedUnique(refs.flatMap((item) => item.payloadPaths));
|
|
@@ -1685,13 +1835,17 @@ function assessPayloadContract(testCase, refs) {
|
|
|
1685
1835
|
? observedPaths.filter((item) => !allowedPaths.includes(item) && !allowedPaths.some((allowed) => item.startsWith(`${allowed}.`)))
|
|
1686
1836
|
: [];
|
|
1687
1837
|
const enumMismatches = [];
|
|
1688
|
-
const delegatesInvalidEnumToScenarioGate = testCase.testPointBindings
|
|
1689
|
-
.filter((binding) => binding.mode === "variant")
|
|
1690
|
-
.some((binding) => /(?:ENUM|CATEGORY).*(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE)|(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE).*(?:ENUM|CATEGORY)/.test(binding.testPoint));
|
|
1691
1838
|
for (const [field, allowed] of Object.entries(testCase.payloadContract.enumValues)) {
|
|
1692
|
-
const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? [])
|
|
1693
|
-
for (const
|
|
1694
|
-
|
|
1839
|
+
const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []));
|
|
1840
|
+
for (const rawValue of observed) {
|
|
1841
|
+
const value = normalizePythonLiteralValue(rawValue);
|
|
1842
|
+
if (allowed.includes(value))
|
|
1843
|
+
continue;
|
|
1844
|
+
const associatedTestPoints = orderedUnique(refs.flatMap((item) => Object.entries(item.payloadValuesByTestPoint)
|
|
1845
|
+
.filter(([, values]) => (values[field] ?? []).includes(rawValue))
|
|
1846
|
+
.map(([testPoint]) => testPoint)));
|
|
1847
|
+
const intentionallyInvalid = associatedTestPoints.length > 0 && associatedTestPoints.every((testPoint) => intentionalInvalidEnumVariant({ testCase, testPoint, field, observedValue: rawValue, allowed }));
|
|
1848
|
+
if (!intentionallyInvalid)
|
|
1695
1849
|
enumMismatches.push(`${field}=${value}; allowed=${allowed.join("|")}`);
|
|
1696
1850
|
}
|
|
1697
1851
|
}
|
|
@@ -20,6 +20,14 @@ export const backendPytestCollectionFindingSchema = z.object({
|
|
|
20
20
|
classification: z.literal("test-asset-defect"),
|
|
21
21
|
repairability: z.enum(["repairable", "blocked"]),
|
|
22
22
|
detail: z.string().min(1),
|
|
23
|
+
caseId: z.string().min(1).optional(),
|
|
24
|
+
reasonCodes: z.array(z.string().min(1)).optional(),
|
|
25
|
+
repairPaths: z.array(z.string().min(1)).optional(),
|
|
26
|
+
payloadExpectedPaths: z.array(z.string()).optional(),
|
|
27
|
+
payloadObservedPaths: z.array(z.string()).optional(),
|
|
28
|
+
payloadMissingPaths: z.array(z.string()).optional(),
|
|
29
|
+
payloadUnexpectedPaths: z.array(z.string()).optional(),
|
|
30
|
+
payloadEnumMismatches: z.array(z.string()).optional(),
|
|
23
31
|
}).strict();
|
|
24
32
|
export const backendPytestCollectionFactsSchema = z.object({
|
|
25
33
|
schemaId: z.literal("backend-test-pytest-collection-v3"),
|
|
@@ -61,11 +69,22 @@ export const backendPytestCollectionFactsSchema = z.object({
|
|
|
61
69
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "backend pytest fixture resolution without an attempt cannot have an exit code" });
|
|
62
70
|
}
|
|
63
71
|
});
|
|
72
|
+
const backendTestScenarioParamDiagnosticSchema = z.object({
|
|
73
|
+
reasonCode: z.enum(["SCENARIO_PARAM_MISMATCH", "SCENARIO_PARAM_UNDETERMINED"]),
|
|
74
|
+
tpId: z.string().min(1).max(256),
|
|
75
|
+
field: z.string().min(1).max(256).optional(),
|
|
76
|
+
expectedRaw: z.string().max(512).optional(),
|
|
77
|
+
observedRaw: z.string().max(512),
|
|
78
|
+
expectedNormalized: z.string().max(512).optional(),
|
|
79
|
+
observedNormalized: z.string().max(512).optional(),
|
|
80
|
+
sourceLocation: z.string().max(512).optional(),
|
|
81
|
+
}).strict();
|
|
64
82
|
const backendTestExcludedItemSchema = z.object({
|
|
65
83
|
itemId: z.string().min(1),
|
|
66
84
|
caseId: z.string().optional(),
|
|
67
85
|
symbol: z.string().optional(),
|
|
68
86
|
reasons: z.array(z.string().min(1)).min(1),
|
|
87
|
+
scenarioParamDiagnostics: z.array(backendTestScenarioParamDiagnosticSchema).max(16).optional(),
|
|
69
88
|
}).strict();
|
|
70
89
|
export const backendTestExecutionReadinessSchema = z.object({
|
|
71
90
|
schemaId: z.literal("backend-test-execution-readiness-v2"),
|
|
@@ -81,6 +100,14 @@ export const backendTestExecutionReadinessSchema = z.object({
|
|
|
81
100
|
collectedItemIds: z.array(z.string()),
|
|
82
101
|
eligibleItemIds: z.array(z.string()),
|
|
83
102
|
excludedItems: z.array(backendTestExcludedItemSchema),
|
|
103
|
+
exclusionSummary: z.object({
|
|
104
|
+
excludedItemCount: z.number().int().min(0),
|
|
105
|
+
excludedCaseCount: z.number().int().min(0),
|
|
106
|
+
excludedByReason: z.record(z.string(), z.number().int().min(1)),
|
|
107
|
+
excludedByCase: z.record(z.string(), z.number().int().min(1)),
|
|
108
|
+
}).strict(),
|
|
109
|
+
pytestSpawned: z.literal(false),
|
|
110
|
+
businessTestBodyExecutedCount: z.literal(0),
|
|
84
111
|
fixtureIssues: z.array(z.string()),
|
|
85
112
|
assetHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
86
113
|
eligibilityInputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
@@ -702,6 +729,7 @@ export function buildBackendTestItemEligibility(collectedItemIds, input) {
|
|
|
702
729
|
const parameterTokens = orderedUnique((itemId.match(/TP-[A-Z0-9-]+/g) ?? []));
|
|
703
730
|
const mapping = input.correspondenceEntries.find((entry) => symbol && (entry.declaredPrimarySymbol === symbol || entry.pytestSymbols.includes(symbol)));
|
|
704
731
|
const reasons = [];
|
|
732
|
+
const scenarioParamDiagnostics = [];
|
|
705
733
|
if (!mapping)
|
|
706
734
|
reasons.push("no exact Markdown Case/primary-symbol correspondence");
|
|
707
735
|
if (mapping && mapping.status !== "EXACT_1_TO_1")
|
|
@@ -724,13 +752,30 @@ export function buildBackendTestItemEligibility(collectedItemIds, input) {
|
|
|
724
752
|
// or observed Python payload shape.
|
|
725
753
|
const normalizedField = entry.field?.toLowerCase();
|
|
726
754
|
const fieldIsPayloadBound = Boolean(normalizedField && payloadPaths.some((item) => item === normalizedField || item.endsWith(`.${normalizedField}`)));
|
|
727
|
-
if (fieldIsPayloadBound && entry.status !== "MATCH")
|
|
755
|
+
if (fieldIsPayloadBound && entry.status !== "MATCH") {
|
|
728
756
|
reasons.push(`scenario-param ${entry.tpId} field ${entry.field} is ${entry.status}`);
|
|
757
|
+
scenarioParamDiagnostics.push({
|
|
758
|
+
reasonCode: entry.status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
|
|
759
|
+
tpId: entry.tpId,
|
|
760
|
+
...(entry.field ? { field: entry.field } : {}),
|
|
761
|
+
...(entry.expectedRaw !== undefined ? { expectedRaw: entry.expectedRaw.slice(0, 512) } : {}),
|
|
762
|
+
observedRaw: (entry.observedRaw ?? "unavailable").slice(0, 512),
|
|
763
|
+
...(entry.expectedNormalized !== undefined ? { expectedNormalized: entry.expectedNormalized.slice(0, 512) } : {}),
|
|
764
|
+
...(entry.observedNormalized !== undefined ? { observedNormalized: entry.observedNormalized.slice(0, 512) } : {}),
|
|
765
|
+
...(entry.sourceLocation ? { sourceLocation: entry.sourceLocation.slice(0, 512) } : {}),
|
|
766
|
+
});
|
|
767
|
+
}
|
|
729
768
|
}
|
|
730
769
|
if (reasons.length === 0)
|
|
731
770
|
eligibleItemIds.push(itemId);
|
|
732
771
|
else
|
|
733
|
-
excludedItems.push({
|
|
772
|
+
excludedItems.push({
|
|
773
|
+
itemId,
|
|
774
|
+
...(mapping?.caseId ? { caseId: mapping.caseId } : {}),
|
|
775
|
+
...(symbol ? { symbol } : {}),
|
|
776
|
+
reasons: orderedUnique(reasons),
|
|
777
|
+
...(scenarioParamDiagnostics.length > 0 ? { scenarioParamDiagnostics: scenarioParamDiagnostics.slice(0, 16) } : {}),
|
|
778
|
+
});
|
|
734
779
|
}
|
|
735
780
|
return { eligibleItemIds, excludedItems };
|
|
736
781
|
}
|
|
@@ -763,6 +808,9 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
763
808
|
collectedItemIds: input.effective.collectedItemIds,
|
|
764
809
|
eligibleItemIds: eligibility.eligibleItemIds,
|
|
765
810
|
excludedItems: eligibility.excludedItems,
|
|
811
|
+
exclusionSummary: summarizeBackendTestExclusions(eligibility.excludedItems),
|
|
812
|
+
pytestSpawned: false,
|
|
813
|
+
businessTestBodyExecutedCount: 0,
|
|
766
814
|
fixtureIssues: input.effective.findings.filter((item) => /fixture/i.test(item.kind)).map((item) => item.detail),
|
|
767
815
|
assetHashes: input.effective.inputHashes,
|
|
768
816
|
eligibilityInputHashes: input.eligibilityInputHashes ?? {},
|
|
@@ -778,6 +826,26 @@ export async function readBackendTestExecutionReadiness(filePath) {
|
|
|
778
826
|
function formatReadinessCounts(readiness) {
|
|
779
827
|
return `mapped=${readiness.mappedScripts.length} collected=${readiness.collectedItemIds.length} eligible=${readiness.eligibleItemIds.length} excluded=${readiness.excludedItems.length}`;
|
|
780
828
|
}
|
|
829
|
+
export function summarizeBackendTestExclusions(excludedItems) {
|
|
830
|
+
const reasonCounts = new Map();
|
|
831
|
+
const caseCounts = new Map();
|
|
832
|
+
for (const item of excludedItems) {
|
|
833
|
+
if (item.caseId)
|
|
834
|
+
caseCounts.set(item.caseId, (caseCounts.get(item.caseId) ?? 0) + 1);
|
|
835
|
+
for (const reason of item.reasons) {
|
|
836
|
+
const category = classifyExclusionReason(reason);
|
|
837
|
+
reasonCounts.set(category, (reasonCounts.get(category) ?? 0) + 1);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
excludedItemCount: excludedItems.length,
|
|
842
|
+
excludedCaseCount: caseCounts.size,
|
|
843
|
+
excludedByReason: Object.fromEntries(READINESS_EXCLUSION_CATEGORIES
|
|
844
|
+
.filter((category) => (reasonCounts.get(category) ?? 0) > 0)
|
|
845
|
+
.map((category) => [category, reasonCounts.get(category)])),
|
|
846
|
+
excludedByCase: Object.fromEntries([...caseCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
781
849
|
export function classifyExclusionReason(reason) {
|
|
782
850
|
if (reason === "no exact Markdown Case/primary-symbol correspondence")
|
|
783
851
|
return "correspondence-missing";
|
|
@@ -70,6 +70,7 @@ export const backendTestResultContractSchema = z
|
|
|
70
70
|
schemaVersion: z.literal(1),
|
|
71
71
|
/** Process-level status of the pytest invocation. Task Pool: use with outcome. */
|
|
72
72
|
executionStatus: backendTestExecutionStatusSchema,
|
|
73
|
+
pytestSpawned: z.literal(true),
|
|
73
74
|
pytestExitCode: z.number().int().min(0).max(255),
|
|
74
75
|
collectionStatus: backendTestCollectionStatusSchema,
|
|
75
76
|
/** Total tests counted from JUnit (or 0 when report unavailable). */
|
|
@@ -659,6 +660,7 @@ export function deriveBackendTestResult(input) {
|
|
|
659
660
|
const result = {
|
|
660
661
|
schemaVersion: 1,
|
|
661
662
|
executionStatus,
|
|
663
|
+
pytestSpawned: true,
|
|
662
664
|
pytestExitCode: input.pytestExitCode,
|
|
663
665
|
collectionStatus: "unknown",
|
|
664
666
|
tests: 0,
|
|
@@ -733,6 +735,7 @@ export function deriveBackendTestResult(input) {
|
|
|
733
735
|
const result = {
|
|
734
736
|
schemaVersion: 1,
|
|
735
737
|
executionStatus,
|
|
738
|
+
pytestSpawned: true,
|
|
736
739
|
pytestExitCode: exit,
|
|
737
740
|
collectionStatus,
|
|
738
741
|
tests: parsed.tests,
|
|
@@ -931,11 +934,12 @@ export async function materializeBackendTestResultFromPytestHtml(input) {
|
|
|
931
934
|
outcome = "command-error";
|
|
932
935
|
}
|
|
933
936
|
const commandSummary = input.commandSummary ??
|
|
934
|
-
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -
|
|
937
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python reports/backend-test-pytest-launcher.py reports/backend-test-pytest-targets.txt ${htmlRelativePath} -> pytest.main(-v -p no:cacheprovider --self-contained-html)`;
|
|
935
938
|
assertNoSecrets("commandSummary", commandSummary);
|
|
936
939
|
const result = backendTestResultContractSchema.parse({
|
|
937
940
|
schemaVersion: 1,
|
|
938
941
|
executionStatus,
|
|
942
|
+
pytestSpawned: true,
|
|
939
943
|
pytestExitCode: exit,
|
|
940
944
|
collectionStatus,
|
|
941
945
|
tests: parsed.tests,
|