fix(web-probe): require workbench readiness after navigation (#682)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -557,12 +557,18 @@ async function gotoTarget(rawTarget) {
|
||||
const response = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
await page.waitForTimeout(1000).catch(() => {});
|
||||
const httpStatus = response ? response.status() : null;
|
||||
const readiness = await waitForTargetPageReady(page, target, { timeoutMs: 15000 });
|
||||
if (!readiness.ok) {
|
||||
const error = new Error("workbench-app-not-ready: " + (readiness.reason || "target page did not expose the Workbench shell"));
|
||||
error.navigationReadiness = readiness;
|
||||
throw error;
|
||||
}
|
||||
const pageProvenance = await refreshPageProvenance("goto", httpStatus);
|
||||
attempts.push({ attempt, ok: true, httpStatus });
|
||||
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus, pageId, pageProvenance: compactPageProvenance(pageProvenance), attempts };
|
||||
attempts.push({ attempt, ok: true, httpStatus, readiness });
|
||||
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus, pageId, pageProvenance: compactPageProvenance(pageProvenance), readiness, attempts };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
attempts.push({ attempt, ok: false, failureKind: navigationFailureKind(message), message: redactErrorMessage(message) });
|
||||
attempts.push({ attempt, ok: false, failureKind: navigationFailureKind(message), message: redactErrorMessage(message), readiness: error?.navigationReadiness ?? null });
|
||||
if (attempt >= 3 || !isRetryableNavigationError(message)) {
|
||||
throw Object.assign(new Error(message), { attempts, target });
|
||||
}
|
||||
@@ -661,7 +667,7 @@ function compactPageProvenance(value) {
|
||||
}
|
||||
|
||||
function isRetryableNavigationError(message) {
|
||||
return /net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|net::ERR_CONNECTION_RESET|Navigation timeout/iu.test(String(message || ""));
|
||||
return /net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|net::ERR_CONNECTION_RESET|Navigation timeout|workbench-app-not-ready/iu.test(String(message || ""));
|
||||
}
|
||||
|
||||
function navigationFailureKind(message) {
|
||||
@@ -670,6 +676,7 @@ function navigationFailureKind(message) {
|
||||
if (/net::ERR_ABORTED/iu.test(text)) return "net::ERR_ABORTED";
|
||||
if (/net::ERR_CONNECTION_RESET/iu.test(text)) return "net::ERR_CONNECTION_RESET";
|
||||
if (/Navigation timeout/iu.test(text)) return "navigation-timeout";
|
||||
if (/workbench-app-not-ready/iu.test(text)) return "workbench-app-not-ready";
|
||||
return "navigation-error";
|
||||
}
|
||||
|
||||
@@ -679,6 +686,65 @@ function redactErrorMessage(message) {
|
||||
.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gu, "$1[redacted]");
|
||||
}
|
||||
|
||||
async function waitForTargetPageReady(targetPage, targetUrl, options = {}) {
|
||||
const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Math.max(1, Number(options.timeoutMs)) : 15000;
|
||||
const targetPathname = safeUrlPath(targetUrl) || "";
|
||||
if (!isWorkbenchPathname(targetPathname)) return { ok: true, reason: "not-workbench-route", valuesRedacted: true };
|
||||
const started = Date.now();
|
||||
await targetPage.waitForFunction(() => {
|
||||
const visible = (element) => {
|
||||
if (!element) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(element);
|
||||
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
||||
};
|
||||
const workspace = document.querySelector("#workspace, .workbench-route");
|
||||
const sessionCreate = document.querySelector("#session-create");
|
||||
const commandInput = document.querySelector("#command-input");
|
||||
const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
|
||||
const warning = document.querySelector(".composer-warning");
|
||||
return Boolean(visible(workspace) && (visible(sessionCreate) || visible(commandInput) || visible(activeTab) || visible(warning)));
|
||||
}, null, { timeout: timeoutMs }).catch(() => null);
|
||||
const snapshot = await workbenchReadinessSnapshot(targetPage);
|
||||
const ok = snapshot.workbenchShellVisible === true && (snapshot.sessionCreateVisible === true || snapshot.commandInputPresent === true || snapshot.activeTabPresent === true || snapshot.warningPresent === true);
|
||||
return {
|
||||
ok,
|
||||
reason: ok ? "workbench-ready" : snapshot.loginVisible ? "login-visible" : "workbench-app-not-ready",
|
||||
durationMs: Date.now() - started,
|
||||
snapshot,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
async function workbenchReadinessSnapshot(targetPage) {
|
||||
return targetPage.evaluate(() => {
|
||||
const visible = (element) => {
|
||||
if (!element) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(element);
|
||||
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
||||
};
|
||||
return {
|
||||
url: window.location.href,
|
||||
path: window.location.pathname,
|
||||
readyState: document.readyState,
|
||||
workbenchShellVisible: visible(document.querySelector("#workspace, .workbench-route")),
|
||||
sessionCreateVisible: visible(document.querySelector("#session-create")),
|
||||
commandInputPresent: visible(document.querySelector("#command-input")),
|
||||
activeTabPresent: visible(document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']")),
|
||||
warningPresent: visible(document.querySelector(".composer-warning")),
|
||||
loginVisible: visible(document.querySelector("form.login-card, .login-card, [data-testid='login']")),
|
||||
bodyTextHash: sha256Text(String(document.body?.innerText || "").slice(0, 2000)),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}).catch((error) => ({ error: errorSummary(error), valuesRedacted: true }));
|
||||
}
|
||||
|
||||
function isWorkbenchPathname(value) {
|
||||
const pathname = String(value || "");
|
||||
return pathname === "/workbench" || pathname === "/workspace" || pathname.startsWith("/workbench/") || pathname.startsWith("/workspace/");
|
||||
}
|
||||
|
||||
async function createSessionFromUi() {
|
||||
const beforeUrl = currentPageUrl();
|
||||
const before = await workbenchSessionSnapshot();
|
||||
|
||||
Reference in New Issue
Block a user