fix: stabilize hwlab web-probe script navigation
This commit is contained in:
@@ -326,6 +326,9 @@ function scriptHelpers() {
|
||||
goto,
|
||||
gotoRaw,
|
||||
gotoStable,
|
||||
reloadStable,
|
||||
safeReload,
|
||||
gotoCurrentStable,
|
||||
waitWorkbenchReady,
|
||||
waitForReady,
|
||||
fetchJson,
|
||||
@@ -819,6 +822,155 @@ async function gotoRaw(target = "/workbench", options = {}) {
|
||||
return page.url();
|
||||
}
|
||||
|
||||
async function reloadStable(options = {}) {
|
||||
return stableCurrentPageNavigation("reload-stable", null, normalizeHelperOptions(options));
|
||||
}
|
||||
|
||||
async function safeReload(options = {}) {
|
||||
try {
|
||||
return await reloadStable(options);
|
||||
} catch (error) {
|
||||
const failure = classifiedProbeError(error);
|
||||
return {
|
||||
ok: false,
|
||||
failureKind: failure.failureKind,
|
||||
error: failure.code,
|
||||
errorMessage: sanitize(failure.message),
|
||||
guidance: failure.guidance,
|
||||
beforeUrl: error && typeof error === "object" && typeof error.beforeUrl === "string" ? error.beforeUrl : null,
|
||||
finalUrl: currentPageUrl(),
|
||||
readiness: publicReadiness({ error: failure.code, failureKind: failure.failureKind }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function gotoCurrentStable(options = {}) {
|
||||
options = normalizeHelperOptions(options);
|
||||
const target = typeof options.url === "string" && options.url.length > 0 ? new URL(options.url, baseUrl).toString() : currentPageUrl();
|
||||
if (!target) throw stableProbeError("browser-load-jitter", "cannot retry current page navigation because the browser page has no URL", publicReadiness({ error: "browser-page-missing" }));
|
||||
return stableCurrentPageNavigation("goto-current-stable", target, options);
|
||||
}
|
||||
|
||||
async function stableCurrentPageNavigation(operation, targetUrl, options = {}) {
|
||||
options = normalizeHelperOptions(options);
|
||||
const attempts = boundedInteger(options.attempts, 3, 1, 5);
|
||||
const retryDelayMs = boundedInteger(options.retryDelayMs, 350, 0, 5000);
|
||||
const beforeUrl = currentPageUrl();
|
||||
let retryUrl = targetUrl;
|
||||
let lastRecord = null;
|
||||
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
if (!page || page.isClosed()) page = await context.newPage();
|
||||
const attemptPage = page;
|
||||
const attemptBeforeUrl = currentPageUrl();
|
||||
if (!retryUrl && !attemptBeforeUrl) {
|
||||
throw stableProbeError("browser-load-jitter", "cannot reload because the browser page has no URL", publicReadiness({ error: "browser-page-missing" }));
|
||||
}
|
||||
const navigationTarget = retryUrl || attemptBeforeUrl;
|
||||
const readinessSelectors = normalizeSelectorList(options.selectors, defaultReadinessSelectors(navigationTarget || baseUrl));
|
||||
const network = createNetworkTracker(attemptPage, options.apiPattern);
|
||||
const record = {
|
||||
ok: false,
|
||||
attempt,
|
||||
attempts,
|
||||
policy: operation,
|
||||
targetPath: urlPath(navigationTarget),
|
||||
beforePath: urlPath(attemptBeforeUrl),
|
||||
finalPath: null,
|
||||
stage: operation,
|
||||
error: null,
|
||||
message: null,
|
||||
selector: null,
|
||||
selectors: [],
|
||||
apiRequestsSent: false,
|
||||
apiRequestCount: 0,
|
||||
apiResponseFailed: false,
|
||||
apiFailureCount: 0,
|
||||
screenshot: null,
|
||||
durationMs: 0,
|
||||
};
|
||||
const started = Date.now();
|
||||
|
||||
try {
|
||||
if (operation === "reload-stable" && attempt === 1) {
|
||||
await attemptPage.reload(navigationOptions(options));
|
||||
} else {
|
||||
await attemptPage.goto(navigationTarget, navigationOptions(options));
|
||||
}
|
||||
retryUrl = attemptPage.url();
|
||||
record.finalPath = urlPath(attemptPage.url());
|
||||
const readiness = await waitForReadyInternal(attemptPage, {
|
||||
...options,
|
||||
selectors: readinessSelectors,
|
||||
url: attemptPage.url(),
|
||||
}, network);
|
||||
applyReadinessToRecord(record, readiness);
|
||||
if (!readiness.ok) {
|
||||
record.screenshot = await captureReadinessScreenshot(attemptPage, attempt, options);
|
||||
lastRecord = record;
|
||||
} else {
|
||||
record.ok = true;
|
||||
record.stage = "ready";
|
||||
lastRecord = record;
|
||||
}
|
||||
} catch (error) {
|
||||
record.finalPath = attemptPage && !attemptPage.isClosed() ? urlPath(attemptPage.url()) : null;
|
||||
record.error = classifyNavigationError(error);
|
||||
record.message = error instanceof Error ? error.message : String(error);
|
||||
record.screenshot = await captureReadinessScreenshot(attemptPage, attempt, options);
|
||||
lastRecord = record;
|
||||
} finally {
|
||||
Object.assign(record, network.summary());
|
||||
record.durationMs = Date.now() - started;
|
||||
network.dispose();
|
||||
readinessRecords.push(record);
|
||||
}
|
||||
|
||||
if (record.ok) {
|
||||
const result = {
|
||||
ok: true,
|
||||
operation,
|
||||
attempt,
|
||||
attempts,
|
||||
beforeUrl,
|
||||
finalUrl: page.url(),
|
||||
readiness: record,
|
||||
summary: publicReadiness(),
|
||||
};
|
||||
recordStep(typeof options.name === "string" ? options.name : operation, {
|
||||
ok: true,
|
||||
operation,
|
||||
attempt,
|
||||
attempts,
|
||||
beforeUrl,
|
||||
finalUrl: result.finalUrl,
|
||||
});
|
||||
Object.defineProperty(result, "page", {
|
||||
enumerable: false,
|
||||
value: page,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
if (attempt < attempts && retryDelayMs > 0) await sleep(retryDelayMs);
|
||||
}
|
||||
|
||||
const code = lastRecord?.error || "browser-load-jitter";
|
||||
const message = lastRecord?.message || operation + " did not become ready";
|
||||
recordStep(typeof options.name === "string" ? options.name : operation, {
|
||||
ok: false,
|
||||
operation,
|
||||
attempts,
|
||||
beforeUrl,
|
||||
finalUrl: currentPageUrl(),
|
||||
error: code,
|
||||
message,
|
||||
screenshot: lastRecord?.screenshot ?? null,
|
||||
});
|
||||
const error = stableProbeError(code, message, publicReadiness({ error: code }));
|
||||
error.beforeUrl = beforeUrl;
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function gotoStable(target = "/workbench", options = {}) {
|
||||
options = normalizeHelperOptions(options);
|
||||
const url = new URL(target, baseUrl).toString();
|
||||
@@ -1126,13 +1278,16 @@ function navigationOptions(options = {}) {
|
||||
"expectApiRequest",
|
||||
"failOnApiResponseFailed",
|
||||
"freshPage",
|
||||
"name",
|
||||
"readinessTimeoutMs",
|
||||
"record",
|
||||
"retryDelayMs",
|
||||
"reusePage",
|
||||
"screenshotOnFailure",
|
||||
"selectorState",
|
||||
"selectors",
|
||||
"stable",
|
||||
"url",
|
||||
]) {
|
||||
delete next[key];
|
||||
}
|
||||
@@ -1300,7 +1455,8 @@ function probeFailureKind(code, message) {
|
||||
if (code === "script-api-misuse") return "script-api-misuse";
|
||||
if (code === "auth-login-failed") return "auth-failed";
|
||||
if (code === "browser-failed") return "browser-failed";
|
||||
if (code === "browser-load-jitter" || code === "selector-timeout" || code === "api-not-sent" || code === "api-response-failed") return "navigation-failed";
|
||||
if (code === "browser-load-jitter") return "browser-navigation-jitter";
|
||||
if (code === "selector-timeout" || code === "api-not-sent" || code === "api-response-failed") return "navigation-failed";
|
||||
if (/browser has been closed|browser executable|failed to launch/iu.test(message)) return "browser-failed";
|
||||
return "assertion-failed";
|
||||
}
|
||||
@@ -1310,6 +1466,7 @@ function probeFailureGuidance(failureKind) {
|
||||
if (failureKind === "auth-redirect-login") return "Inspect auth/session bootstrap and finalUrl; the browser returned to /login after CLI-managed auth.";
|
||||
if (failureKind === "same-origin-api-fetch-failed") return "Inspect the failed same-origin API path, current URL, auth state, and browser console/network evidence in the full report.";
|
||||
if (failureKind === "script-api-misuse") return evaluateSingleArgGuidance();
|
||||
if (failureKind === "browser-navigation-jitter") return "Use reloadStable(), gotoCurrentStable(), or safeReload() to retry the same browser navigation; inspect readiness.last, lastUrl, and screenshots before treating this as an API fetch failure.";
|
||||
if (failureKind === "navigation-failed") return "Inspect probe.readiness for selector/API readiness details and lastScreenshot for the browser state.";
|
||||
if (failureKind === "auth-failed") return "Inspect probe.auth sourceRef/fingerprint/status; do not print or copy the web login secret.";
|
||||
if (failureKind === "browser-failed") return "Inspect Playwright/browser-launcher availability on the target workspace.";
|
||||
@@ -1765,6 +1922,7 @@ function compactSummaryForStdout(summary) {
|
||||
nextAction: typeof value.nextAction === "string" ? compactText(value.nextAction, 1200) : value.nextAction ?? null,
|
||||
baseUrl: value.baseUrl ?? null,
|
||||
finalUrl: value.finalUrl ?? null,
|
||||
lastUrl: value.lastUrl ?? null,
|
||||
scriptSha256: value.scriptSha256 ?? null,
|
||||
runDir: value.runDir ?? runDir,
|
||||
reportPath: value.reportPath ?? null,
|
||||
@@ -1869,6 +2027,7 @@ function scriptIssueSummary(payload) {
|
||||
nextAction: ok ? null : issueNextAction(failureKind, payload),
|
||||
baseUrl: payload?.baseUrl ?? null,
|
||||
finalUrl: payload?.finalUrl ?? payload?.lastUrl ?? null,
|
||||
lastUrl: payload?.lastUrl ?? payload?.finalUrl ?? null,
|
||||
scriptSha256: payload?.scriptSha256 ?? null,
|
||||
runDir,
|
||||
reportPath: payload?.reportPath ?? null,
|
||||
@@ -1897,6 +2056,7 @@ function classifyIssueFailureKind(kind, message = "") {
|
||||
if (/composer-disabled/iu.test(text)) return "composer-disabled";
|
||||
if (/session-not-selected|session_required/iu.test(text)) return "session-not-selected";
|
||||
if (/same-origin-api-fetch-failed/iu.test(text)) return "same-origin-api-fetch-failed";
|
||||
if (/browser-navigation-jitter|browser-load-jitter|page\.(?:reload|goto)|gotoCurrentStable|reloadStable|safeReload|ERR_NETWORK_CHANGED|ERR_ABORTED|net::|navigation failed|domcontentloaded|chrome-error:\/\/chromewebdata/iu.test(text)) return "browser-navigation-jitter";
|
||||
if (/api|fetch|network|Failed to fetch|HTTP|status/iu.test(text)) return "network-or-api-fetch-bug";
|
||||
if (/script-api-misuse|page\.evaluate|safeEvaluate|Too many arguments/iu.test(text)) return "script-bug";
|
||||
if (/auth|login|credential/iu.test(text)) return "target-auth-bug";
|
||||
@@ -1910,6 +2070,7 @@ function issueNextAction(failureKind, payload) {
|
||||
if (failureKind === "auth-redirect-login") return "Inspect summary.finalUrl, auth/session bootstrap, and target login redirect handling before rerunning fresh-session.";
|
||||
if (failureKind === "composer-disabled" || failureKind === "session-not-selected") return "Inspect readiness.composer disabledReason and workspace/session summary; create or select a submit-capable session, then rerun the same probe.";
|
||||
if (failureKind === "same-origin-api-fetch-failed") return "Inspect summary.apiMatrix failed rows, current URL, auth state, and browser console/network evidence in reportPath.";
|
||||
if (failureKind === "browser-navigation-jitter") return "Use reloadStable(), gotoCurrentStable(), or safeReload() to retry the same URL/page reload with bounded attempts; inspect summary.finalUrl, summary.lastScreenshot, and readiness attempts instead of the API matrix first.";
|
||||
if (failureKind === "network-or-api-fetch-bug") return "Inspect summary.apiMatrix failed rows and retry the same node/lane after checking API availability.";
|
||||
if (failureKind === "script-bug") return "Fix the probe script or use safeEvaluate/safeFetchJson/fetchApiMatrix helpers, then rerun the same command.";
|
||||
if (failureKind === "target-auth-bug") return "Inspect credential sourceRef/fingerprint and target /auth/login state; do not print secrets.";
|
||||
|
||||
Reference in New Issue
Block a user