From 1b018a3e1e485ca14e25a4f9ba7577b7dab8f202 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 1 Jul 2026 01:59:07 +0000 Subject: [PATCH] fix: simplify sentinel dashboard verify --- scripts/src/hwlab-node-web-sentinel-p5.ts | 653 ++++------------------ scripts/src/web-probe-remote-artifact.ts | 3 +- 2 files changed, 100 insertions(+), 556 deletions(-) diff --git a/scripts/src/hwlab-node-web-sentinel-p5.ts b/scripts/src/hwlab-node-web-sentinel-p5.ts index 372b9935..c90cf215 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5.ts @@ -467,6 +467,7 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options: artifactCount: transport.artifactCount ?? null, expectedArtifactCount: transport.expectedArtifactCount ?? null, downloadFailure: transport.downloadFailure ?? null, + pollFailure: transport.pollFailure ?? null, }, result: compactCommand(result), degradedReason: ok ? null : dashboardDegradedReason(result, transport, page, screenshotOk), @@ -517,7 +518,7 @@ page.on("requestfailed", (request) => { let httpStatus = null; let navigationError = null; let navigationAttempts = 0; -const maxNavigationAttempts = 3; +const maxNavigationAttempts = 2; const perAttemptTimeout = Math.max(5000, Math.floor(timeout / maxNavigationAttempts)); for (let attempt = 1; attempt <= maxNavigationAttempts; attempt += 1) { navigationAttempts = attempt; @@ -525,17 +526,14 @@ for (let attempt = 1; attempt <= maxNavigationAttempts; attempt += 1) { try { const response = await page.goto(url, { timeout: perAttemptTimeout, waitUntil: "domcontentloaded" }); httpStatus = response?.status() ?? null; - await page.waitForLoadState("networkidle", { timeout: Math.min(10000, perAttemptTimeout) }).catch(() => {}); await page.waitForFunction(() => { const root = document.querySelector("#monitor-web-root"); if (!root) return false; const ready = root.getAttribute("data-monitor-ready") === "true"; const error = document.querySelector("#monitor-web-error"); - const runs = document.querySelectorAll(".run-list .run-row").length; - const trend = document.querySelector("[data-monitor-trend-curve]"); - return ready && (error || runs > 0 || trend); - }, null, { timeout: Math.min(15000, perAttemptTimeout) }).catch(() => {}); - await page.waitForTimeout(500); + return ready || Boolean(error); + }, null, { timeout: Math.min(10000, perAttemptTimeout) }).catch(() => {}); + await page.waitForTimeout(300); const appReady = await page.evaluate(() => document.querySelector("#monitor-web-root")?.getAttribute("data-monitor-ready") === "true").catch(() => false); if (appReady || attempt === maxNavigationAttempts) break; } catch (error) { @@ -545,24 +543,6 @@ for (let attempt = 1; attempt <= maxNavigationAttempts; attempt += 1) { await page.waitForTimeout(750 * attempt); } -await page.evaluate(() => { - const detailPane = document.querySelector(".workspace-grid .pane-detail"); - if (detailPane instanceof HTMLElement) detailPane.scrollTop = Math.min(96, Math.max(0, detailPane.scrollHeight - detailPane.clientHeight)); -}).catch(() => {}); -await page.waitForTimeout(150); - -const trendHoverPoint = await page.evaluate(() => { - const target = document.querySelector(".trend-dot-hit .trend-dot-duration"); - if (!(target instanceof SVGElement)) return null; - const rect = target.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) return null; - return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; -}).catch(() => null); -if (trendHoverPoint) { - await page.mouse.move(trendHoverPoint.x, trendHoverPoint.y); - await page.waitForTimeout(250); -} - if (captureScreenshot && screenshotPath) { await page.screenshot({ path: screenshotPath, fullPage, animations: "disabled" }).catch((error) => { pageErrors.push({ message: "screenshot failed: " + String(error?.message || error).slice(0, 400) }); @@ -570,53 +550,48 @@ if (captureScreenshot && screenshotPath) { } const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId }) => { - const visible = (element) => Boolean(element && !element.hidden); - const text = (selector) => String(document.querySelector(selector)?.textContent || "").replace(/\s+/g, " ").trim(); const numberValue = (value) => Number.isFinite(Number(value)) ? Number(value) : 0; - const errorSampleCount = (counts) => numberValue(counts?.red) + numberValue(counts?.critical) + numberValue(counts?.error); - const warningSampleCount = (counts) => numberValue(counts?.warning) + numberValue(counts?.warn) + numberValue(counts?.amber); - const allSampleCount = (counts) => Object.values(counts || {}).reduce((sum, value) => sum + numberValue(value), 0); + const objectOrNull = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null; const root = document.querySelector("#monitor-web-root"); const shell = document.querySelector("[data-monitor-shell='true']"); const error = document.querySelector("#monitor-web-error"); - const trend = document.querySelector("[data-monitor-trend-curve]"); - const trendTooltip = document.querySelector("[data-monitor-trend-tooltip='true']"); - const timeline = document.querySelector("[data-monitor-timeline='true']"); - const workspace = document.querySelector("[data-monitor-independent-scroll='true']"); - const checksPanel = document.querySelector("[data-monitor-checks='true']"); - const panes = Array.from(document.querySelectorAll(".workspace-grid .pane, [data-monitor-checks='true']")); - const detailPane = document.querySelector(".workspace-grid .pane-detail"); - const detailHeader = detailPane?.querySelector(".pane-header"); - const checksHeader = checksPanel?.querySelector(".pane-header"); - const internalTextPattern = /水合|投影|Trace|trace|Shell|API|DOM|Console|console|Runner|runner|JSONL|steer|facts|分页|HTTP|http|requestfailed|pageerror|Final Response|Code Agent|web-probe|observe|analyzer|终态/u; - const checkRows = Array.from(document.querySelectorAll("[data-check-row='true']")); - const cards = checkRows.slice(0, 8).map((row) => ({ - code: String(row.querySelector(".check-code")?.textContent || "").trim(), - title: String(row.querySelector(".check-title-cell strong")?.textContent || row.querySelector("strong")?.textContent || "").trim(), - body: String(row.textContent || "").replace(/\s+/g, " ").trim().slice(0, 180), - })); - const badCardTitles = cards.filter((card) => internalTextPattern.test(card.title)); - const badCardBodies = cards.filter((card) => internalTextPattern.test(card.body)); - const legendTexts = Array.from(document.querySelectorAll(".trend-legend .legend-item")).map((item) => String(item.textContent || "").replace(/\s+/g, " ").trim()); - const legendNumber = (label) => { - const row = legendTexts.find((item) => item.includes(label)) || ""; - const match = /(\d+(?:\.\d+)?)/u.exec(row); - return match ? Number(match[1]) : null; - }; - const chartTiming = { - latestMinutes: legendNumber("最新运行耗时"), - maxMinutes: legendNumber("最近最高耗时"), - title: text("#trend-heading"), - legendHasDuration: legendTexts.some((item) => item.includes("最新运行耗时")), - }; + const dataset = root ? { + node: root.getAttribute("data-node"), + lane: root.getAttribute("data-lane"), + sentinelId: root.getAttribute("data-sentinel-id"), + basePath: root.getAttribute("data-base-path"), + contractVersion: root.getAttribute("data-contract-version"), + ready: root.getAttribute("data-monitor-ready"), + } : {}; const apiUrl = (path) => { const basePath = root?.getAttribute("data-base-path") || expectedRoutePrefix || ""; const prefix = basePath.replace(/\/+$/u, ""); return (prefix + (path.startsWith("/") ? path : "/" + path)) || path; }; - const runsPayload = await fetch(apiUrl("/api/runs?limit=30&sort=updated"), { cache: "no-store" }).then((item) => item.json()).catch(() => null); - const overviewPayload = await fetch(apiUrl("/api/overview"), { cache: "no-store" }).then((item) => item.json()).catch(() => null); - const runs = Array.isArray(runsPayload?.runs) ? runsPayload.runs : Array.isArray(runsPayload?.items) ? runsPayload.items : []; + const fetchJson = async (path) => { + try { + const response = await fetch(apiUrl(path), { cache: "no-store" }); + return { + ok: response.ok, + httpStatus: response.status, + body: await response.json().catch(() => null), + }; + } catch (error) { + return { + ok: false, + httpStatus: null, + body: null, + error: String(error?.message || error).slice(0, 300), + }; + } + }; + const [overviewResult, runsResult] = await Promise.all([ + fetchJson("/api/overview"), + fetchJson("/api/runs?limit=30&sort=updated"), + ]); + const overviewPayload = objectOrNull(overviewResult.body) || {}; + const runsPayload = objectOrNull(runsResult.body) || {}; + const runs = Array.isArray(runsPayload.runs) ? runsPayload.runs : Array.isArray(runsPayload.items) ? runsPayload.items : []; const latestRun = runs[0] || null; const latestCounts = latestRun && latestRun.severityCounts && typeof latestRun.severityCounts === "object" && !Array.isArray(latestRun.severityCounts) ? latestRun.severityCounts @@ -625,178 +600,9 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId runId: latestRun?.id || latestRun?.runId || null, typeCount: numberValue(latestRun?.findingTypeCount ?? latestRun?.findingCount ?? latestRun?.finding_count), durationMinutes: numberValue(latestRun?.runDurationMinutes ?? latestRun?.durationMinutes ?? latestRun?.timing?.durationMinutes), - error: 0, - warning: 0, - total: 0, - all: 0, - errorSamples: errorSampleCount(latestCounts), - warningSamples: warningSampleCount(latestCounts), - alertSamples: errorSampleCount(latestCounts) + warningSampleCount(latestCounts), - allSamples: allSampleCount(latestCounts), + severityKeys: Object.keys(latestCounts).sort(), }; - const latestDetailPayload = latestRunCounts.runId - ? await fetch(apiUrl("/api/runs/" + encodeURIComponent(latestRunCounts.runId)), { cache: "no-store" }).then((item) => item.json()).catch(() => null) - : null; - const latestDetailRows = Array.isArray(latestDetailPayload?.findings) ? latestDetailPayload.findings : []; - const rowSeverity = (row) => { - const raw = String(row?.maxSeverity || row?.checkLevel || row?.severity || row?.level || "").toLowerCase(); - if (["red", "critical", "error", "blocked", "failed"].includes(raw)) return "red"; - if (["warning", "warn", "amber"].includes(raw)) return "warning"; - if (["info", "notice"].includes(raw)) return "info"; - return "healthy"; - }; - const sampleCount = (row) => Number.isFinite(Number(row?.count)) ? Number(row.count) : 1; - const summarizeRows = (rows) => { - const errorRows = rows.filter((row) => rowSeverity(row) === "red"); - const warningRows = rows.filter((row) => rowSeverity(row) === "warning"); - const sum = (items) => items.reduce((total, row) => total + sampleCount(row), 0); - return { - typeCount: rows.length, - errorTypeCount: errorRows.length, - warningTypeCount: warningRows.length, - alertTypeCount: errorRows.length + warningRows.length, - errorSamples: sum(errorRows), - warningSamples: sum(warningRows), - alertSamples: sum(errorRows) + sum(warningRows), - }; - }; - const latestDetailSummary = summarizeRows(latestDetailRows); - const expectsAlertRows = latestDetailSummary.alertTypeCount > 0; - latestRunCounts.typeCount = latestDetailSummary.typeCount; - latestRunCounts.error = latestDetailSummary.errorTypeCount; - latestRunCounts.warning = latestDetailSummary.warningTypeCount; - latestRunCounts.total = latestDetailSummary.alertTypeCount; - latestRunCounts.all = latestDetailSummary.typeCount; - const trendTooltipSummary = tooltipSummary(trendTooltip); - const detailText = text(".pane-detail"); - const runListText = text(".run-list"); - const timingVisibility = { - latestRunMinutes: latestRunCounts.durationMinutes, - detailHasDuration: /运行分钟\s*[\d.]+\s*分钟/u.test(detailText), - listHasDuration: /运行\s+[\d.]+\s*分钟/u.test(runListText), - tooltipHasDuration: trendTooltipSummary.hasDuration, - }; - const workspaceRect = workspace?.getBoundingClientRect(); - const checksRect = checksPanel?.getBoundingClientRect(); - const heightSummary = (rect) => { - const viewportHeight = window.innerHeight || 1; - const heightPx = rect ? Math.round(rect.height) : null; - const ratio = rect ? Math.round((rect.height / viewportHeight) * 1000) / 1000 : null; - return { - present: Boolean(rect), - heightPx, - ratio, - targetPx: Math.round(viewportHeight * 0.8), - bounded80: Boolean(rect && rect.height <= viewportHeight * 0.82 + 1), - near80: Boolean(rect && rect.height >= viewportHeight * 0.74 && rect.height <= viewportHeight * 0.82 + 1), - }; - }; - const workspacePaneHeights = Array.from(document.querySelectorAll(".workspace-grid > .pane")).map((pane) => heightSummary(pane.getBoundingClientRect())); - const stackedWorkspace = window.matchMedia("(max-width: 1120px)").matches; - const panelHeights = { - viewportHeight: window.innerHeight, - stackedWorkspace, - workspace: heightSummary(workspaceRect), - checks: heightSummary(checksRect), - workspacePanes: workspacePaneHeights, - workspacePaneBounded: workspacePaneHeights.length >= 2 && workspacePaneHeights.every((item) => item.bounded80 === true), - workspaceOk: false, - checksOk: false, - }; - panelHeights.workspaceOk = stackedWorkspace - ? panelHeights.workspacePaneBounded === true - : panelHeights.workspace.near80 === true && panelHeights.workspacePaneBounded === true; - panelHeights.checksOk = panelHeights.checks.near80 === true; - const checkScope = { - present: Boolean(checksPanel), - scope: checksPanel?.getAttribute("data-check-scope") || null, - runId: checksPanel?.getAttribute("data-check-run-id") || null, - typeCount: numberValue(checksPanel?.getAttribute("data-check-type-count")), - errorTypeCount: numberValue(checksPanel?.getAttribute("data-check-error-type-count")), - warningTypeCount: numberValue(checksPanel?.getAttribute("data-check-warning-type-count")), - alertTypeCount: numberValue(checksPanel?.getAttribute("data-check-alert-type-count")), - errorSamples: numberValue(checksPanel?.getAttribute("data-check-error-samples")), - warningSamples: numberValue(checksPanel?.getAttribute("data-check-warning-samples")), - alertSamples: numberValue(checksPanel?.getAttribute("data-check-alert-samples")), - visibleRowCount: document.querySelectorAll("[data-check-row='true']").length, - visibleAlertSamples: numberValue(checksPanel?.getAttribute("data-visible-check-alert-samples")), - expectsAlertRows, - matchesLatestRun: false, - matchesRunDetail: false, - belowWorkspace: Boolean(workspaceRect && checksRect && checksRect.top >= workspaceRect.bottom - 2), - fullWidth: Boolean(workspaceRect && checksRect && checksRect.width >= workspaceRect.width - 2), - }; - const runListRowFor = (runId) => Array.from(document.querySelectorAll(".run-list .run-row")) - .find((row) => row.getAttribute("data-run-id") === runId) || null; - const selectedRunRow = runListRowFor(latestRunCounts.runId) || document.querySelector(".run-list .run-row.selected"); - const selectedRunTags = { - error: numberValue(String(selectedRunRow?.querySelector("[data-run-error-tag='true']")?.textContent || "").match(/(\d+)/u)?.[1]), - warning: numberValue(String(selectedRunRow?.querySelector("[data-run-warning-tag='true']")?.textContent || "").match(/(\d+)/u)?.[1]), - errorVisible: Boolean(selectedRunRow?.querySelector("[data-run-error-tag='true']")), - warningVisible: Boolean(selectedRunRow?.querySelector("[data-run-warning-tag='true']")), - rowSelected: Boolean(selectedRunRow?.classList.contains("selected")), - matchesRunDetail: false, - }; - selectedRunTags.matchesRunDetail = selectedRunTags.error === latestDetailSummary.errorTypeCount - && selectedRunTags.warning === latestDetailSummary.warningTypeCount - && selectedRunTags.errorVisible === (latestDetailSummary.errorTypeCount > 0) - && selectedRunTags.warningVisible === (latestDetailSummary.warningTypeCount > 0) - && selectedRunTags.rowSelected === true; - checkScope.matchesLatestRun = checkScope.present === true - && checkScope.scope === "run" - && checkScope.runId === latestRunCounts.runId - && checkScope.errorTypeCount === latestRunCounts.error - && checkScope.warningTypeCount === latestRunCounts.warning - && checkScope.alertTypeCount === latestRunCounts.total; - checkScope.matchesRunDetail = checkScope.present === true - && checkScope.typeCount === latestDetailSummary.typeCount - && checkScope.errorTypeCount === latestDetailSummary.errorTypeCount - && checkScope.warningTypeCount === latestDetailSummary.warningTypeCount - && checkScope.alertTypeCount === latestDetailSummary.alertTypeCount - && checkScope.errorSamples === latestDetailSummary.errorSamples - && checkScope.warningSamples === latestDetailSummary.warningSamples - && checkScope.alertSamples === latestDetailSummary.alertSamples - && checkScope.visibleRowCount === latestDetailSummary.alertTypeCount - && checkScope.visibleAlertSamples === latestDetailSummary.alertSamples; - const overviewCounts = overviewPayload?.severityCounts && typeof overviewPayload.severityCounts === "object" && !Array.isArray(overviewPayload.severityCounts) - ? overviewPayload.severityCounts - : {}; - const overviewSamples = { - error: errorSampleCount(overviewCounts), - warning: warningSampleCount(overviewCounts), - total: errorSampleCount(overviewCounts) + warningSampleCount(overviewCounts), - allSamples: allSampleCount(overviewCounts), - }; - chartTiming.ok = chartTiming.title.includes("运行耗时") && chartTiming.legendHasDuration === true && Number.isFinite(chartTiming.latestMinutes); - chartTiming.matchesLatestRun = chartTiming.ok === true - && Math.abs(Number(chartTiming.latestMinutes) - Number(latestRunCounts.durationMinutes)) < 0.02; - const trendPanel = document.querySelector(".trend-panel"); - const trendLegend = document.querySelector(".trend-panel .trend-legend"); - const trendPanelRect = trendPanel?.getBoundingClientRect(); - const trendLegendRect = trendLegend?.getBoundingClientRect(); - const trendPanelCompact = { - present: Boolean(trendPanelRect && trendLegendRect), - bottomSlackPx: trendPanelRect && trendLegendRect ? Math.round(trendPanelRect.bottom - trendLegendRect.bottom) : null, - ok: Boolean(trendPanelRect && trendLegendRect && trendPanelRect.bottom - trendLegendRect.bottom <= 28), - }; - const firstCheckRow = document.querySelector("[data-check-row='true']"); - let checkDialog = { opened: false, title: "", width: null, height: null, large: false }; - if (firstCheckRow instanceof HTMLElement) { - firstCheckRow.click(); - await new Promise((resolve) => window.setTimeout(resolve, 80)); - const dialog = document.querySelector("[data-check-dialog='true'] .check-dialog"); - const rect = dialog?.getBoundingClientRect(); - checkDialog = { - opened: Boolean(dialog), - title: String(dialog?.querySelector("#check-dialog-title")?.textContent || "").trim(), - width: rect ? Math.round(rect.width) : null, - height: rect ? Math.round(rect.height) : null, - large: Boolean(rect && rect.width >= Math.min(900, window.innerWidth * 0.7) && rect.height >= Math.min(460, window.innerHeight * 0.5)), - }; - const close = dialog?.querySelector("button[aria-label='关闭监测项详情']"); - if (close instanceof HTMLElement) close.click(); - } - const datasetSentinelId = root?.getAttribute("data-sentinel-id") || ""; + const datasetSentinelId = String(dataset.sentinelId || ""); const finalPath = new URL(window.location.href).pathname.replace(/\/+$/u, "") || "/"; const expectedPath = expectedRoutePrefix.replace(/\/+$/u, "") || "/"; const routePrefixMatches = expectedPath === "/" ? finalPath === "/" : finalPath === expectedPath || finalPath.startsWith(expectedPath + "/"); @@ -804,16 +610,15 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId expectedSentinelId, expectedRoutePrefix, datasetSentinelId, - overviewSentinelId: overviewPayload?.sentinelId || null, - runsSentinelId: runsPayload?.sentinelId || null, + overviewSentinelId: overviewPayload.sentinelId || null, + runsSentinelId: runsPayload.sentinelId || null, finalPath, routePrefixMatches, datasetMatches: expectedSentinelId ? datasetSentinelId === expectedSentinelId : true, - overviewMatches: expectedSentinelId ? overviewPayload?.sentinelId === expectedSentinelId : true, - runsPayloadMatches: expectedSentinelId ? runsPayload?.sentinelId === expectedSentinelId : true, + overviewMatches: expectedSentinelId ? overviewPayload.sentinelId === expectedSentinelId : true, + runsPayloadMatches: expectedSentinelId ? runsPayload.sentinelId === expectedSentinelId : true, runRowsMatch: expectedSentinelId ? runs.every((run) => (run?.sentinelId || expectedSentinelId) === expectedSentinelId) : true, }; - const statusText = text(".status-strip"); const doc = document.documentElement; const body = document.body; const viewport = { width: window.innerWidth, height: window.innerHeight }; @@ -832,7 +637,6 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId if (overflow.length < 5) { overflow.push({ tag: element.tagName.toLowerCase(), - className: String(element.className || "").slice(0, 40), x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), @@ -846,55 +650,28 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId return { shell: Boolean(root && shell), ready: root?.getAttribute("data-monitor-ready") === "true", - dataset: root ? { - node: root.getAttribute("data-node"), - lane: root.getAttribute("data-lane"), - sentinelId: datasetSentinelId, - basePath: root.getAttribute("data-base-path"), - contractVersion: root.getAttribute("data-contract-version"), - } : {}, + dataset, + contract: { + htmlShell: Boolean(root && shell), + appReady: root?.getAttribute("data-monitor-ready") === "true", + apiOverview: overviewResult.ok === true && overviewPayload.ok !== false, + apiRuns: runsResult.ok === true && Array.isArray(runs), + runCount: runs.length, + latestRunId: latestRunCounts.runId, + latestFindingTypeCount: latestRunCounts.typeCount, + }, sentinelBoundary, title: document.title, finalUrl: window.location.href, - statusText: text(".topbar .pill"), - subtitle: text(".subtitle"), - summaryText: text(".status-strip"), - runRows: document.querySelectorAll(".run-list .run-row").length, + runRows: runs.length, checkRows: document.querySelectorAll("[data-check-row='true']").length, - badCardTitleCount: badCardTitles.length, - badCardBodyCount: badCardBodies.length, - trendCurve: Boolean(trend), - trendDotCount: document.querySelectorAll(".trend-dot-hit").length, - trendTooltip: trendTooltipSummary, - trendPanelText: text("#trend-heading"), - chartTiming, latestRunCounts, - latestDetailSummary, - timingVisibility, - checkScope, - selectedRunTags, - trendPanelCompact, - checkDialog, - overviewSamples, - panelHeights, - scopeLabels: { - latestPointLegend: legendTexts.some((item) => item.includes("最新运行耗时")), - historicalSamples: legendTexts.some((item) => item.includes("历史样本累计")) || statusText.includes("历史错误样本"), - }, - timelineItems: document.querySelectorAll(".timeline-list .timeline-item").length, - timelineVisible: Boolean(timeline), - errorVisible: visible(error), - errorText: visible(error) ? text("#monitor-web-error").slice(0, 500) : "", - scrollModel: { - workspace: Boolean(workspace), - paneCount: panes.length, - independentScroll: panes.length >= 3 && panes.every((pane) => { - const style = window.getComputedStyle(pane); - return style.overflowY === "auto" || style.overflowY === "scroll"; - }), - stickyHeader: stickyHeaderSummary(detailPane, detailHeader), - stickyChecksHeader: stickyHeaderSummary(checksPanel, checksHeader), + api: { + overview: { ok: overviewResult.ok, httpStatus: overviewResult.httpStatus }, + runs: { ok: runsResult.ok, httpStatus: runsResult.httpStatus }, }, + errorVisible: Boolean(error && !error.hidden), + errorText: error && !error.hidden ? String(error.textContent || "").replace(/\s+/g, " ").trim().slice(0, 500) : "", layout: { viewport, documentSize, @@ -903,193 +680,23 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId overflow: overflow.slice(0, 2), }, }; - - function tooltipSummary(element) { - const body = String(element?.textContent || "").replace(/\s+/g, " ").trim(); - return { - visible: Boolean(element && body.length > 0), - text: body.slice(0, 240), - hasValues: /错误\s+\d+/u.test(body) && /告警\s+\d+/u.test(body) && /合计\s+\d+/u.test(body), - hasTime: /UTC/u.test(body) || /\d{4}-\d{2}-\d{2}/u.test(body), - hasDuration: /运行\s+[\d.]+\s*分钟/u.test(body), - }; - } - - function stickyHeaderSummary(pane, header) { - if (!(pane instanceof HTMLElement) || !(header instanceof HTMLElement)) { - return { present: false, coversScroll: false, backgroundOpaque: false, detailScrollTop: null }; - } - const rect = header.getBoundingClientRect(); - const style = window.getComputedStyle(header); - const sampleX = Math.round(rect.left + Math.min(32, Math.max(2, rect.width / 2))); - const sampleY = Math.round(rect.top + Math.min(12, Math.max(2, rect.height / 2))); - const topElement = document.elementFromPoint(sampleX, sampleY); - return { - present: true, - detailScrollTop: pane.scrollTop, - headerTop: Math.round(rect.top), - headerBottom: Math.round(rect.bottom), - zIndex: style.zIndex, - backgroundColor: style.backgroundColor, - coversScroll: Boolean(topElement && header.contains(topElement)), - backgroundOpaque: backgroundIsOpaque(style.backgroundColor), - topElementClass: String(topElement?.className || "").slice(0, 80), - }; - } - - function backgroundIsOpaque(value) { - const rgba = /rgba?\(([^)]+)\)/u.exec(value); - if (rgba === null) return value.length > 0 && value !== "transparent"; - const parts = rgba[1].split(",").map((part) => part.trim()); - if (parts.length < 4) return true; - return Number(parts[3]) >= 0.99; - } }, { expectedRoutePrefix, expectedSentinelId }); -const runFilterProbe = await page.evaluate(async ({ expectedRoutePrefix }) => { - const numberValue = (value) => Number.isFinite(Number(value)) ? Number(value) : 0; - const root = document.querySelector("#monitor-web-root"); - const apiUrl = (path) => { - const basePath = root?.getAttribute("data-base-path") || expectedRoutePrefix || ""; - const prefix = basePath.replace(/\/+$/u, ""); - return (prefix + (path.startsWith("/") ? path : "/" + path)) || path; - }; - const rowSeverity = (row) => { - const raw = String(row?.maxSeverity || row?.checkLevel || row?.severity || row?.level || "").toLowerCase(); - if (["red", "critical", "error", "blocked", "failed"].includes(raw)) return "red"; - if (["warning", "warn", "amber"].includes(raw)) return "warning"; - if (["info", "notice"].includes(raw)) return "info"; - return "healthy"; - }; - const sampleCount = (row) => Number.isFinite(Number(row?.count)) ? Number(row.count) : 1; - const summarizeRows = (rows) => { - const errorRows = rows.filter((row) => rowSeverity(row) === "red"); - const warningRows = rows.filter((row) => rowSeverity(row) === "warning"); - const sum = (items) => items.reduce((total, row) => total + sampleCount(row), 0); - return { - typeCount: rows.length, - errorTypeCount: errorRows.length, - warningTypeCount: warningRows.length, - alertTypeCount: errorRows.length + warningRows.length, - errorSamples: sum(errorRows), - warningSamples: sum(warningRows), - alertSamples: sum(errorRows) + sum(warningRows), - }; - }; - const runListRowFor = (runId) => Array.from(document.querySelectorAll(".run-list .run-row")) - .find((row) => row.getAttribute("data-run-id") === runId) || null; - const panelCounts = () => { - const panel = document.querySelector("[data-monitor-checks='true']"); - const panelRunId = panel?.getAttribute("data-check-run-id") || null; - const selectedRunRow = runListRowFor(panelRunId) || document.querySelector(".run-list .run-row.selected"); - return { - present: Boolean(panel), - runId: panelRunId, - typeCount: numberValue(panel?.getAttribute("data-check-type-count")), - errorTypeCount: numberValue(panel?.getAttribute("data-check-error-type-count")), - warningTypeCount: numberValue(panel?.getAttribute("data-check-warning-type-count")), - alertTypeCount: numberValue(panel?.getAttribute("data-check-alert-type-count")), - errorSamples: numberValue(panel?.getAttribute("data-check-error-samples")), - warningSamples: numberValue(panel?.getAttribute("data-check-warning-samples")), - alertSamples: numberValue(panel?.getAttribute("data-check-alert-samples")), - visibleRowCount: document.querySelectorAll("[data-check-row='true']").length, - visibleAlertSamples: numberValue(panel?.getAttribute("data-visible-check-alert-samples")), - selectedRunErrorTag: numberValue(String(selectedRunRow?.querySelector("[data-run-error-tag='true']")?.textContent || "").match(/(\d+)/u)?.[1]), - selectedRunWarningTag: numberValue(String(selectedRunRow?.querySelector("[data-run-warning-tag='true']")?.textContent || "").match(/(\d+)/u)?.[1]), - selectedRunRowSelected: Boolean(selectedRunRow?.classList.contains("selected")), - }; - }; - const waitForRun = async (runId) => { - for (let index = 0; index < 30; index += 1) { - const current = panelCounts(); - if (current.runId === runId && current.present === true) return true; - await new Promise((resolve) => window.setTimeout(resolve, 100)); - } - return false; - }; - const select = document.querySelector("select[aria-label='选择运行记录']"); - if (!(select instanceof HTMLSelectElement)) { - return { ok: false, requestedRunId: null, reason: "run-select-missing" }; - } - const options = Array.from(select.options).filter((option) => option.value); - const requestedOption = options[Math.min(2, Math.max(0, options.length - 1))] || options[0] || null; - const requestedRunId = requestedOption?.value || null; - const fallbackOption = requestedOption || null; - const targetRunId = fallbackOption?.value || requestedRunId; - if (!targetRunId) return { ok: false, requestedRunId, reason: "run-option-missing" }; - select.value = targetRunId; - select.dispatchEvent(new Event("change", { bubbles: true })); - const panelReady = await waitForRun(targetRunId); - const detailPayload = await fetch(apiUrl("/api/runs/" + encodeURIComponent(targetRunId)), { cache: "no-store" }).then((item) => item.json()).catch(() => null); - const detailRows = Array.isArray(detailPayload?.findings) ? detailPayload.findings : []; - const expected = summarizeRows(detailRows); - const observed = panelCounts(); - const matchesRunDetail = observed.runId === targetRunId - && observed.typeCount === expected.typeCount - && observed.errorTypeCount === expected.errorTypeCount - && observed.warningTypeCount === expected.warningTypeCount - && observed.alertTypeCount === expected.alertTypeCount - && observed.errorSamples === expected.errorSamples - && observed.warningSamples === expected.warningSamples - && observed.alertSamples === expected.alertSamples - && observed.visibleRowCount === expected.alertTypeCount - && observed.visibleAlertSamples === expected.alertSamples - && observed.selectedRunErrorTag === expected.errorTypeCount - && observed.selectedRunWarningTag === expected.warningTypeCount - && observed.selectedRunRowSelected === true; - return { - ok: panelReady === true && matchesRunDetail === true, - requestedRunId, - requestedOptionPresent: Boolean(requestedOption), - targetRunId, - panelReady, - observed, - expected, - matchesRunDetail, - }; -}, { expectedRoutePrefix }); -dom.runFilterProbe = runFilterProbe; - const consoleErrors = consoleMessages.filter((item) => item.type === "error"); const ok = !navigationError && httpStatus !== null && httpStatus >= 200 && httpStatus < 300 && dom.shell === true - && dom.ready === true + && (captureScreenshot || dom.ready === true) + && dom.contract?.apiOverview === true + && dom.contract?.apiRuns === true && dom.sentinelBoundary?.datasetMatches === true && dom.sentinelBoundary?.overviewMatches === true && dom.sentinelBoundary?.runsPayloadMatches === true && dom.sentinelBoundary?.runRowsMatch === true && dom.sentinelBoundary?.routePrefixMatches === true && dom.errorVisible !== true - && dom.trendCurve === true - && dom.chartTiming?.ok === true - && dom.chartTiming?.matchesLatestRun === true - && dom.checkScope?.matchesLatestRun === true - && dom.checkScope?.matchesRunDetail === true - && dom.selectedRunTags?.matchesRunDetail === true - && dom.runFilterProbe?.ok === true - && dom.runFilterProbe?.requestedOptionPresent === true - && dom.checkScope?.belowWorkspace === true - && dom.checkScope?.fullWidth === true - && dom.scopeLabels?.latestPointLegend === true - && dom.scopeLabels?.historicalSamples === true - && (dom.trendDotCount === 0 || (dom.trendTooltip?.visible === true && dom.trendTooltip?.hasValues === true && dom.trendTooltip?.hasTime === true && dom.trendTooltip?.hasDuration === true)) - && dom.timingVisibility?.detailHasDuration === true - && dom.timingVisibility?.listHasDuration === true - && dom.trendPanelCompact?.ok === true - && (dom.checkScope?.expectsAlertRows !== true || (dom.checkRows > 0 && dom.checkDialog?.opened === true && dom.checkDialog?.large === true)) - && dom.badCardTitleCount === 0 - && dom.badCardBodyCount === 0 - && dom.timelineVisible === true - && dom.scrollModel?.independentScroll === true - && dom.scrollModel?.stickyHeader?.present === true - && dom.scrollModel?.stickyHeader?.backgroundOpaque === true - && dom.scrollModel?.stickyChecksHeader?.present === true - && dom.scrollModel?.stickyChecksHeader?.backgroundOpaque === true - && dom.panelHeights?.workspaceOk === true - && dom.panelHeights?.checksOk === true && dom.layout?.horizontalOverflow !== true && pageErrors.length === 0; @@ -1155,7 +762,12 @@ function compactDashboardArtifact(artifact: Record): Record, page: Record | null, screenshotOk: boolean): string { if (result.timedOut) return "sentinel-dashboard-command-timeout"; - if (transport.ok !== true) return "sentinel-dashboard-transport-failed"; + if (transport.ok !== true) { + const pollFailure = record(transport.pollFailure); + if (pollFailure.phase === "timeout") return "sentinel-dashboard-transport-timeout"; + if (Object.keys(pollFailure).length > 0) return "sentinel-dashboard-transport-failed"; + return "sentinel-dashboard-transport-failed"; + } if (page === null) return "sentinel-dashboard-browser-output-missing"; if (page.ok !== true) return "sentinel-dashboard-render-failed"; if (!screenshotOk) return "sentinel-dashboard-screenshot-missing"; @@ -1438,11 +1050,11 @@ function probeSentinelPublicDashboard(state: SentinelCicdState, timeoutSeconds: "const [rootUrl,cssUrl,jsUrl,vueUrl,rootCode,rootRc,cssCode,cssRc,jsCode,jsRc,vueCode,vueRc,rootPath,cssPath,jsPath,vuePath]=process.argv.slice(2);", "function read(path){try{return fs.readFileSync(path,'utf8')}catch{return ''}}", "const root=read(rootPath); const css=read(cssPath); const js=read(jsPath); const vue=read(vuePath);", - "const rootOk=Number(rootRc)===0&&Number(rootCode)>=200&&Number(rootCode)<300&&root.includes('id=\"monitor-web-root\"')&&root.includes('/monitor-web/assets/monitor-web.js')&&root.includes('monitor-web-bootstrap');", - "const cssOk=Number(cssRc)===0&&Number(cssCode)>=200&&Number(cssCode)<300&&css.includes('monitor-shell')&&css.includes('workspace-grid')&&css.includes('trend-stage')&&css.length>1000;", - "const jsOk=Number(jsRc)===0&&Number(jsCode)>=200&&Number(jsCode)<300&&js.includes('createApp')&&js.includes('/api/overview')&&js.includes('data-monitor-trend-curve')&&js.length>1000;", - "const vueOk=Number(vueRc)===0&&Number(vueCode)>=200&&Number(vueCode)<300&&vue.includes('createApp')&&vue.length>80000;", - "console.log(JSON.stringify({ok:rootOk&&cssOk&&jsOk&&vueOk,root:{url:rootUrl,httpStatus:Number(rootCode),bytes:Buffer.byteLength(root),shell:root.includes('id=\"monitor-web-root\"'),contract:root.includes('draft-2026-06-27-p11-monitor-web-observability-dashboard')},css:{url:cssUrl,httpStatus:Number(cssCode),bytes:Buffer.byteLength(css),workspaceGrid:css.includes('workspace-grid'),trendStage:css.includes('trend-stage')},js:{url:jsUrl,httpStatus:Number(jsCode),bytes:Buffer.byteLength(js),vueApp:js.includes('createApp'),apiClient:js.includes('/api/overview'),trend:js.includes('data-monitor-trend-curve')},vue:{url:vueUrl,httpStatus:Number(vueCode),bytes:Buffer.byteLength(vue),runtime:vue.includes('createApp')},valuesRedacted:true}));", + "const rootOk=Number(rootRc)===0&&Number(rootCode)>=200&&Number(rootCode)<300&&root.includes('id=\"monitor-web-root\"')&&root.includes('/monitor-web/assets/monitor-web.js');", + "const cssOk=Number(cssRc)===0&&Number(cssCode)>=200&&Number(cssCode)<300&&css.length>1000;", + "const jsOk=Number(jsRc)===0&&Number(jsCode)>=200&&Number(jsCode)<300&&js.length>1000;", + "const vueOk=Number(vueRc)===0&&Number(vueCode)>=200&&Number(vueCode)<300&&vue.length>80000;", + "console.log(JSON.stringify({ok:rootOk&&cssOk&&jsOk&&vueOk,root:{url:rootUrl,httpStatus:Number(rootCode),bytes:Buffer.byteLength(root),shell:root.includes('id=\"monitor-web-root\"'),contract:root.includes('draft-2026-06-27-p11-monitor-web-observability-dashboard')},css:{url:cssUrl,httpStatus:Number(cssCode),bytes:Buffer.byteLength(css)},js:{url:jsUrl,httpStatus:Number(jsCode),bytes:Buffer.byteLength(js)},vue:{url:vueUrl,httpStatus:Number(vueCode),bytes:Buffer.byteLength(vue)},valuesRedacted:true}));", "NODE", ].join("\n"); const result = runCommand(["bash", "-lc", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 30) * 1000 }); @@ -1529,120 +1141,51 @@ function renderDashboardResult(result: Record): string { const page = record(result.page); const dom = record(page.dom); const dataset = record(dom.dataset); + const contract = record(dom.contract); + const sentinelBoundary = record(dom.sentinelBoundary); + const api = record(dom.api); + const apiOverview = record(api.overview); + const apiRuns = record(api.runs); const layout = record(dom.layout); - const chartTiming = record(dom.chartTiming); const latestRunCounts = record(dom.latestRunCounts); - const latestDetailSummary = record(dom.latestDetailSummary); - const timingVisibility = record(dom.timingVisibility); - const checkScope = record(dom.checkScope); - const selectedRunTags = record(dom.selectedRunTags); - const trendPanelCompact = record(dom.trendPanelCompact); - const checkDialog = record(dom.checkDialog); - const runFilterProbe = record(dom.runFilterProbe); - const runFilterObserved = record(runFilterProbe.observed); - const runFilterExpected = record(runFilterProbe.expected); - const overviewSamples = record(dom.overviewSamples); - const panelHeights = record(dom.panelHeights); - const workspaceHeight = record(panelHeights.workspace); - const checksHeight = record(panelHeights.checks); const screenshot = record(result.screenshot); const remote = record(result.remote); const transport = record(result.transport); + const pollFailure = record(transport.pollFailure); const degradedReason = result.degradedReason ?? null; return [ String(result.command), "", table(["NODE", "LANE", "SENTINEL", "STATUS", "URL"], [[result.node, result.lane, result.sentinelId, result.ok === true ? "pass" : "blocked", result.publicUrl]]), "", - table(["HTTP", "SHELL", "RUN_ROWS", "CHECK_ROWS", "TABS", "ERRORS", "CONSOLE_ERR", "REQ_FAIL"], [[ + table(["HTTP", "SHELL", "READY", "API_OVERVIEW", "API_RUNS", "RUN_ROWS", "ERRORS", "CONSOLE_ERR", "REQ_FAIL"], [[ page.httpStatus ?? "-", dom.shell, + dom.ready, + `${apiOverview.ok ?? "-"}/${apiOverview.httpStatus ?? "-"}`, + `${apiRuns.ok ?? "-"}/${apiRuns.httpStatus ?? "-"}`, dom.runRows, - dom.checkRows, - dom.detailTabs, page.pageErrorCount, page.consoleErrorCount, page.requestFailureCount, ]]), "", - table(["TITLE", "STATUS_TEXT", "CONTRACT", "BASE_PATH"], [[dom.title, dom.statusText, dataset.contractVersion, dataset.basePath ?? "-"]]), - "", - table(["TREND_LATEST_MIN", "TREND_MAX_MIN", "TREND_DURATION", "MATCH_LATEST", "BAD_TITLE", "BAD_BODY"], [[ - chartTiming.latestMinutes ?? "-", - chartTiming.maxMinutes ?? "-", - chartTiming.ok ?? "-", - chartTiming.matchesLatestRun ?? "-", - dom.badCardTitleCount ?? "-", - dom.badCardBodyCount ?? "-", + table(["CONTRACT", "BASE_PATH", "DATASET_SENTINEL", "OVERVIEW_SENTINEL", "RUNS_SENTINEL", "ROUTE_MATCH"], [[ + dataset.contractVersion ?? "-", + dataset.basePath ?? "-", + sentinelBoundary.datasetSentinelId ?? "-", + sentinelBoundary.overviewSentinelId ?? "-", + sentinelBoundary.runsSentinelId ?? "-", + sentinelBoundary.routePrefixMatches ?? "-", ]]), "", - table(["LATEST_RUN", "TYPE_COUNT", "ERR_TYPES", "ALERT_TYPES", "TOTAL_TYPES", "SAMPLE_TOTAL", "HIST_ERR", "HIST_ALERT"], [[ + table(["HTML_SHELL", "APP_READY", "API_OVERVIEW_OK", "API_RUNS_OK", "LATEST_RUN", "TYPE_COUNT"], [[ + contract.htmlShell ?? "-", + contract.appReady ?? "-", + contract.apiOverview ?? "-", + contract.apiRuns ?? "-", latestRunCounts.runId ?? "-", latestRunCounts.typeCount ?? "-", - latestRunCounts.error ?? "-", - latestRunCounts.warning ?? "-", - latestRunCounts.total ?? "-", - latestRunCounts.alertSamples ?? "-", - overviewSamples.error ?? "-", - overviewSamples.warning ?? "-", - ]]), - "", - table(["RUN_MIN", "DETAIL_MIN_VISIBLE", "LIST_MIN_VISIBLE", "TOOLTIP_MIN_VISIBLE"], [[ - latestRunCounts.durationMinutes ?? "-", - timingVisibility.detailHasDuration ?? "-", - timingVisibility.listHasDuration ?? "-", - timingVisibility.tooltipHasDuration ?? "-", - ]]), - "", - table(["CHECK_SCOPE", "CHECK_RUN", "CHECK_TYPES", "CHECK_ERR_TYPES", "CHECK_ALERT_TYPES", "SAMPLE_ERR", "SAMPLE_ALERT", "CHECK_MATCH_LATEST", "CHECK_MATCH_DETAIL"], [[ - checkScope.scope ?? "-", - checkScope.runId ?? "-", - `${checkScope.typeCount ?? "-"}/${latestDetailSummary.typeCount ?? "-"}`, - `${checkScope.errorTypeCount ?? "-"}/${latestDetailSummary.errorTypeCount ?? "-"}`, - `${checkScope.alertTypeCount ?? "-"}/${latestDetailSummary.alertTypeCount ?? "-"}`, - checkScope.errorSamples ?? "-", - checkScope.alertSamples ?? "-", - checkScope.matchesLatestRun ?? "-", - checkScope.matchesRunDetail ?? "-", - ]]), - "", - table(["CHECK_VISIBLE_ROWS", "EXPECT_ALERT_ROWS", "CHECK_VISIBLE_ALERT", "RUN_TAG_ERR", "RUN_TAG_ALERT", "RUN_TAG_MATCH", "BELOW_WORKSPACE", "FULL_WIDTH"], [[ - checkScope.visibleRowCount ?? "-", - checkScope.expectsAlertRows ?? "-", - checkScope.visibleAlertSamples ?? "-", - selectedRunTags.error ?? "-", - selectedRunTags.warning ?? "-", - selectedRunTags.matchesRunDetail ?? "-", - checkScope.belowWorkspace ?? "-", - checkScope.fullWidth ?? "-", - ]]), - "", - table(["TREND_PANEL_SLACK", "TREND_PANEL_COMPACT", "DETAIL_DIALOG", "DIALOG_LARGE"], [[ - trendPanelCompact.bottomSlackPx ?? "-", - trendPanelCompact.ok ?? "-", - checkDialog.opened ?? "-", - checkDialog.large ?? "-", - ]]), - "", - table(["WORKSPACE_H", "WORKSPACE_RATIO", "WORKSPACE_80", "CHECKS_H", "CHECKS_RATIO", "CHECKS_80", "PANES_80"], [[ - `${workspaceHeight.heightPx ?? "-"}/${workspaceHeight.targetPx ?? "-"}`, - workspaceHeight.ratio ?? "-", - panelHeights.workspaceOk ?? "-", - `${checksHeight.heightPx ?? "-"}/${checksHeight.targetPx ?? "-"}`, - checksHeight.ratio ?? "-", - panelHeights.checksOk ?? "-", - panelHeights.workspacePaneBounded ?? "-", - ]]), - "", - table(["FILTER_RUN", "FILTER_OPTION", "FILTER_TYPES", "FILTER_ERR_TYPES", "FILTER_ALERT_TYPES", "FILTER_SAMPLE_ERR", "FILTER_SAMPLE_ALERT", "FILTER_MATCH_DETAIL"], [[ - runFilterProbe.targetRunId ?? "-", - runFilterProbe.requestedOptionPresent ?? "-", - `${runFilterObserved.typeCount ?? "-"}/${runFilterExpected.typeCount ?? "-"}`, - `${runFilterObserved.errorTypeCount ?? "-"}/${runFilterExpected.errorTypeCount ?? "-"}`, - `${runFilterObserved.alertTypeCount ?? "-"}/${runFilterExpected.alertTypeCount ?? "-"}`, - runFilterObserved.errorSamples ?? "-", - runFilterObserved.alertSamples ?? "-", - runFilterProbe.matchesRunDetail ?? "-", ]]), "", table(["VIEWPORT", "DOC", "H_OVERFLOW", "OVERFLOW_COUNT"], [[ @@ -1656,14 +1199,14 @@ function renderDashboardResult(result: Record): string { ? "SCREENSHOT\n-" : table(["LOCAL_PATH", "BYTES", "SHA256", "VERIFIED"], [[screenshot.localPath, screenshot.bytes, short(screenshot.sha256), screenshot.verified]]), "", - degradedReason === null ? "BLOCKER\n-" : table(["CODE", "REMOTE_EXIT", "TRANSPORT"], [[degradedReason, remote.exitCode, transport.ok]]), + degradedReason === null ? "BLOCKER\n-" : table(["CODE", "REMOTE_EXIT", "TRANSPORT", "POLL_PHASE"], [[degradedReason, remote.exitCode, transport.ok, pollFailure.phase ?? "-"]]), "", "NEXT", ` screenshot: bun scripts/cli.ts web-probe sentinel dashboard screenshot --node ${result.node} --lane ${result.lane} --sentinel ${result.sentinelId}`, ` validate: bun scripts/cli.ts web-probe sentinel validate --node ${result.node} --lane ${result.lane} --sentinel ${result.sentinelId}`, "", "DISCLOSURE", - " dashboard verify uses the YAML publicExposure URL and remote browser execution; it does not start a sentinel run or inspect provider payloads.", + " dashboard verify uses YAML publicExposure, remote browser execution and API/data-contract checks; it does not assert UI copy or CSS class names.", ].join("\n"); } diff --git a/scripts/src/web-probe-remote-artifact.ts b/scripts/src/web-probe-remote-artifact.ts index 47557bd5..266fd13e 100644 --- a/scripts/src/web-probe-remote-artifact.ts +++ b/scripts/src/web-probe-remote-artifact.ts @@ -134,6 +134,7 @@ export function runWebProbeRemoteArtifactJob(options: WebProbeRemoteArtifactJobO if (cleanup.result !== null) commandResults.push(cleanup.result); const manifestExitCode = manifest?.exitCode ?? null; const ok = pollFailure === null && manifest !== null && manifest.exitCode === 0 && downloadFailure === null; + const exitCode = ok ? 0 : manifestExitCode ?? (pollFailure !== null ? 1 : submit.exitCode ?? 1); const transport = { ok, command: "web-probe remote-artifact", @@ -168,7 +169,7 @@ export function runWebProbeRemoteArtifactJob(options: WebProbeRemoteArtifactJobO return { result: syntheticCommandResult( [transPath(), options.route, "web-probe-remote-artifact"], - ok ? 0 : manifestExitCode ?? submit.exitCode ?? 1, + exitCode, JSON.stringify(transport, null, 2), commandResults, ),