From 8fd0fe0226b8e7f50e03dbd03bb6d7802148ce73 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 27 Jun 2026 05:27:32 +0000 Subject: [PATCH] fix(web-probe): flag mdtodo hwpod and pane gap regressions --- .../scenarios.mdtodo.yaml | 18 +- .../hwlab-node-web-observe-analyzer-source.ts | 158 ++++++++++++++++++ .../hwlab-node-web-observe-runner-source.ts | 34 ++++ 3 files changed, 205 insertions(+), 5 deletions(-) diff --git a/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml b/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml index 226ed2ea..a73068b6 100644 --- a/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml +++ b/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml @@ -19,17 +19,25 @@ sentinel: promptSetRef: config/hwlab-web-probe-sentinel/prompt-set.dsflash-go.yaml#sentinel.promptSet reportViewRef: config/hwlab-web-probe-sentinel/report-views.yaml#sentinel.reportViews commandSequence: + - type: goto + path: /projects/mdtodo/sources/constart-71freq-mdtodo/files/file_0db4dc4e46adf188/tasks/R1 + - type: screenshot + label: mdtodo-few-task-gap - type: gotoProjectMdtodo - type: selectMdtodoFile filename: 20260609_频率判断_用户反馈.md - type: screenshot label: mdtodo-mobile-selected - - type: openMdtodoReportPreview - task: R1 - link: R1 + - type: selectMdtodoTask + task: R14 - type: screenshot - label: mdtodo-report-preview + label: mdtodo-r14-selected + - type: openMdtodoReportPreview + task: R14 + link: R14 + - type: screenshot + label: mdtodo-r14-report-preview - type: toggleMdtodoReportFullscreen text: toggle - type: screenshot - label: mdtodo-report-fullscreen + label: mdtodo-r14-report-fullscreen diff --git a/scripts/src/hwlab-node-web-observe-analyzer-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-source.ts index 04eb8ce7..71c73c7a 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-source.ts @@ -808,6 +808,16 @@ function compactProjectManagementSample(value) { launchButtonText: value.launchButtonText ?? null, blockerCount: value.blockerCount ?? 0, blockers: Array.isArray(value.blockers) ? value.blockers.slice(0, 6) : [], + paneGaps: Array.isArray(value.paneGaps) ? value.paneGaps.slice(0, 8).map((item) => ({ + name: item?.name ?? null, + visible: item?.visible === true, + widthPx: item?.widthPx ?? null, + heightPx: item?.heightPx ?? null, + bottomGapPx: item?.bottomGapPx ?? null, + bottomGapRatio: item?.bottomGapRatio ?? null, + contentNodeCount: item?.contentNodeCount ?? null, + valuesRedacted: true + })) : [], workbenchLinkCount: value.workbenchLinkCount ?? 0, valuesRedacted: true }; @@ -1042,6 +1052,10 @@ function buildProjectManagementReport(samples, control, network, pagePerformance const reportLinkSamples = projectSamples.filter((sample) => Number(sample.projectManagement?.reportLinkCount ?? 0) > 0); const reportPreviewSamples = projectSamples.filter((sample) => sample.projectManagement?.reportPreviewVisible === true && Number(sample.projectManagement?.reportPreview?.textBytes ?? 0) > 0); const reportFullscreenSamples = projectSamples.filter((sample) => sample.projectManagement?.reportFullscreenVisible === true); + const hwpodBlockerSamples = projectManagementHwpodBlockerRows(projectSamples); + const projectionReportSamples = projectManagementProjectionReportRows(projectSamples); + const hwpodApiFailures = projectManagementHwpodApiFailureRows(projectApiFailures); + const severePaneGapSamples = projectManagementPaneGapRows(projectSamples); const previewCommands = commandRows.filter((item) => item.type === "openMdtodoReportPreview" || item.type === "toggleMdtodoReportFullscreen"); const launchNonEmpty = launchSuccess.filter((item) => item.chatObserved === true && (Number(item.workbenchMessageCount ?? 0) > 0 || Number(item.workbenchTraceRowCount ?? 0) > 0)); const launchEmpty = launchSuccess.filter((item) => item.chatObserved !== true || (Number(item.workbenchMessageCount ?? 0) === 0 && Number(item.workbenchTraceRowCount ?? 0) === 0)); @@ -1081,6 +1095,12 @@ function buildProjectManagementReport(samples, control, network, pagePerformance maxReportLinkCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.reportLinkCount)), reportPreviewVisibleSamples: reportPreviewSamples.length, reportFullscreenVisibleSamples: reportFullscreenSamples.length, + hwpodBlockerSampleCount: hwpodBlockerSamples.length, + projectionReportSampleCount: projectionReportSamples.length, + hwpodApiFailureCount: hwpodApiFailures.length, + severePaneGapSampleCount: severePaneGapSamples.length, + maxPaneBottomGapPx: maxNumber(severePaneGapSamples.map((item) => item.maxBottomGapPx)), + maxPaneBottomGapRatio: maxNumber(severePaneGapSamples.map((item) => item.maxBottomGapRatio)), launchButtonVisibleSamples: launchVisibleSamples.length, launchButtonEnabledSamples: launchEnabledSamples.length, launchButtonDisabledSamples: Math.max(0, launchVisibleSamples.length - launchEnabledSamples.length), @@ -1128,6 +1148,7 @@ function buildProjectManagementReport(samples, control, network, pagePerformance reportPreviewVisible: sample.projectManagement?.reportPreviewVisible === true, reportPreviewBytes: sample.projectManagement?.reportPreview?.textBytes ?? 0, reportFullscreenVisible: sample.projectManagement?.reportFullscreenVisible === true, + paneGaps: Array.isArray(sample.projectManagement?.paneGaps) ? sample.projectManagement.paneGaps.slice(0, 4) : [], launchButtonVisible: sample.projectManagement?.launchButtonVisible === true, launchButtonEnabled: sample.projectManagement?.launchButtonEnabled === true, blockerCount: sample.projectManagement?.blockerCount ?? 0, @@ -1140,6 +1161,10 @@ function buildProjectManagementReport(samples, control, network, pagePerformance projectApiByPath, projectApiFailures: projectApiFailures.slice(0, 40), projectApiRequestFailed: projectApiFailedRequests.slice(0, 40), + hwpodBlockerSamples: hwpodBlockerSamples.slice(0, 40), + projectionReportSamples: projectionReportSamples.slice(0, 40), + hwpodApiFailures: hwpodApiFailures.slice(0, 40), + severePaneGapSamples: severePaneGapSamples.slice(0, 40), projectApiPerformance, slowProjectApiPerformance, valuesRedacted: true @@ -1204,12 +1229,29 @@ function compactProjectManagementForOutput(report) { slowSamples: Array.isArray(item?.slowSamples) ? item.slowSamples.slice(0, 3).map(compactSlowSample) : [], valuesRedacted: true }); + const compactSample = (item) => ({ + seq: item?.seq ?? null, + ts: item?.ts ?? null, + pageRole: item?.pageRole ?? null, + path: item?.path ?? null, + selectedTaskRefHash: item?.selectedTaskRefHash ?? null, + selectedFileLabelPreview: item?.selectedFileLabelPreview ?? null, + severePaneCount: item?.severePaneCount ?? null, + maxBottomGapPx: item?.maxBottomGapPx ?? null, + maxBottomGapRatio: item?.maxBottomGapRatio ?? null, + paneGaps: Array.isArray(item?.paneGaps) ? item.paneGaps.slice(0, 4) : undefined, + valuesRedacted: true + }); return { summary: report.summary ?? null, samples: Array.isArray(report.samples) ? report.samples.slice(-8) : [], commands: Array.isArray(report.commands) ? report.commands.slice(-8).map(compactCommand) : [], launchCommands: Array.isArray(report.launchCommands) ? report.launchCommands.slice(-8).map(compactCommand) : [], projectApiByPath: Array.isArray(report.projectApiByPath) ? report.projectApiByPath.slice(0, 8).map(compactApiGroup) : [], + hwpodBlockerSamples: Array.isArray(report.hwpodBlockerSamples) ? report.hwpodBlockerSamples.slice(0, 8).map(compactSample) : [], + projectionReportSamples: Array.isArray(report.projectionReportSamples) ? report.projectionReportSamples.slice(0, 8).map(compactSample) : [], + hwpodApiFailures: Array.isArray(report.hwpodApiFailures) ? report.hwpodApiFailures.slice(0, 8).map(compactApiGroup) : [], + severePaneGapSamples: Array.isArray(report.severePaneGapSamples) ? report.severePaneGapSamples.slice(0, 8).map(compactSample) : [], projectApiPerformance: Array.isArray(report.projectApiPerformance) ? report.projectApiPerformance.slice(0, 8).map(compactPerformance) : [], slowProjectApiPerformance: Array.isArray(report.slowProjectApiPerformance) ? report.slowProjectApiPerformance.slice(0, 8).map(compactPerformance) : [], valuesRedacted: true @@ -1290,6 +1332,110 @@ function projectManagementPerformanceRows(pagePerformance, config) { .map((item) => ({ ...item, projectSlowBudgetMs: config?.slowApiBudgetMs ?? null })); } +function projectManagementDigestText(value) { + if (!value || typeof value !== "object") return ""; + return String(value.textPreview ?? value.preview ?? value.text ?? "").trim(); +} + +function projectManagementSampleRef(sample) { + return { + seq: sample?.seq ?? null, + ts: sample?.ts ?? null, + pageRole: sample?.pageRole ?? null, + path: sample?.path ?? null, + selectedTaskRefHash: sample?.projectManagement?.selectedTaskRef?.hash ?? null, + selectedFileLabelPreview: sample?.projectManagement?.selectedFileLabel?.textPreview ?? null, + valuesRedacted: true + }; +} + +function projectManagementHwpodBlockerRows(projectSamples) { + const pattern = /(?:no outbound WebSocket hwpod-node|HWLAB_HWPOD_NODE_OPS_URL|hwpod-node-ops contract)/iu; + const rows = []; + for (const sample of projectSamples || []) { + const blockers = Array.isArray(sample?.projectManagement?.blockers) ? sample.projectManagement.blockers : []; + const matched = blockers + .filter((item) => pattern.test(projectManagementDigestText(item))) + .map((item) => ({ + index: item?.index ?? null, + testId: item?.testId ?? null, + role: item?.role ?? null, + textHash: item?.textHash ?? null, + textPreview: item?.textPreview ?? null, + valuesRedacted: true + })); + if (matched.length > 0) rows.push({ ...projectManagementSampleRef(sample), blockers: matched.slice(0, 4), valuesRedacted: true }); + } + return rows; +} + +function projectManagementProjectionReportRows(projectSamples) { + const pattern = /(?:报告索引待刷新|projection-only|任务投影确认存在报告链接)/iu; + return (projectSamples || []) + .filter((sample) => pattern.test(projectManagementDigestText(sample?.projectManagement?.reportPreview))) + .map((sample) => ({ + ...projectManagementSampleRef(sample), + reportPreviewHash: sample?.projectManagement?.reportPreview?.textHash ?? null, + reportPreviewPreview: sample?.projectManagement?.reportPreview?.textPreview ?? null, + reportPreviewBytes: sample?.projectManagement?.reportPreview?.textBytes ?? null, + valuesRedacted: true + })); +} + +function projectManagementHwpodApiFailureRows(projectApiFailures) { + const pattern = /^\/v1\/project-management\/mdtodo\/(?:task-detail|report-preview)\b/u; + return (projectApiFailures || []) + .filter((item) => pattern.test(String(item?.path || "")) && Number(item?.status ?? 0) >= 500) + .map((item) => ({ + ts: item?.ts ?? null, + method: item?.method ?? null, + path: item?.path ?? null, + status: item?.status ?? null, + type: item?.type ?? null, + failureKind: item?.failureKind ?? null, + valuesRedacted: true + })); +} + +function projectManagementPaneGapRows(projectSamples) { + const rows = []; + for (const sample of projectSamples || []) { + const paneGaps = Array.isArray(sample?.projectManagement?.paneGaps) ? sample.projectManagement.paneGaps : []; + const severeGaps = paneGaps + .filter((item) => item?.visible === true) + .filter((item) => { + const bottomGapPx = Number(item?.bottomGapPx ?? 0); + const bottomGapRatio = Number(item?.bottomGapRatio ?? 0); + const heightPx = Number(item?.heightPx ?? 0); + return heightPx >= 120 && bottomGapPx >= 180 && bottomGapRatio >= 0.28; + }) + .map((item) => ({ + name: item?.name ?? null, + widthPx: item?.widthPx ?? null, + heightPx: item?.heightPx ?? null, + bottomGapPx: item?.bottomGapPx ?? null, + bottomGapRatio: item?.bottomGapRatio ?? null, + contentNodeCount: item?.contentNodeCount ?? null, + valuesRedacted: true + })); + if (severeGaps.length === 0) continue; + const maxGapPx = maxNumber(severeGaps.map((item) => item.bottomGapPx)); + const maxGapRatio = maxNumber(severeGaps.map((item) => item.bottomGapRatio)); + const multiPane = severeGaps.length >= 2; + const singleExtreme = maxGapPx >= 240 && maxGapRatio >= 0.45; + if (!multiPane && !singleExtreme) continue; + rows.push({ + ...projectManagementSampleRef(sample), + severePaneCount: severeGaps.length, + maxBottomGapPx: maxGapPx, + maxBottomGapRatio: maxGapRatio, + paneGaps: severeGaps.slice(0, 4), + valuesRedacted: true + }); + } + return rows; +} + function buildProjectManagementFindings(report) { if (!report?.enabled) return []; const findings = []; @@ -1312,6 +1458,12 @@ function buildProjectManagementFindings(report) { if (Number(summary.selectedTaskSampleCount ?? 0) > 0 && Number(summary.selectedTaskBodyVisibleSamples ?? 0) === 0) { findings.push({ id: "mdtodo-task-body-not-visible", severity: "red", summary: "selected MDTODO task was sampled but no rendered task body was visible", count: summary.selectedTaskSampleCount, samples: report.samples.filter((item) => item.selectedTaskRefHash).slice(-12), valuesRedacted: true }); } + if (Number(summary.hwpodBlockerSampleCount ?? 0) > 0) { + findings.push({ id: "mdtodo-hwpod-node-disconnected", severity: "red", summary: "MDTODO surfaced the hwpod-node disconnected / HWLAB_HWPOD_NODE_OPS_URL fallback blocker", count: summary.hwpodBlockerSampleCount, samples: report.hwpodBlockerSamples.slice(0, 12), valuesRedacted: true }); + } + if (Number(summary.projectionReportSampleCount ?? 0) > 0) { + findings.push({ id: "mdtodo-report-projection-only", severity: "red", summary: "MDTODO report preview is projection-only instead of opening the full markdown report from the HWPOD source", count: summary.projectionReportSampleCount, samples: report.projectionReportSamples.slice(0, 12), valuesRedacted: true }); + } if (Number(summary.maxReportLinkCount ?? 0) > 0 && Number(summary.reportPreviewVisibleSamples ?? 0) === 0) { const severity = Number(summary.reportPreviewCommandCount ?? 0) > 0 ? "red" : "amber"; findings.push({ id: "mdtodo-report-preview-missing", severity, summary: "MDTODO report links were visible but no markdown report preview was sampled", count: summary.maxReportLinkCount, previewCommandCount: summary.reportPreviewCommandCount, samples: report.samples.filter((item) => Number(item.reportLinkCount ?? 0) > 0).slice(-12), valuesRedacted: true }); @@ -1325,6 +1477,12 @@ function buildProjectManagementFindings(report) { if (Number(summary.mdtodoTaskCountMin ?? 0) > 0 && Number(summary.mdtodoTaskCountMax ?? 0) > 0 && (Number(summary.mdtodoTaskCountMax) - Number(summary.mdtodoTaskCountMin) >= 10 || Number(summary.mdtodoTaskCountMax) / Math.max(1, Number(summary.mdtodoTaskCountMin)) >= 2)) { findings.push({ id: "mdtodo-task-count-diverged", severity: "amber", summary: "MDTODO task count varied sharply during observation; compare control commands and observer samples for projection divergence", minTaskCount: summary.mdtodoTaskCountMin, maxTaskCount: summary.mdtodoTaskCountMax, samples: report.samples.slice(-20), valuesRedacted: true }); } + if (Number(summary.severePaneGapSampleCount ?? 0) > 0) { + findings.push({ id: "mdtodo-pane-bottom-gap", severity: "red", summary: "MDTODO task tree, main detail, or report sidebar left large unused bottom gaps when the visible task set was small", count: summary.severePaneGapSampleCount, maxBottomGapPx: summary.maxPaneBottomGapPx, maxBottomGapRatio: summary.maxPaneBottomGapRatio, samples: report.severePaneGapSamples.slice(0, 12), valuesRedacted: true }); + } + if (Number(summary.hwpodApiFailureCount ?? 0) > 0) { + findings.push({ id: "project-management-hwpod-api-failed", severity: "red", summary: "HWPOD-backed MDTODO task detail or report preview API returned a server error during natural page use", count: summary.hwpodApiFailureCount, failures: report.hwpodApiFailures.slice(0, 12), valuesRedacted: true }); + } if (Number(summary.projectApiFailureCount ?? 0) > 0 || Number(summary.projectApiRequestFailedCount ?? 0) > 0) { findings.push({ id: "project-management-api-failed", severity: "amber", summary: "natural project-management or Workbench launch API requests failed during observation", count: Number(summary.projectApiFailureCount ?? 0) + Number(summary.projectApiRequestFailedCount ?? 0), groups: report.projectApiByPath.slice(0, 12), valuesRedacted: true }); } diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 3379a421..d581e797 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -3448,6 +3448,29 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag text: trim(element.textContent || "", 260), })).filter((item) => item.text); const workbenchLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-link-summary"] li, a[href*="/workbench/sessions/"]')).filter(visible); + const measurePaneGap = (name, paneSelector, contentSelector) => { + const pane = document.querySelector(paneSelector); + if (!visible(pane)) return { name, visible: false }; + const rect = pane.getBoundingClientRect(); + const contentNodes = Array.from(pane.querySelectorAll(contentSelector)).filter(visible); + const contentBottom = Math.max(rect.top, ...contentNodes.map((element) => element.getBoundingClientRect().bottom)); + const bottomGapPx = Math.max(0, Math.round(rect.bottom - contentBottom)); + const heightPx = Math.max(0, Math.round(rect.height)); + return { + name, + visible: true, + widthPx: Math.max(0, Math.round(rect.width)), + heightPx, + bottomGapPx, + bottomGapRatio: heightPx > 0 ? Number((bottomGapPx / heightPx).toFixed(3)) : 0, + contentNodeCount: contentNodes.length, + }; + }; + const paneGaps = [ + measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', '[data-task-ref], [role="treeitem"], [role="listitem"], li, button, input, select, .task-row-shell, .task-tools'), + measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .task-status-stack > *, .task-document-footer'), + measurePaneGap("report-sidebar", '[data-testid="mdtodo-report-sidebar"]', '[data-testid="mdtodo-report-preview"] > *, [data-testid="mdtodo-report-error"], [data-testid="mdtodo-report-fullscreen"], [data-testid="mdtodo-report-close"], .report-sidebar-header, .report-preview .markdown-body > *'), + ]; return { pageKind: mdtodoVisible || path.startsWith("/projects/mdtodo") ? "project-management-mdtodo" : rootVisible || path === "/projects" || path.startsWith("/projects/") ? "project-management-root" : "project-management-unknown", configuredPath, @@ -3480,6 +3503,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag launchButtonText: trim(launch?.textContent || "", 120), blockerCount: blockers.length, blockers, + paneGaps, workbenchLinkCount: workbenchLinks.length, valuesRedacted: true, }; @@ -3706,6 +3730,16 @@ function digestProjectManagement(value) { role: item?.role ?? null, ...textDigest(item?.text || "", 160), })) : [], + paneGaps: Array.isArray(value.paneGaps) ? value.paneGaps.slice(0, 8).map((item) => ({ + name: item?.name ?? null, + visible: item?.visible === true, + widthPx: Number.isFinite(Number(item?.widthPx)) ? Number(item.widthPx) : null, + heightPx: Number.isFinite(Number(item?.heightPx)) ? Number(item.heightPx) : null, + bottomGapPx: Number.isFinite(Number(item?.bottomGapPx)) ? Number(item.bottomGapPx) : null, + bottomGapRatio: Number.isFinite(Number(item?.bottomGapRatio)) ? Number(item.bottomGapRatio) : null, + contentNodeCount: Number.isFinite(Number(item?.contentNodeCount)) ? Number(item.contentNodeCount) : null, + valuesRedacted: true, + })) : [], workbenchLinkCount: Number.isFinite(Number(value.workbenchLinkCount)) ? Number(value.workbenchLinkCount) : 0, valuesRedacted: true };