Merge pull request #1349 from pikasTech/fix/1338-dashboard-verify-contract

fix: simplify sentinel dashboard verify
This commit is contained in:
Lyon
2026-07-01 09:59:48 +08:00
committed by GitHub
2 changed files with 100 additions and 556 deletions
+98 -555
View File
@@ -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<string, unknown>): Record<str
function dashboardDegradedReason(result: CommandResult, transport: Record<string, unknown>, page: Record<string, unknown> | 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, unknown>): 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, unknown>): 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");
}
+2 -1
View File
@@ -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,
),