@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.11
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 +114 -109
- package/README.md +2 -2
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/executors/dag-pi-executor.js +69 -7
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +46 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +107 -20
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/governance/checks.js +1 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +10 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +474 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +19 -3
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +133 -7
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +312 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +583 -172
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
- package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +29 -9
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +203 -0
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +174 -2
- package/dist/worker/observe/static/styles.css +357 -35
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +533 -79
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +6 -4
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +186 -8
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +168 -16
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +28 -17
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- package/dist/workflows/dag/init-hybrid.js +239 -52
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +45 -0
- package/dist/workflows/dag/types.js +62 -12
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +267 -238
- package/docs/templates/frontend-implementation-contract.schema.json +681 -24
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DagNodeRemark {
|
|
2
|
+
failed: boolean;
|
|
3
|
+
text: string;
|
|
4
|
+
isErrorSummary: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveNodeRemark(node: {
|
|
8
|
+
status?: string | null;
|
|
9
|
+
errorPreview?: string | null;
|
|
10
|
+
label?: string | null;
|
|
11
|
+
nodeId: string;
|
|
12
|
+
rank?: string | number | null;
|
|
13
|
+
} | null | undefined): DagNodeRemark;
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
dashboardVisibleFeatures,
|
|
71
71
|
findDagForRun,
|
|
72
72
|
} from "../dag-model.js";
|
|
73
|
+
import { describeDagNodePurpose } from "../dag-node-purpose.js";
|
|
73
74
|
import { layoutDag } from "../dag-layout.js";
|
|
74
75
|
import {
|
|
75
76
|
showView,
|
|
@@ -136,7 +137,7 @@ export function extractFirstErrorSummary(errorPreview) {
|
|
|
136
137
|
return effective ?? "";
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
/** Resolve 备注 cell content for a DAG node (failed → summary; else
|
|
140
|
+
/** Resolve 备注 cell content for a DAG node (failed → summary; else purpose). */
|
|
140
141
|
export function resolveNodeRemark(node) {
|
|
141
142
|
const failed = isNodeFailed(node?.status);
|
|
142
143
|
if (failed) {
|
|
@@ -147,10 +148,10 @@ export function resolveNodeRemark(node) {
|
|
|
147
148
|
isErrorSummary: Boolean(summary),
|
|
148
149
|
};
|
|
149
150
|
}
|
|
150
|
-
const
|
|
151
|
+
const purpose = describeDagNodePurpose(node?.nodeId, node);
|
|
151
152
|
return {
|
|
152
153
|
failed: false,
|
|
153
|
-
text:
|
|
154
|
+
text: purpose || "—",
|
|
154
155
|
isErrorSummary: false,
|
|
155
156
|
};
|
|
156
157
|
}
|
|
@@ -363,7 +364,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
363
364
|
"page-action-btn",
|
|
364
365
|
"在操作台中止此运行 →",
|
|
365
366
|
);
|
|
366
|
-
interruptLink.href = `/#/recovery?dagRunId=${encodeURIComponent(dag.dagRunId)}&intent=interrupt`;
|
|
367
|
+
interruptLink.href = `/#/operate/recovery?dagRunId=${encodeURIComponent(dag.dagRunId)}&intent=interrupt`;
|
|
367
368
|
interruptLink.setAttribute("data-testid", "inspect-dag-interrupt-link");
|
|
368
369
|
interruptLink.title = "只跳转到操作台 Recovery,不会在观测页执行中止";
|
|
369
370
|
primaryEntries.push(["协作中止", interruptLink]);
|
|
@@ -481,6 +482,7 @@ export function startDagPolling(dagRunId, initial = true) {
|
|
|
481
482
|
const active = await renderDagDetail(dagRunId, isInitial);
|
|
482
483
|
if (generation !== uiState.pollingGeneration || !active) return;
|
|
483
484
|
const delay = detailPollDelay("running", isPageVisible());
|
|
485
|
+
if (delay == null) return;
|
|
484
486
|
uiState.dagPollTimer = setTimeout(() => {
|
|
485
487
|
uiState.dagPollTimer = null;
|
|
486
488
|
void poll(false);
|
|
@@ -84,7 +84,7 @@ let dashboardLoadingNode = null;
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* 「昨夜班次」条带:夜间结果在观测侧的唯一一级呈现(替代旧「夜间任务」tab)。
|
|
87
|
-
* 管理动作(晨审处置)始终落在操作侧 /#/night,保持 Inspect 只读不变式。
|
|
87
|
+
* 管理动作(晨审处置)始终落在操作侧 /#/operate/night,保持 Inspect 只读不变式。
|
|
88
88
|
*/
|
|
89
89
|
async function renderNightStrip() {
|
|
90
90
|
const strip = document.getElementById("dashboard-night");
|
|
@@ -118,14 +118,14 @@ async function renderNightStrip() {
|
|
|
118
118
|
|
|
119
119
|
const actions = el("div", "night-strip-actions");
|
|
120
120
|
const detail = el("a", "page-action-btn", "夜间详情");
|
|
121
|
-
detail.href = "
|
|
121
|
+
detail.href = "/#/inspect/night";
|
|
122
122
|
detail.addEventListener("click", (e) => {
|
|
123
123
|
e.preventDefault();
|
|
124
124
|
navigate("/night");
|
|
125
125
|
});
|
|
126
126
|
actions.appendChild(detail);
|
|
127
127
|
const manage = el("a", "page-action-btn night-strip-manage", "晨审处置 →");
|
|
128
|
-
manage.href = "/#/night";
|
|
128
|
+
manage.href = "/#/operate/night";
|
|
129
129
|
manage.title = "夜间计划的管理与晨审处置在操作台完成(Inspect 保持只读)";
|
|
130
130
|
actions.appendChild(manage);
|
|
131
131
|
strip.appendChild(actions);
|
|
@@ -738,10 +738,12 @@ export function startDashboardPolling(scrollTo) {
|
|
|
738
738
|
const poll = async (initialScrollTo) => {
|
|
739
739
|
await renderDashboard(initialScrollTo);
|
|
740
740
|
if (generation !== uiState.pollingGeneration) return;
|
|
741
|
+
const delay = dashboardPollDelay();
|
|
742
|
+
if (delay == null) return;
|
|
741
743
|
uiState.dashboardTimer = setTimeout(() => {
|
|
742
744
|
uiState.dashboardTimer = null;
|
|
743
745
|
void poll();
|
|
744
|
-
},
|
|
746
|
+
}, delay);
|
|
745
747
|
};
|
|
746
748
|
void poll(scrollTo);
|
|
747
749
|
}
|
|
@@ -46,8 +46,8 @@ export async function renderNightJobs(scheduleId) {
|
|
|
46
46
|
showView("night");
|
|
47
47
|
bindNightToolbar();
|
|
48
48
|
const crumbs = [
|
|
49
|
-
{ label: UI_TEXT.dashboard, href: "
|
|
50
|
-
{ label: UI_TEXT.nightJobs, href: "
|
|
49
|
+
{ label: UI_TEXT.dashboard, href: "/#/inspect" },
|
|
50
|
+
{ label: UI_TEXT.nightJobs, href: "/#/inspect/night" },
|
|
51
51
|
];
|
|
52
52
|
if (scheduleId) crumbs.push({ label: scheduleId });
|
|
53
53
|
setBreadcrumb(crumbs);
|
|
@@ -93,6 +93,7 @@ function startPoolAutoPolling() {
|
|
|
93
93
|
if (generation !== uiState.pollingGeneration) return;
|
|
94
94
|
if (!poolAutoRefreshEnabled()) return;
|
|
95
95
|
const delay = isPageVisible() ? POOL_POLL_MS : dashboardPollDelay();
|
|
96
|
+
if (delay == null) return;
|
|
96
97
|
uiState.poolTimer = setTimeout(() => {
|
|
97
98
|
uiState.poolTimer = null;
|
|
98
99
|
void poll();
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
} from "../relations.js";
|
|
51
51
|
import { copyText, renderRecommendedCommand } from "../copy.js";
|
|
52
52
|
import {
|
|
53
|
+
fetchWithWorkspaceScope,
|
|
53
54
|
isPageVisible,
|
|
54
55
|
detailPollDelay,
|
|
55
56
|
dashboardPollDelay,
|
|
@@ -126,7 +127,7 @@ export function renderInlineArtifactList(container, artifacts) {
|
|
|
126
127
|
list.appendChild(li);
|
|
127
128
|
void (async () => {
|
|
128
129
|
try {
|
|
129
|
-
const res = await
|
|
130
|
+
const res = await fetchWithWorkspaceScope(artifactUrl(artifactPath));
|
|
130
131
|
const text = await res.text();
|
|
131
132
|
if (!res.ok) {
|
|
132
133
|
let message = text;
|
|
@@ -444,6 +445,7 @@ export function startRunPolling(workerRunId, initial = true) {
|
|
|
444
445
|
const active = await renderRun(workerRunId, isInitial);
|
|
445
446
|
if (generation !== uiState.pollingGeneration || !active) return;
|
|
446
447
|
const delay = detailPollDelay("running", isPageVisible());
|
|
448
|
+
if (delay == null) return;
|
|
447
449
|
uiState.runPollTimer = setTimeout(() => {
|
|
448
450
|
uiState.runPollTimer = null;
|
|
449
451
|
void poll(false);
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
repoBaseName,
|
|
35
35
|
parseMarkdownBlocks,
|
|
36
36
|
markdownLinkHref,
|
|
37
|
+
formatLedgerSummary,
|
|
37
38
|
} from "../format.js";
|
|
38
39
|
import { parseRoute, navigate, buildHashPath } from "../router.js";
|
|
39
40
|
import {
|
|
@@ -85,7 +86,7 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
85
86
|
showView("task");
|
|
86
87
|
setBreadcrumb([
|
|
87
88
|
{ label: UI_TEXT.dashboard, href: "#/" },
|
|
88
|
-
{ label: "资源池", href: "
|
|
89
|
+
{ label: "资源池", href: "/#/inspect/pool" },
|
|
89
90
|
{ label: displayLabel },
|
|
90
91
|
]);
|
|
91
92
|
|
|
@@ -94,6 +95,7 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
94
95
|
const linksEl = document.getElementById("task-links");
|
|
95
96
|
const evidenceEl = document.getElementById("task-evidence");
|
|
96
97
|
const runsEl = document.getElementById("task-runs");
|
|
98
|
+
const ledgerEl = document.getElementById("task-ledger");
|
|
97
99
|
if (!metaEl || !linksEl || !evidenceEl || !runsEl) return false;
|
|
98
100
|
if (titleEl) titleEl.textContent = displayLabel;
|
|
99
101
|
mountViewState(metaEl, "loading", "加载 Task…");
|
|
@@ -252,9 +254,62 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
252
254
|
const active = ["running", "pending", "blocked", "stale"].includes(
|
|
253
255
|
(task.status ?? "").toLowerCase(),
|
|
254
256
|
);
|
|
257
|
+
if (ledgerEl) {
|
|
258
|
+
void renderTaskLedger(ledgerEl, taskId);
|
|
259
|
+
}
|
|
255
260
|
return active;
|
|
256
261
|
}
|
|
257
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Task 详情页 ledger 安全摘要(AC-009):从只读 GET /api/tasks/:id/ledger 拉取,
|
|
265
|
+
* 计数/状态/failure code + 有界排除理由;无 ledger 显示空态,失败安全降级。
|
|
266
|
+
*/
|
|
267
|
+
async function renderTaskLedger(ledgerEl, taskId) {
|
|
268
|
+
clearNode(ledgerEl);
|
|
269
|
+
ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
|
|
270
|
+
mountViewState(ledgerEl, "loading", "加载 ledger…");
|
|
271
|
+
let body = null;
|
|
272
|
+
try {
|
|
273
|
+
body = await fetchJson(`/api/tasks/${encodeURIComponent(taskId)}/ledger`);
|
|
274
|
+
} catch {
|
|
275
|
+
body = null;
|
|
276
|
+
}
|
|
277
|
+
if (!body) {
|
|
278
|
+
mountViewState(ledgerEl, "error", "ledger 只读接口失败(安全降级)。");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (body.present !== true) {
|
|
282
|
+
mountViewState(
|
|
283
|
+
ledgerEl,
|
|
284
|
+
body.error ? "error" : "empty",
|
|
285
|
+
body.error
|
|
286
|
+
? `ledger 不可用(${body.error})。`
|
|
287
|
+
: "无 source fidelity ledger(任务无引用或 ledger 缺失,不伪造计数)。",
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const pre = el(
|
|
292
|
+
"pre",
|
|
293
|
+
"cmd",
|
|
294
|
+
formatLedgerSummary({
|
|
295
|
+
present: true,
|
|
296
|
+
fragmentCount: body.fragmentCount,
|
|
297
|
+
canonicalRequirementCount: body.canonicalRequirementCount,
|
|
298
|
+
contextOnlyCount: body.contextOnlyCount,
|
|
299
|
+
duplicateCount: body.duplicateCount,
|
|
300
|
+
unresolvedCount: body.unresolvedCount,
|
|
301
|
+
conflictCount: body.conflictCount,
|
|
302
|
+
gateStatus: body.gateStatus,
|
|
303
|
+
failureCode: body.failureCode,
|
|
304
|
+
exclusions: body.exclusions ?? [],
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
pre.dataset.testid = "task-ledger-summary";
|
|
308
|
+
clearNode(ledgerEl);
|
|
309
|
+
ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
|
|
310
|
+
ledgerEl.appendChild(pre);
|
|
311
|
+
}
|
|
312
|
+
|
|
258
313
|
export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
|
|
259
314
|
if (runsEl === undefined) {
|
|
260
315
|
runsEl = before;
|
|
@@ -5,8 +5,10 @@ import path from "node:path";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import YAML from "yaml";
|
|
7
7
|
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
8
|
+
import { resolveBackendTestManifestModules, backendManifestStatusFinding } from "./backend-test-markdown-workflow.js";
|
|
8
9
|
import { applyOpenApiPartitionDomainPolicy, assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
|
|
9
10
|
import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
|
|
11
|
+
import { inferScenarioParamIntent } from "./backend-test-scenario-param.js";
|
|
10
12
|
const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
|
|
11
13
|
const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
|
|
12
14
|
const NON_CANONICAL_CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-(?:\d{2}|\d{2,3}[A-Z]+))\b.*$/gm;
|
|
@@ -280,6 +282,80 @@ export const backendTestMarkdownPytestCorrespondenceFactsSchema = z.object({
|
|
|
280
282
|
entries: z.array(correspondenceEntrySchema),
|
|
281
283
|
findings: z.array(z.string()),
|
|
282
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
|
+
}
|
|
283
359
|
function isRecord(value) {
|
|
284
360
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
285
361
|
}
|
|
@@ -592,6 +668,19 @@ function automationLabelBody(body, labels) {
|
|
|
592
668
|
const match = new RegExp(`^\\s*[-*+]\\s*(?:${escaped})\\s*[::]\\s*(.*)$`, "mi").exec(automation);
|
|
593
669
|
return match?.[1]?.replaceAll("`", "").trim() ?? "";
|
|
594
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
|
+
}
|
|
595
684
|
function testPointBindingFacts(body, testPoints) {
|
|
596
685
|
const bindings = TEST_POINT_BINDING_MODES.flatMap((mode) => {
|
|
597
686
|
const value = automationLabelBody(body, AUTOMATION_BINDING_LABELS[mode]);
|
|
@@ -659,10 +748,18 @@ async function markdownModuleFiles(workspaceRoot, layout) {
|
|
|
659
748
|
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
660
749
|
const dir = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
|
|
661
750
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
662
|
-
|
|
751
|
+
const all = entries
|
|
663
752
|
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md") && entry.name.toLowerCase() !== "readme.md")
|
|
664
753
|
.map((entry) => path.join(dir, entry.name))
|
|
665
754
|
.sort();
|
|
755
|
+
// Current-round Markdown scope is the README Module Index manifest: unmanifested
|
|
756
|
+
// legacy .md files must not enter Case/coverage/partition statistics. Workspaces
|
|
757
|
+
// without a Module Index fall back to the whole directory (prior behaviour).
|
|
758
|
+
const manifest = await resolveBackendTestManifestModules(workspaceRoot, resolved);
|
|
759
|
+
if (manifest.manifestStatus !== "valid")
|
|
760
|
+
return [];
|
|
761
|
+
const declaredAbs = new Set(manifest.declaredMdFiles.map((rel) => path.join(workspaceRoot, ...rel.split("/")).toLowerCase()));
|
|
762
|
+
return all.filter((file) => declaredAbs.has(file.toLowerCase())).sort();
|
|
666
763
|
}
|
|
667
764
|
async function parseMarkdownCases(workspaceRoot, layout) {
|
|
668
765
|
const result = [];
|
|
@@ -874,6 +971,16 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
874
971
|
findings.push(...coverageScope.findings);
|
|
875
972
|
const matrix = parseCoverageMatrix(readme);
|
|
876
973
|
findings.push(...matrix.findings);
|
|
974
|
+
const manifest = await resolveBackendTestManifestModules(input.workspaceRoot, layout);
|
|
975
|
+
// Fail closed on README Module Index entries declared but never materialized.
|
|
976
|
+
if (manifest.declaredMissing.length > 0) {
|
|
977
|
+
findings.push(`declared Markdown module is missing (Module Index): ${manifest.declaredMissing.join(", ")}`);
|
|
978
|
+
}
|
|
979
|
+
// Fail closed on every non-valid Module Index state: without a valid unique
|
|
980
|
+
// Module Index the current round must not re-scan historical `.md` files.
|
|
981
|
+
if (manifest.manifestStatus !== "valid") {
|
|
982
|
+
findings.push(backendManifestStatusFinding(manifest.manifestStatus, manifest.moduleIndexHeadingCount));
|
|
983
|
+
}
|
|
877
984
|
const cases = await parseMarkdownCases(input.workspaceRoot, layout);
|
|
878
985
|
for (const file of await markdownModuleFiles(input.workspaceRoot, layout)) {
|
|
879
986
|
const markdown = await readFile(file, "utf8");
|
|
@@ -1109,6 +1216,15 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
1109
1216
|
"",
|
|
1110
1217
|
facts.status,
|
|
1111
1218
|
"",
|
|
1219
|
+
`- Manifest scope: ${manifest.manifestStatus === "valid" ? "declared-modules" : `fails-closed:${manifest.manifestStatus}`} (declared-missing=${manifest.declaredMissing.length}, unmanifested-advisory=${manifest.unmanifested.length}, module-index-heading-count=${manifest.moduleIndexHeadingCount})`,
|
|
1220
|
+
"",
|
|
1221
|
+
"## Unmanifested Markdown Modules",
|
|
1222
|
+
"",
|
|
1223
|
+
"- Advisory: existing .md files not declared by the README Module Index are excluded from Case/coverage/partition statistics for this round.",
|
|
1224
|
+
...(manifest.unmanifested.length > 0
|
|
1225
|
+
? manifest.unmanifested.map((item) => `- ${item}`)
|
|
1226
|
+
: ["- None"]),
|
|
1227
|
+
"",
|
|
1112
1228
|
"## Coverage Scope",
|
|
1113
1229
|
"",
|
|
1114
1230
|
`- Change Classification: ${facts.coverageScope.changeClassification}`,
|
|
@@ -1376,6 +1492,11 @@ def payload_expr_nodes(expr, assigned, parameters, seen=None):
|
|
|
1376
1492
|
if isinstance(key,ast.Constant) and key.value == expr.slice.value:
|
|
1377
1493
|
result.extend(shape_only(item) for item in payload_expr_nodes(value,assigned,parameters,set(seen)))
|
|
1378
1494
|
return result
|
|
1495
|
+
if isinstance(expr,ast.IfExp):
|
|
1496
|
+
result=[]
|
|
1497
|
+
result.extend(payload_expr_nodes(expr.body,assigned,parameters,set(seen)))
|
|
1498
|
+
result.extend(payload_expr_nodes(expr.orelse,assigned,parameters,set(seen)))
|
|
1499
|
+
return result
|
|
1379
1500
|
if isinstance(expr,ast.Call):
|
|
1380
1501
|
result=[]
|
|
1381
1502
|
if isinstance(expr.func,ast.Name) and expr.func.id in FUNCTION_PAYLOADS:
|
|
@@ -1416,6 +1537,30 @@ def decorator_payload_nodes(parameters,assigned):
|
|
|
1416
1537
|
for value in values: result.extend(payload_expr_nodes(value,assigned,parameters))
|
|
1417
1538
|
return result
|
|
1418
1539
|
|
|
1540
|
+
def decorator_payload_nodes_by_tp(fn,assigned):
|
|
1541
|
+
result={}
|
|
1542
|
+
for dec in fn.decorator_list:
|
|
1543
|
+
if not isinstance(dec,ast.Call) or not isinstance(dec.func,ast.Attribute) or dec.func.attr != 'parametrize' or len(dec.args) < 2: continue
|
|
1544
|
+
if not isinstance(dec.args[0],ast.Constant) or not isinstance(dec.args[0].value,str): continue
|
|
1545
|
+
names=[item.strip() for item in dec.args[0].value.split(',')]
|
|
1546
|
+
rows=dec.args[1]
|
|
1547
|
+
if isinstance(rows,ast.Name) and rows.id in GLOBAL_VALUES: rows=GLOBAL_VALUES[rows.id]
|
|
1548
|
+
for node in ast.walk(rows):
|
|
1549
|
+
if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute) or node.func.attr != 'param': continue
|
|
1550
|
+
tp=None
|
|
1551
|
+
for keyword in node.keywords:
|
|
1552
|
+
if keyword.arg == 'id' and isinstance(keyword.value,ast.Constant) and isinstance(keyword.value.value,str) and keyword.value.value.startswith('TP-'):
|
|
1553
|
+
tp=keyword.value.value
|
|
1554
|
+
if not tp: continue
|
|
1555
|
+
parameters={}
|
|
1556
|
+
for index,name in enumerate(names):
|
|
1557
|
+
if index < len(node.args): parameters.setdefault(name,[]).append(node.args[index])
|
|
1558
|
+
payload_nodes=decorator_payload_nodes(parameters,assigned)
|
|
1559
|
+
paths=set(); values={}
|
|
1560
|
+
for payload_node in payload_nodes: collect_dict(payload_node,'',paths,values)
|
|
1561
|
+
result[tp]={key:sorted(items) for key,items in sorted(values.items())}
|
|
1562
|
+
return result
|
|
1563
|
+
|
|
1419
1564
|
def fallback_payload_nodes(fn):
|
|
1420
1565
|
result=[]
|
|
1421
1566
|
assigned={}
|
|
@@ -1445,7 +1590,7 @@ def fallback_payload_nodes(fn):
|
|
|
1445
1590
|
|
|
1446
1591
|
def is_direct_transport_call(node):
|
|
1447
1592
|
if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute): return False
|
|
1448
|
-
if node.func.attr.lower() not in {'get','post','put','patch','delete','request'}: return False
|
|
1593
|
+
if node.func.attr.lower() not in {'get','post','put','patch','delete','request','call'}: return False
|
|
1449
1594
|
receiver_tokens=[]
|
|
1450
1595
|
current=node.func.value
|
|
1451
1596
|
while isinstance(current,ast.Attribute):
|
|
@@ -1493,6 +1638,7 @@ for fn in ast.walk(tree):
|
|
|
1493
1638
|
'values':{key:sorted(items) for key,items in sorted(values.items())},
|
|
1494
1639
|
'fallbackPaths':sorted(fallback_paths),
|
|
1495
1640
|
'fallbackValues':{key:sorted(items) for key,items in sorted(fallback_values.items())},
|
|
1641
|
+
'valuesByTestPoint':decorator_payload_nodes_by_tp(fn,{}),
|
|
1496
1642
|
}
|
|
1497
1643
|
print(json.dumps(out,ensure_ascii=True))
|
|
1498
1644
|
`;
|
|
@@ -1623,6 +1769,7 @@ function pytestSymbols(script, source) {
|
|
|
1623
1769
|
payloadValues: payloadShape.values,
|
|
1624
1770
|
fallbackPayloadPaths: payloadShape.fallbackPaths ?? [],
|
|
1625
1771
|
fallbackPayloadValues: payloadShape.fallbackValues ?? {},
|
|
1772
|
+
payloadValuesByTestPoint: payloadShape.valuesByTestPoint ?? {},
|
|
1626
1773
|
};
|
|
1627
1774
|
});
|
|
1628
1775
|
}
|
|
@@ -1636,6 +1783,33 @@ function executionSignature(testCase) {
|
|
|
1636
1783
|
.join("\n");
|
|
1637
1784
|
return createHash("sha256").update(normalized).digest("hex");
|
|
1638
1785
|
}
|
|
1786
|
+
function normalizePythonLiteralValue(value) {
|
|
1787
|
+
const trimmed = value.trim();
|
|
1788
|
+
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
1789
|
+
return trimmed.slice(1, -1);
|
|
1790
|
+
}
|
|
1791
|
+
return trimmed;
|
|
1792
|
+
}
|
|
1793
|
+
function intentionalInvalidEnumVariant(input) {
|
|
1794
|
+
if (!input.testCase.testPointBindings.some((binding) => binding.mode === "variant" && binding.testPoint === input.testPoint))
|
|
1795
|
+
return false;
|
|
1796
|
+
const inferred = inferScenarioParamIntent({ tpId: input.testPoint, caseBody: input.testCase.body });
|
|
1797
|
+
if (inferred.intentSource !== "machine-line" || inferred.field?.toLowerCase() !== input.field.toLowerCase())
|
|
1798
|
+
return false;
|
|
1799
|
+
const observed = normalizePythonLiteralValue(input.observedValue);
|
|
1800
|
+
if (input.allowed.includes(observed))
|
|
1801
|
+
return false;
|
|
1802
|
+
if (inferred.intent === "enum-invalid" || inferred.intent === "wrong-type")
|
|
1803
|
+
return true;
|
|
1804
|
+
if (inferred.intent === "null")
|
|
1805
|
+
return observed === "None" || observed === "null";
|
|
1806
|
+
if (inferred.intent === "empty")
|
|
1807
|
+
return observed === "";
|
|
1808
|
+
if (!inferred.intent.startsWith("custom-literal:"))
|
|
1809
|
+
return false;
|
|
1810
|
+
const expected = normalizePythonLiteralValue(inferred.example ?? inferred.intent.slice("custom-literal:".length));
|
|
1811
|
+
return observed === expected;
|
|
1812
|
+
}
|
|
1639
1813
|
function assessPayloadContract(testCase, refs) {
|
|
1640
1814
|
const requiredPaths = testCase.payloadContract.requiredPaths;
|
|
1641
1815
|
const directObservedPaths = orderedUnique(refs.flatMap((item) => item.payloadPaths));
|
|
@@ -1657,13 +1831,17 @@ function assessPayloadContract(testCase, refs) {
|
|
|
1657
1831
|
? observedPaths.filter((item) => !allowedPaths.includes(item) && !allowedPaths.some((allowed) => item.startsWith(`${allowed}.`)))
|
|
1658
1832
|
: [];
|
|
1659
1833
|
const enumMismatches = [];
|
|
1660
|
-
const delegatesInvalidEnumToScenarioGate = testCase.testPointBindings
|
|
1661
|
-
.filter((binding) => binding.mode === "variant")
|
|
1662
|
-
.some((binding) => /(?:ENUM|CATEGORY).*(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE)|(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE).*(?:ENUM|CATEGORY)/.test(binding.testPoint));
|
|
1663
1834
|
for (const [field, allowed] of Object.entries(testCase.payloadContract.enumValues)) {
|
|
1664
|
-
const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? [])
|
|
1665
|
-
for (const
|
|
1666
|
-
|
|
1835
|
+
const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []));
|
|
1836
|
+
for (const rawValue of observed) {
|
|
1837
|
+
const value = normalizePythonLiteralValue(rawValue);
|
|
1838
|
+
if (allowed.includes(value))
|
|
1839
|
+
continue;
|
|
1840
|
+
const associatedTestPoints = orderedUnique(refs.flatMap((item) => Object.entries(item.payloadValuesByTestPoint)
|
|
1841
|
+
.filter(([, values]) => (values[field] ?? []).includes(rawValue))
|
|
1842
|
+
.map(([testPoint]) => testPoint)));
|
|
1843
|
+
const intentionallyInvalid = associatedTestPoints.length > 0 && associatedTestPoints.every((testPoint) => intentionalInvalidEnumVariant({ testCase, testPoint, field, observedValue: rawValue, allowed }));
|
|
1844
|
+
if (!intentionallyInvalid)
|
|
1667
1845
|
enumMismatches.push(`${field}=${value}; allowed=${allowed.join("|")}`);
|
|
1668
1846
|
}
|
|
1669
1847
|
}
|