fix(web-probe): flag mdtodo hwpod and pane gap regressions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user