fix(web-probe): require workbench readiness after navigation (#682)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-22 21:29:07 +08:00
committed by GitHub
parent c260210762
commit 3c92f64f21
@@ -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();