From b4d6a5a7544f49372aba83aa31966c98974159da Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:54:22 +0800 Subject: [PATCH] fix: require observer workbench readiness (#688) Co-authored-by: Codex --- scripts/src/hwlab-node-impl.ts | 3 ++- .../hwlab-node-web-observe-runner-source.ts | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 77835646..87b278b5 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -7866,7 +7866,7 @@ function renderWebObserveCollectTable(value: Record): string { if (jsonlTail.length > 0) { lines.push( "JSONL tail:", - webObserveTable(["TS", "TYPE", "COMMAND", "ATTEMPTS", "READY", "DOM", "PATH", "MESSAGE"], jsonlTail.map((item) => { + webObserveTable(["TS", "ROLE", "TYPE", "COMMAND", "ATTEMPTS", "READY", "DOM", "PATH", "MESSAGE"], jsonlTail.map((item) => { const error = record(item.error); const attempts = Array.isArray(error.attempts) ? error.attempts : []; const lastAttempt = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {}; @@ -7881,6 +7881,7 @@ function renderWebObserveCollectTable(value: Record): string { ].join(" "); return [ webObserveShort(webObserveText(item.ts), 24), + webObserveShort(webObserveText(item.pageRole ?? item.role), 12), webObserveShort(webObserveText(item.type), 20), webObserveShort(webObserveText(item.commandId), 28), webObserveText(attempts.length), diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index a999c451..4414ea5d 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -76,7 +76,13 @@ try { await runControlCommand({ id: "startup-goto", type: "goto", path: targetPath, createdAt: new Date().toISOString(), source: "startup" }, async () => gotoTarget(targetPath)); observerPage = await context.newPage(); attachPassiveListeners(observerPage, "observer", observerPageId); - await runControlCommand({ id: "startup-observer-goto", type: "observerGoto", path: targetPath, createdAt: new Date().toISOString(), source: "startup" }, async () => syncObserverPageToControlSession("startup")); + await runControlCommand({ id: "startup-observer-goto", type: "observerGoto", path: targetPath, createdAt: new Date().toISOString(), source: "startup" }, async () => { + const result = await syncObserverPageToControlSession("startup"); + if (!result?.ok) { + throw Object.assign(new Error("observer startup did not reach the target workbench route: " + (result?.failureKind || result?.reason || "observer-not-ready")), { details: result }); + } + return result; + }); terminalStatus = "running"; await writeManifest({ status: "running", auth: publicAuth(auth) }); await writeHeartbeat({ status: "running" }); @@ -269,18 +275,24 @@ async function syncObserverPageToControlSession(reason, explicitSessionId = null const snapshot = await workbenchSessionSnapshot(); const sessionId = explicitSessionId || snapshot?.activeSessionId || snapshot?.routeSessionId || routeSessionIdFromUrl(currentPageUrl()); const target = sessionId ? "/workbench/sessions/" + encodeURIComponent(sessionId) : targetPath; + const targetUrl = new URL(target, baseUrl).toString(); const beforeUrl = pageUrl(observerPage); const beforeSessionId = routeSessionIdFromUrl(beforeUrl); if (sessionId && beforeSessionId === sessionId && !forceRefresh) return { ok: true, reason, changed: false, observerRoundTrip: false, sessionId, beforeUrl, afterUrl: beforeUrl, pageRole: "observer", pageId: observerPageId }; let status = null; let statusText = null; - const response = await observerPage.goto(new URL(target, baseUrl).toString(), { waitUntil: "domcontentloaded", timeout: 45000 }).catch((error) => ({ observerGotoError: errorSummary(error) })); + const response = await observerPage.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 45000 }).catch((error) => ({ observerGotoError: errorSummary(error) })); if (response?.observerGotoError) return { ok: false, reason, changed: false, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, error: response.observerGotoError, valuesRedacted: true }; status = typeof response?.status === "function" ? response.status() : null; statusText = typeof response?.statusText === "function" ? response.statusText() : null; + const readiness = await waitForTargetPageReady(observerPage, targetUrl, { timeoutMs: 15000 }); + if (!readiness.ok) { + lastObserverRefreshAtMs = Date.now(); + return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, readiness, hydration: null, failureKind: readiness.reason || "observer-target-not-ready", valuesRedacted: true }; + } const hydration = await waitForWorkbenchSessionHydrated(observerPage, sessionId, { timeoutMs: 15000 }); lastObserverRefreshAtMs = Date.now(); - return { ok: true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, hydration, valuesRedacted: true }; + return { ok: hydration.ok === true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, readiness, hydration, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", valuesRedacted: true }; } async function waitForWorkbenchSessionHydrated(targetPage, sessionId, options = {}) { @@ -291,8 +303,9 @@ async function waitForWorkbenchSessionHydrated(targetPage, sessionId, options = while (Date.now() <= deadline) { last = await workbenchSessionSnapshot(targetPage); const expected = String(sessionId || "").trim(); - const routeOk = !expected || last?.routeSessionId === expected; - const activeOk = !expected || last?.activeSessionId === expected; + const observedPath = safeUrlPath(last?.url || pageUrl(targetPage)); + const routeOk = expected ? last?.routeSessionId === expected : isWorkbenchPathname(observedPath || ""); + const activeOk = expected ? last?.activeSessionId === expected : isWorkbenchPathname(observedPath || ""); if (routeOk && activeOk) return { ok: true, durationMs: Date.now() - started, snapshot: last, valuesRedacted: true }; await targetPage.waitForTimeout(250).catch(() => {}); }