Files
pikasTech-unidesk/scripts/src/hwlab-node-web-probe-console-verify.ts
T
2026-07-18 13:56:20 +02:00

550 lines
28 KiB
TypeScript

// Responsibility: Generate the repo-owned Cloud Console route-matrix browser command.
import type { HwlabRuntimeWebProbeConsoleVerificationProfileSpec } from "./hwlab-node-web-probe-console-profile";
export function nodeWebProbeConsoleVerifyScript(
profileName: string,
profile: HwlabRuntimeWebProbeConsoleVerificationProfileSpec,
configRef: string,
): string {
const config = { profileName, configRef, ...profile };
return String.raw`const config = ${JSON.stringify(config)};
export default async function consoleVerify({ page, context, goto, reloadStable, wait, screenshot, jsonArtifact, recordStep }) {
const failures = [];
const pages = [];
const screenshots = [];
const pageErrors = [];
const consoleErrors = [];
const requestFailures = [];
const responseErrors = [];
let totalFailureCount = 0;
let totalPageErrorCount = 0;
let totalConsoleErrorCount = 0;
let totalRequestFailureCount = 0;
let totalResponseErrorCount = 0;
const addFailure = (message) => {
totalFailureCount += 1;
if (failures.length < config.outputLimits.failures) failures.push(String(message));
};
const addConsoleError = (message) => {
totalConsoleErrorCount += 1;
if (consoleErrors.length < config.outputLimits.consoleErrors) consoleErrors.push(String(message).slice(0, 300));
};
const addPageError = (error) => {
totalPageErrorCount += 1;
if (pageErrors.length < config.outputLimits.pageErrors) {
pageErrors.push(String(error?.stack || errorMessage(error)).slice(0, 600));
}
};
const addRequestFailure = (request) => {
totalRequestFailureCount += 1;
if (requestFailures.length < config.outputLimits.requestFailures) {
requestFailures.push({
method: request.method(),
url: request.url().replace(/[?#].*$/u, ""),
errorText: String(request.failure()?.errorText || "request failed").slice(0, 300),
});
}
};
const addResponseError = (response) => {
totalResponseErrorCount += 1;
if (responseErrors.length < config.outputLimits.responseErrors) {
responseErrors.push({ status: response.status(), url: response.url().replace(/[?#].*$/u, "") });
}
};
await context.addInitScript(() => {
const OriginalEventSource = window.EventSource;
window.__unideskConsoleEventSources = [];
if (!OriginalEventSource || window.__unideskConsoleEventSourceWrapped === true) return;
window.__unideskConsoleEventSourceWrapped = true;
function TrackedEventSource(url, options) {
const instance = new OriginalEventSource(url, options);
window.__unideskConsoleEventSources.push(String(url));
return instance;
}
TrackedEventSource.prototype = OriginalEventSource.prototype;
TrackedEventSource.CONNECTING = OriginalEventSource.CONNECTING;
TrackedEventSource.OPEN = OriginalEventSource.OPEN;
TrackedEventSource.CLOSED = OriginalEventSource.CLOSED;
window.EventSource = TrackedEventSource;
});
page.on("console", (message) => {
if (message.type() === "error") addConsoleError(message.text());
});
page.on("pageerror", addPageError);
page.on("requestfailed", addRequestFailure);
page.on("response", (response) => {
if (response.status() >= 500) addResponseError(response);
});
const routeById = new Map(config.routes.map((route) => [route.id, route]));
const requiredRoute = (id) => {
const route = routeById.get(id);
if (!route) throw new Error("console verification profile references missing route " + id);
return route;
};
const gotoRoute = async (id) => {
const route = requiredRoute(id);
await goto(route.path, { selectors: [route.readySelector], readinessTimeoutMs: config.navigationTimeoutMs, attempts: 3 });
if (config.settleMs > 0) await wait(config.settleMs);
return route;
};
const captureScreenshot = async (name) => {
if (screenshots.length >= config.outputLimits.screenshots) {
addFailure("screenshot budget exceeded before " + name);
return null;
}
try {
const artifact = await screenshot(name);
screenshots.push(artifact);
return artifact;
} catch (error) {
addFailure("screenshot " + name + " failed: " + errorMessage(error));
return null;
}
};
for (const viewport of config.viewports) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
const beforeCount = pages.length;
for (const route of config.routes) {
await gotoRoute(route.id);
const metrics = await page.evaluate(({ requestedRoute, viewportId, readySelector, textPreviewChars }) => {
const html = document.documentElement;
const body = document.body;
const app = document.querySelector("#app");
const root = document.querySelector(readySelector);
const text = (app?.textContent || "").replace(/\s+/gu, " ").trim();
const rootRect = root?.getBoundingClientRect();
return {
requestedRoute,
viewportId,
finalPath: location.pathname + location.search,
title: document.title,
textLength: text.length,
textPreview: text.slice(0, textPreviewChars),
appVisible: Boolean(app && app.getBoundingClientRect().width > 0 && app.getBoundingClientRect().height > 0),
rootVisible: Boolean(rootRect && rootRect.width > 0 && rootRect.height > 0),
horizontalOverflowPx: Math.max(0, html.scrollWidth - innerWidth, body.scrollWidth - innerWidth),
documentHeight: Math.max(html.scrollHeight, body.scrollHeight),
};
}, {
requestedRoute: route.path,
viewportId: viewport.id,
readySelector: route.readySelector,
textPreviewChars: config.outputLimits.textPreviewChars,
});
pages.push(metrics);
if (!metrics.appVisible || !metrics.rootVisible || metrics.textLength < config.minTextLength) {
addFailure(viewport.id + " " + route.path + ": app/root blank");
}
if (metrics.finalPath.startsWith("/login")) addFailure(viewport.id + " " + route.path + ": redirected to login");
if (metrics.horizontalOverflowPx > config.maxHorizontalOverflowPx) {
addFailure(viewport.id + " " + route.path + ": horizontal overflow " + metrics.horizontalOverflowPx + "px");
}
if (route.screenshot) await captureScreenshot(viewport.id + "-" + route.id + ".png");
}
recordStep("console-matrix-" + viewport.id, {
ok: totalFailureCount === 0,
viewport,
routeSamples: pages.length - beforeCount,
totalFailureCount,
});
}
const workflowEvidence = {};
if (config.workflows.projects) workflowEvidence.projects = await verifyProjects(config.workflows.projects);
if (config.workflows.hwpod) workflowEvidence.hwpod = await verifyHwpod(config.workflows.hwpod);
if (config.workflows.mdtodo) workflowEvidence.mdtodo = await verifyMdtodo(config.workflows.mdtodo);
if (config.workflows.tasktree) workflowEvidence.tasktree = await verifyTasktree(config.workflows.tasktree);
if (config.workflows.agentObserver) workflowEvidence.agentObserver = await verifyAgentObserver(config.workflows.agentObserver);
if (totalResponseErrorCount > 0) addFailure("HTTP 5xx responses: " + totalResponseErrorCount);
const evidence = {
profileName: config.profileName,
configRef: config.configRef,
pages,
failures,
failureCount: totalFailureCount,
consoleErrors,
consoleErrorCount: totalConsoleErrorCount,
pageErrors,
pageErrorCount: totalPageErrorCount,
requestFailures,
requestFailureCount: totalRequestFailureCount,
responseErrors,
responseErrorCount: totalResponseErrorCount,
screenshots,
workflowEvidence,
valuesRedacted: true,
};
const report = await jsonArtifact("cloud-console-verification.json", evidence);
const ok = totalFailureCount === 0;
recordStep("console-verification-final", {
ok,
profileName: config.profileName,
routeSamples: pages.length,
failureCount: totalFailureCount,
screenshotCount: screenshots.length,
});
return {
ok,
status: ok ? "pass" : "blocked",
pageErrors,
consoleErrors,
requestFailures,
responseErrors,
failureCount: totalFailureCount,
failures,
pageErrorCount: totalPageErrorCount,
consoleErrorCount: totalConsoleErrorCount,
requestFailureCount: totalRequestFailureCount,
responseErrorCount: totalResponseErrorCount,
profileName: config.profileName,
configRef: config.configRef,
routeSamples: pages.length,
viewportCount: config.viewports.length,
screenshotCount: screenshots.length,
report,
workflowEvidence,
issueEvidence: {
profileName: config.profileName,
configRef: config.configRef,
reportSha256: typeof report?.sha256 === "string" ? report.sha256 : null,
screenshotSha256: screenshots.map((item) => item?.sha256).filter((item) => typeof item === "string"),
failureCount: totalFailureCount,
failures,
pageErrors,
consoleErrors,
requestFailures,
responseErrors,
valuesRedacted: true,
},
valuesRedacted: true,
};
async function verifyProjects(workflow) {
await gotoRoute(workflow.routeId);
const button = page.getByRole("button", { name: workflow.listButtonName, exact: true });
if (await button.count() < 1) {
addFailure("Projects: list view control missing");
return { listView: false };
}
await button.first().click();
await wait(config.settleMs);
const queryPresent = new URL(page.url()).searchParams.has(workflow.viewQueryKey);
if (!queryPresent) addFailure("Projects: list view did not enter URL state");
return { listView: queryPresent, finalPath: new URL(page.url()).pathname + new URL(page.url()).search };
}
async function verifyHwpod(workflow) {
await gotoRoute(workflow.devicesRouteId);
const listButton = page.getByRole("button", { name: workflow.listButtonName, exact: true });
if (await listButton.count() < 1) {
addFailure("HWPOD devices: list view control missing");
} else {
await listButton.first().click();
await wait(config.settleMs);
if (await page.locator(workflow.devicesListSelector).count() < 1) addFailure("HWPOD devices: list projection missing");
const cardButton = page.getByRole("button", { name: workflow.cardButtonName, exact: true });
if (await cardButton.count() < 1) addFailure("HWPOD devices: card view control missing");
else {
await cardButton.first().click();
await wait(config.settleMs);
if (await page.locator(workflow.devicesCardSelector).count() < 1) addFailure("HWPOD devices: card projection missing");
}
}
await page.emulateMedia({ reducedMotion: "reduce" });
const reducedMotion = await page.evaluate((selector) => ({
matches: matchMedia("(prefers-reduced-motion: reduce)").matches,
animations: [...document.querySelectorAll(selector)].map((element) => getComputedStyle(element).animationName),
}), workflow.pcbTraceSelector);
if (!reducedMotion.matches || reducedMotion.animations.some((name) => name !== "none")) {
addFailure("HWPOD devices: reduced-motion does not stop PCB animation");
}
await page.emulateMedia({ reducedMotion: "no-preference" });
let deviceDeepLink = null;
const detail = page.getByRole("button", { name: workflow.detailButtonName, exact: true }).first();
if (await detail.count() > 0) {
await detail.click();
await wait(config.settleMs);
deviceDeepLink = new URL(page.url()).pathname;
const basePath = requiredRoute(workflow.devicesRouteId).path.split("?")[0];
if (!deviceDeepLink.startsWith(workflow.detailPathPrefix) || deviceDeepLink === basePath) {
addFailure("HWPOD devices: invalid detail deep link " + deviceDeepLink);
}
await reloadStable({ selectors: [requiredRoute(workflow.devicesRouteId).readySelector], readinessTimeoutMs: config.navigationTimeoutMs });
if (new URL(page.url()).pathname !== deviceDeepLink) addFailure("HWPOD devices: deep link did not survive refresh");
}
await gotoRoute(workflow.nodesRouteId);
const nodeListButton = page.getByRole("button", { name: workflow.listButtonName, exact: true });
if (await nodeListButton.count() < 1) addFailure("HWPOD nodes: list view control missing");
else {
await nodeListButton.first().click();
await wait(config.settleMs);
if (await page.locator(workflow.nodesListSelector).count() < 1) addFailure("HWPOD nodes: list projection missing");
}
await gotoRoute(workflow.onboardingRouteId);
const onboarding = await page.evaluate((workflowConfig) => ({
commands: [...document.querySelectorAll(workflowConfig.onboardingCommandSelector)].map((element) => element.textContent?.trim() || ""),
downloadHref: document.querySelector(workflowConfig.downloadSelector)?.getAttribute("href") || null,
internalVisibleCount: document.querySelectorAll(workflowConfig.forbiddenInternalSelector).length,
}), workflow);
for (const prefix of workflow.requiredCommandPrefixes) {
if (!onboarding.commands.some((command) => command.startsWith(prefix))) addFailure("HWPOD onboarding: required command prefix missing: " + prefix);
}
for (const forbidden of workflow.forbiddenDownloadSubstrings) {
if (onboarding.downloadHref?.includes(forbidden)) addFailure("HWPOD onboarding: forbidden download endpoint visible: " + forbidden);
}
if (onboarding.internalVisibleCount > 0) addFailure("HWPOD onboarding: internal entrypoint token visible");
return { deviceDeepLink, reducedMotion, onboarding };
}
async function verifyMdtodo(workflow) {
await gotoRoute(workflow.routeId);
const firstTask = page.locator(workflow.taskSelector).first();
await firstTask.waitFor({ state: "visible", timeout: config.navigationTimeoutMs }).catch(() => undefined);
if (await firstTask.count() < 1) {
addFailure("MDTODO: projected task tree is empty");
return { taskDeepLink: null, reportPreviewVisible: false, reportFullscreenVisible: false };
}
await firstTask.click();
await wait(config.settleMs);
const taskDeepLink = new URL(page.url()).pathname;
if (!taskDeepLink.includes(workflow.taskDeepLinkSegment)) addFailure("MDTODO: task deep link missing " + taskDeepLink);
await reloadStable({ selectors: [workflow.taskDetailSelector], readinessTimeoutMs: config.navigationTimeoutMs });
if (new URL(page.url()).pathname !== taskDeepLink) addFailure("MDTODO: task deep link did not survive refresh");
let reportPreviewVisible = false;
let reportFullscreenVisible = false;
const reportLink = page.locator(workflow.reportLinkSelector).first();
if (await reportLink.count() > 0) {
await reportLink.click();
await wait(config.settleMs);
reportPreviewVisible = await page.locator(workflow.reportPreviewSelector).isVisible().catch(() => false);
if (!reportPreviewVisible) addFailure("MDTODO: report preview did not become visible");
const fullscreen = page.locator(workflow.fullscreenButtonSelector).first();
if (await fullscreen.count() > 0) {
await fullscreen.click();
await wait(config.settleMs);
reportFullscreenVisible = await page.locator(workflow.fullscreenDialogSelector).isVisible().catch(() => false);
if (!reportFullscreenVisible) addFailure("MDTODO: report fullscreen did not become visible");
await page.keyboard.press("Escape");
}
}
return { taskDeepLink, reportPreviewVisible, reportFullscreenVisible };
}
async function verifyTasktree(workflow) {
await gotoRoute(workflow.routeId);
const firstTask = page.locator(workflow.taskSelector).first();
await firstTask.waitFor({ state: "visible", timeout: config.navigationTimeoutMs }).catch(() => undefined);
if (await firstTask.count() < 1) {
addFailure("TaskTree: timeline is empty");
return { taskDeepLink: null, taskCount: 0, visibleBarCount: 0, reportCount: 0, dialogVisible: false };
}
const timeline = await page.evaluate((workflowConfig) => {
const visible = (element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
};
const gantt = document.querySelector(workflowConfig.ganttSelector);
const scroll = document.querySelector(workflowConfig.scrollSelector);
const labels = [...document.querySelectorAll(workflowConfig.taskSelector)];
const bars = [...document.querySelectorAll(workflowConfig.barSelector)];
return {
taskCount: labels.length,
datedTaskCount: labels.filter((element) => element.getAttribute("data-dated") === "true").length,
visibleLabelCount: labels.filter(visible).length,
barCount: bars.length,
visibleBarCount: bars.filter(visible).length,
milestoneCount: document.querySelectorAll(workflowConfig.milestoneSelector).length,
ganttInternalOverflowPx: scroll ? Math.max(0, scroll.scrollWidth - scroll.clientWidth) : null,
splitPane: Boolean(gantt && scroll && !scroll.contains(labels[0])),
labelsWithVisibleText: labels.filter((element) => (element.textContent || "").trim().length > 0).length,
};
}, workflow);
if (timeline.taskCount < 1 || timeline.visibleLabelCount !== timeline.taskCount) addFailure("TaskTree: task labels are not visible");
if (timeline.visibleBarCount !== timeline.datedTaskCount || timeline.labelsWithVisibleText !== timeline.taskCount) addFailure("TaskTree: dated Gantt bars or labels are inconsistent");
const zoomIn = page.getByRole("button", { name: workflow.zoomInButtonName, exact: true });
if (await zoomIn.count() < 1) {
addFailure("TaskTree: timeline zoom control missing");
} else {
for (let attempt = 0; attempt < 4; attempt += 1) {
const scale = (await page.locator(workflow.scaleSelector).textContent().catch(() => ""))?.trim();
if (scale === workflow.secondsScaleLabel) break;
await zoomIn.click();
await wait(config.settleMs);
}
const zoomedTimeline = await page.evaluate((workflowConfig) => {
const scroll = document.querySelector(workflowConfig.scrollSelector);
if (!(scroll instanceof HTMLElement)) return null;
scroll.scrollLeft = Math.max(0, (scroll.scrollWidth - scroll.clientWidth) * 0.4);
scroll.dispatchEvent(new Event("scroll"));
return { clientWidth: scroll.clientWidth, scrollWidth: scroll.scrollWidth };
}, workflow);
await wait(config.settleMs);
const pinnedColumn = await page.evaluate((workflowConfig) => {
const scroll = document.querySelector(workflowConfig.scrollSelector);
const labelPane = document.querySelector(workflowConfig.labelPaneSelector);
const label = document.querySelector(workflowConfig.taskSelector);
const scale = document.querySelector(workflowConfig.scaleSelector)?.textContent?.trim() || "";
if (!(scroll instanceof HTMLElement) || !(labelPane instanceof HTMLElement) || !(label instanceof HTMLElement)) return null;
const labelPaneRect = labelPane.getBoundingClientRect();
const labelRect = label.getBoundingClientRect();
const sampleX = Math.min(labelRect.right - 4, labelRect.left + Math.max(12, labelRect.width / 2));
const sampleY = labelRect.top + labelRect.height / 2;
const topElement = document.elementFromPoint(sampleX, sampleY);
const tickLabels = [...document.querySelectorAll(workflowConfig.tickSelector)];
return {
scale,
labelLeftDelta: Math.round(labelRect.left - labelPaneRect.left),
labelOwnsHitPoint: Boolean(topElement && (topElement === label || label.contains(topElement))),
labelInsideScrollPane: scroll.contains(label),
labelTransform: getComputedStyle(label).transform,
tickLabelsFit: tickLabels.length > 0 && tickLabels.every((tick) => tick.scrollWidth <= tick.clientWidth + 1 && tick.scrollHeight <= tick.clientHeight + 1),
scrollLeft: Math.round(scroll.scrollLeft),
};
}, workflow);
if (!zoomedTimeline || zoomedTimeline.scrollWidth <= zoomedTimeline.clientWidth) addFailure("TaskTree: zoomed timeline is not horizontally scrollable");
if (!pinnedColumn || pinnedColumn.scale !== workflow.secondsScaleLabel) addFailure("TaskTree: timeline did not reach seconds scale");
if (!timeline.splitPane || !pinnedColumn || Math.abs(pinnedColumn.labelLeftDelta) > 2 || !pinnedColumn.labelOwnsHitPoint || pinnedColumn.labelInsideScrollPane || pinnedColumn.labelTransform !== "none") {
addFailure("TaskTree: task label is obscured after horizontal scrolling");
}
if (!pinnedColumn?.tickLabelsFit) addFailure("TaskTree: seconds-scale tick labels are clipped");
await captureScreenshot(workflow.zoomScreenshotName);
timeline.zoomed = { ...zoomedTimeline, ...pinnedColumn };
}
await firstTask.click();
await wait(config.settleMs);
const taskDeepLink = new URL(page.url()).pathname;
if (!taskDeepLink.startsWith(requiredRoute(workflow.routeId).path + "/")) addFailure("TaskTree: group deep link missing " + taskDeepLink);
const groupTask = page.locator(workflow.taskSelector).first();
await groupTask.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
await groupTask.click();
await page.locator(workflow.detailSelector).waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
await wait(config.settleMs);
const detail = await page.evaluate((workflowConfig) => {
const detailPanel = document.querySelector(workflowConfig.detailSelector);
const dialog = document.querySelector(workflowConfig.dialogSelector);
const docked = document.querySelector(workflowConfig.dockedSelector);
const rect = detailPanel?.getBoundingClientRect();
return {
detailVisible: Boolean(rect && rect.width > 0 && rect.height > 0),
detailInsideViewport: Boolean(rect && rect.left >= 0 && rect.top >= 0 && rect.right <= innerWidth + 1 && rect.bottom <= innerHeight + 1),
detailMode: dialog ? "dialog" : docked ? "docked" : "unknown",
reportCount: detailPanel?.querySelectorAll(workflowConfig.reportSelector).length || 0,
};
}, workflow);
if (!detail.detailVisible || !detail.detailInsideViewport) addFailure("TaskTree: detail panel is not bounded by the viewport");
if (detail.detailMode === "unknown") addFailure("TaskTree: detail panel is neither dialog nor docked");
if (detail.reportCount < 1) addFailure("TaskTree: selected task has no execution report");
const artifact = await captureScreenshot(workflow.detailScreenshotName);
if (detail.detailMode === "dialog") await page.keyboard.press("Escape");
await reloadStable({ selectors: [requiredRoute(workflow.routeId).readySelector], readinessTimeoutMs: config.navigationTimeoutMs });
if (new URL(page.url()).pathname !== taskDeepLink) addFailure("TaskTree: group deep link did not survive refresh");
return { taskDeepLink, ...timeline, ...detail, detailScreenshotSha256: artifact?.sha256 || null };
}
async function verifyAgentObserver(workflow) {
await gotoRoute(workflow.routeId);
if (workflow.streamSettleMs > 0) await wait(workflow.streamSettleMs);
const before = await agentObserverState(workflow);
if (before.sourceCount !== 1) addFailure("AgentRun: expected one product EventSource, observed " + before.sourceCount);
const viewResults = [];
for (const view of workflow.views) {
const button = page.getByRole("button", { name: view.buttonName, exact: true });
if (await button.count() < 1) {
addFailure("AgentRun: missing view button " + view.buttonName);
continue;
}
await button.first().click();
await wait(config.settleMs);
const queryValue = new URL(page.url()).searchParams.get("view");
if (queryValue !== view.queryValue) addFailure("AgentRun: view " + view.buttonName + " URL state expected " + view.queryValue + ", got " + queryValue);
const artifact = await captureScreenshot(view.screenshotName);
viewResults.push({ buttonName: view.buttonName, queryValue, screenshotSha256: artifact?.sha256 || null });
}
let selection = null;
if (workflow.selection) {
const candidate = page.locator(workflow.selection.selector).first();
if (await candidate.count() < 1) {
addFailure("AgentRun: selection target missing " + workflow.selection.selector);
} else {
await candidate.focus();
await page.keyboard.press("Enter");
await page.locator(workflow.selection.inspectorSelector).waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
await wait(config.settleMs);
const layout = await page.evaluate(({ inspectorSelector }) => {
const bounds = (selector) => {
const rect = document.querySelector(selector)?.getBoundingClientRect();
return rect ? { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height } : null;
};
const edges = Array.from(document.querySelectorAll(".agent-mind-map-flow .vue-flow__edge-path")).map((edge) => {
const style = getComputedStyle(edge);
const length = typeof edge.getTotalLength === "function" ? edge.getTotalLength() : 0;
const strokeWidth = Number.parseFloat(style.strokeWidth);
const opacity = Number.parseFloat(style.opacity);
const visible = length > 8
&& style.display !== "none"
&& style.visibility !== "hidden"
&& opacity > 0
&& strokeWidth >= 1
&& style.stroke !== "none"
&& style.stroke !== "rgba(0, 0, 0, 0)";
return { length, stroke: style.stroke, strokeWidth, opacity, display: style.display, visibility: style.visibility, visible };
});
const inspector = bounds(inspectorSelector);
const content = bounds(".agent-observer-content");
const header = bounds(inspectorSelector + " .operations-inspector-header");
const tabs = bounds(inspectorSelector + " .operations-inspector-tabs");
const body = bounds(inspectorSelector + " .operations-inspector-body");
return {
edgeCount: edges.length,
visibleEdgeCount: edges.filter((edge) => edge.visible).length,
edgeStyle: edges[0] || null,
inspector,
noWorkspaceOverlap: Boolean(content && inspector && content.right <= inspector.left + 1),
noInspectorChromeOverlap: Boolean(header && tabs && body && header.bottom <= tabs.top + 1 && tabs.bottom <= body.top + 1),
inspectorInsideViewport: Boolean(inspector && inspector.left >= 0 && inspector.right <= innerWidth + 1 && inspector.top >= 0 && inspector.bottom <= innerHeight + 1),
};
}, workflow.selection);
if (layout.visibleEdgeCount < 1) addFailure("AgentRun: mind map has no visible connected SVG edges");
if (!layout.noWorkspaceOverlap) addFailure("AgentRun: Inspector overlaps the workspace");
if (!layout.noInspectorChromeOverlap) addFailure("AgentRun: Inspector header, tabs and body overlap");
if (!layout.inspectorInsideViewport) addFailure("AgentRun: Inspector exceeds the viewport");
const artifact = await captureScreenshot(workflow.selection.screenshotName);
selection = { layout, screenshotSha256: artifact?.sha256 || null };
}
}
const after = await agentObserverState(workflow);
if (after.sourceCount !== before.sourceCount) {
addFailure("AgentRun: view switch changed EventSource count " + before.sourceCount + " -> " + after.sourceCount);
}
if (workflow.blockedPhases.includes(after.phase)) addFailure("AgentRun: transport phase " + after.phase);
return { before, after, views: viewResults, selection };
}
async function agentObserverState(workflow) {
return await page.evaluate(({ eventSourcePath, phaseSelector }) => {
const sources = Array.isArray(window.__unideskConsoleEventSources) ? window.__unideskConsoleEventSources : [];
return {
sources,
sourceCount: sources.filter((url) => String(url).includes(eventSourcePath)).length,
phase: document.querySelector(phaseSelector)?.textContent?.trim() || null,
};
}, workflow);
}
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
`;
}