diff --git a/scripts/src/hwlab-node-web-observe-analyzer-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-source.ts index ccb49546..841ea0f6 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-source.ts @@ -57,9 +57,10 @@ const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvent const apiDomLag = buildApiDomLagReport(samples, network); const projectManagement = buildProjectManagementReport(samples, control, network, pagePerformance, projectManagementConfig); const runnerErrors = errors.slice(-8).map((item) => { - const attempts = Array.isArray(item.error?.attempts) ? item.error.attempts : []; + const details = item.error?.details && typeof item.error.details === "object" ? item.error.details : {}; + const attempts = Array.isArray(item.error?.attempts) ? item.error.attempts : Array.isArray(details.attempts) ? details.attempts : []; const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null; - const readiness = lastAttempt?.readiness || item.error?.navigationReadiness || null; + const readiness = lastAttempt?.readiness || lastAttempt?.readinessBeforeClick || details.readinessBeforeClick || details.readinessAfterWait || item.error?.navigationReadiness || null; const readinessSnapshot = readiness?.snapshot || readiness; return { ts: item.ts ?? null, @@ -78,7 +79,13 @@ const runnerErrors = errors.slice(-8).map((item) => { path: readinessSnapshot.path ?? null, readyState: readinessSnapshot.readyState ?? null, workbenchShellVisible: readinessSnapshot.workbenchShellVisible === true, + sessionCreatePresent: readinessSnapshot.sessionCreatePresent === true, sessionCreateVisible: readinessSnapshot.sessionCreateVisible === true, + sessionRailPresent: readinessSnapshot.sessionRailPresent === true, + sessionRailCollapsed: readinessSnapshot.sessionRailCollapsed ?? null, + sessionCollapseTogglePresent: readinessSnapshot.sessionCollapseTogglePresent === true, + sessionCollapseToggleVisible: readinessSnapshot.sessionCollapseToggleVisible === true, + sessionCollapseToggleExpanded: readinessSnapshot.sessionCollapseToggleExpanded ?? null, commandInputPresent: readinessSnapshot.commandInputPresent === true, activeTabPresent: readinessSnapshot.activeTabPresent === true, warningPresent: readinessSnapshot.warningPresent === true, diff --git a/scripts/src/hwlab-node-web-observe-render.ts b/scripts/src/hwlab-node-web-observe-render.ts index 9ca4111c..3b65ae3c 100644 --- a/scripts/src/hwlab-node-web-observe-render.ts +++ b/scripts/src/hwlab-node-web-observe-render.ts @@ -399,13 +399,18 @@ function renderWebObserveCollectTable(value: Record): string { "JSONL tail:", webObserveTable(["TS", "ROLE", "TYPE", "COMMAND", "MSG", "TRACE", "TRACE_IDS", "MSG_STATUS", "LOAD", "ATTEMPTS", "READY", "DOM", "PATH", "MESSAGE"], jsonlTail.map((item) => { const error = record(item.error); - const attempts = Array.isArray(error.attempts) ? error.attempts : []; + const details = record(error.details); + const attempts = Array.isArray(error.attempts) ? error.attempts : Array.isArray(details.attempts) ? details.attempts : []; const lastAttempt = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {}; - const rawReadiness = record(lastAttempt.readiness ?? error.navigationReadiness); + const rawReadiness = record(lastAttempt.readiness ?? lastAttempt.readinessBeforeClick ?? details.readinessBeforeClick ?? details.readinessAfterWait ?? error.navigationReadiness); const readiness = record(rawReadiness.snapshot ?? rawReadiness); + const createState = readiness.sessionCreateVisible === true ? "Y" : readiness.sessionCreatePresent === true ? "h" : "n"; + const railState = readiness.sessionRailCollapsed === true ? "C" : readiness.sessionRailCollapsed === false ? "O" : "-"; const domBits = [ `shell=${readiness.workbenchShellVisible === true ? "Y" : "n"}`, - `create=${readiness.sessionCreateVisible === true ? "Y" : "n"}`, + `create=${createState}`, + `rail=${railState}`, + `toggle=${readiness.sessionCollapseToggleVisible === true ? "Y" : "n"}`, `input=${readiness.commandInputPresent === true ? "Y" : "n"}`, `tab=${readiness.activeTabPresent === true ? "Y" : "n"}`, `login=${readiness.loginVisible === true ? "Y" : "n"}`, diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 9039dd95..83548fa0 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1188,12 +1188,21 @@ async function workbenchReadinessSnapshot(targetPage) { const style = window.getComputedStyle(element); return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none"; }; + const sessionCreate = document.querySelector("#session-create"); + const sessionRail = document.querySelector("#session-sidebar"); + const sessionCollapseToggle = document.querySelector("#session-collapse-toggle"); 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")), + sessionCreatePresent: Boolean(sessionCreate), + sessionCreateVisible: visible(sessionCreate), + sessionRailPresent: Boolean(sessionRail), + sessionRailCollapsed: sessionRail ? sessionRail.getAttribute("data-collapsed") === "true" || sessionRail.classList.contains("is-collapsed") : null, + sessionCollapseTogglePresent: Boolean(sessionCollapseToggle), + sessionCollapseToggleVisible: visible(sessionCollapseToggle), + sessionCollapseToggleExpanded: sessionCollapseToggle ? sessionCollapseToggle.getAttribute("aria-expanded") : null, 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")), @@ -1280,6 +1289,37 @@ function requestFailureSummary(request) { }; } +async function ensureSessionRailExpanded() { + const before = await workbenchReadinessSnapshot(page); + if (before?.sessionCreateVisible === true) { + return { ok: true, action: "already-visible", before, after: before, valuesRedacted: true }; + } + const toggle = page.locator("#session-collapse-toggle").first(); + const toggleVisible = await toggle.isVisible({ timeout: 2000 }).catch(() => false); + if (before?.sessionRailCollapsed !== true || !toggleVisible) { + return { ok: false, action: "not-expanded", reason: before?.sessionRailCollapsed === true ? "collapse-toggle-not-visible" : "session-rail-not-collapsed", before, after: before, valuesRedacted: true }; + } + await toggle.click(); + await page.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 rail = document.querySelector("#session-sidebar"); + return Boolean(visible(document.querySelector("#session-create")) || (rail && rail.getAttribute("data-collapsed") === "false")); + }, null, { timeout: 5000 }).catch(() => null); + const after = await workbenchReadinessSnapshot(page); + return { + ok: after?.sessionCreateVisible === true, + action: "expanded-session-rail", + before, + after, + valuesRedacted: true + }; +} + async function clickAndWaitForAgentSessionCreate(create) { let removeRequestFailedListener = () => {}; const requestFailedPromise = new Promise((resolve) => { @@ -1316,9 +1356,17 @@ async function createSessionFromUi() { const attempts = []; let createResponse = null; for (let attempt = 1; attempt <= 2; attempt += 1) { - const readinessBeforeClick = await workbenchReadinessSnapshot(page); + const railExpansion = await ensureSessionRailExpanded(); + const readinessBeforeClick = railExpansion.after || await workbenchReadinessSnapshot(page); const create = page.locator("#session-create").first(); - await create.waitFor({ state: "visible", timeout: 15000 }); + try { + await create.waitFor({ state: "visible", timeout: 15000 }); + } catch (error) { + const readinessAfterWait = await workbenchReadinessSnapshot(page); + const createError = new Error("newSession session create button is not visible"); + createError.details = { beforeUrl, afterUrl: currentPageUrl(), before, attempts, attempt, readinessBeforeClick, readinessAfterWait, railExpansion, waitError: errorSummary(error), pageId, valuesRedacted: true }; + throw createError; + } const createButtonState = await create.evaluate((element) => { const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); @@ -1335,7 +1383,7 @@ async function createSessionFromUi() { const outcome = await clickAndWaitForAgentSessionCreate(create); if (outcome?.kind === "response") { createResponse = outcome.response; - attempts.push({ attempt, outcome: "response", readinessBeforeClick, createButtonState, valuesRedacted: true }); + attempts.push({ attempt, outcome: "response", readinessBeforeClick, railExpansion, createButtonState, valuesRedacted: true }); break; } const afterAttempt = await workbenchSessionSnapshot(); @@ -1343,6 +1391,7 @@ async function createSessionFromUi() { attempt, outcome: outcome?.kind || "unknown", readinessBeforeClick, + railExpansion, createButtonState, waitError: outcome?.waitError || null, requestFailure: outcome?.requestFailure || null, diff --git a/scripts/src/hwlab-node-web-probe-runner-source.ts b/scripts/src/hwlab-node-web-probe-runner-source.ts index 693b2c86..98ca0e18 100644 --- a/scripts/src/hwlab-node-web-probe-runner-source.ts +++ b/scripts/src/hwlab-node-web-probe-runner-source.ts @@ -777,9 +777,10 @@ async function ensureWorkbenchComposerReady(options = {}) { } async function createProbeSessionFromUi(options = {}) { + const railExpansion = await ensureSessionRailExpandedForProbe(); const create = page.locator("#session-create").first(); if (!(await create.isVisible({ timeout: Math.min(timeoutMs, 5000) }).catch(() => false))) { - return { ok: false, method: "ui-click", reason: "session-create-not-visible" }; + return { ok: false, method: "ui-click", reason: "session-create-not-visible", railExpansion }; } const before = await collectWorkbenchReadyState(); await create.click(); @@ -797,10 +798,36 @@ async function createProbeSessionFromUi(options = {}) { method: "ui-click", before: before.workspace, after: after.workspace, - composer: after.composer + composer: after.composer, + railExpansion }; } +async function ensureSessionRailExpandedForProbe() { + const before = await collectWorkbenchReadyState(); + if (before?.workspace?.sessionCreateVisible === true) { + return { ok: true, action: "already-visible", before: before.workspace, after: before.workspace, valuesRedacted: true }; + } + const toggle = page.locator("#session-collapse-toggle").first(); + const toggleVisible = await toggle.isVisible({ timeout: Math.min(timeoutMs, 2000) }).catch(() => false); + if (before?.workspace?.sessionRailCollapsed !== true || !toggleVisible) { + return { ok: false, action: "not-expanded", reason: before?.workspace?.sessionRailCollapsed === true ? "collapse-toggle-not-visible" : "session-rail-not-collapsed", before: before.workspace, after: before.workspace, valuesRedacted: true }; + } + await toggle.click(); + await page.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 rail = document.querySelector("#session-sidebar"); + return Boolean(visible(document.querySelector("#session-create")) || (rail && rail.getAttribute("data-collapsed") === "false")); + }, null, { timeout: Math.min(timeoutMs, 5000) }).catch(() => null); + const after = await collectWorkbenchReadyState(); + return { ok: after?.workspace?.sessionCreateVisible === true, action: "expanded-session-rail", before: before.workspace, after: after.workspace, valuesRedacted: true }; +} + async function collectWorkbenchReadyState() { return page.evaluate(() => { const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']"); @@ -808,6 +835,15 @@ async function collectWorkbenchReadyState() { const input = document.querySelector("#command-input"); const send = document.querySelector("#command-send"); const form = document.querySelector("#command-form"); + 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 sessionCreate = document.querySelector("#session-create"); + const sessionRail = document.querySelector("#session-sidebar"); + const sessionCollapseToggle = document.querySelector("#session-collapse-toggle"); const warning = document.querySelector(".composer-warning")?.textContent?.trim() || null; const mode = document.querySelector(".composer-mode")?.textContent?.trim() || null; const draft = input && typeof input.value === "string" ? input.value : ""; @@ -831,7 +867,14 @@ async function collectWorkbenchReadyState() { activeSessionId: sessionId, activeConversationId: conversationId, activeStatus: activeTab?.getAttribute("data-status") || null, - tabCount: document.querySelectorAll(".session-tab").length + tabCount: document.querySelectorAll(".session-tab").length, + sessionCreatePresent: Boolean(sessionCreate), + sessionCreateVisible: visible(sessionCreate), + sessionRailPresent: Boolean(sessionRail), + sessionRailCollapsed: sessionRail ? sessionRail.getAttribute("data-collapsed") === "true" || sessionRail.classList.contains("is-collapsed") : null, + sessionCollapseTogglePresent: Boolean(sessionCollapseToggle), + sessionCollapseToggleVisible: visible(sessionCollapseToggle), + sessionCollapseToggleExpanded: sessionCollapseToggle ? sessionCollapseToggle.getAttribute("aria-expanded") : null }, composer: { ready: composerReady, diff --git a/scripts/src/hwlab-node/web-probe-observe.ts b/scripts/src/hwlab-node/web-probe-observe.ts index f6ae5658..53ad3f8c 100644 --- a/scripts/src/hwlab-node/web-probe-observe.ts +++ b/scripts/src/hwlab-node/web-probe-observe.ts @@ -1782,8 +1782,8 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption "const slimDomSample = (item) => { const v = objectOrNull(item) || {}; return { seq: v.seq ?? null, ts: v.ts ?? null, source: clip(v.source, 32), diagnosticCode: clip(v.diagnosticCode, 48), traceId: clip(v.traceId, 64), httpStatus: v.httpStatus ?? null, idleSeconds: v.idleSeconds ?? null, waitingFor: clip(v.waitingFor, 48), lastEventLabel: clip(v.lastEventLabel, 80), text: clip(v.text ?? v.preview, 180) }; };", "const slimConsoleGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), lastAt: v.lastAt ?? v.firstAt ?? null, firstAt: v.firstAt ?? null, traceIds: Array.isArray(v.traceIds) ? v.traceIds.slice(0, 3).map((x) => clip(x, 64)) : [] }; };", "const slimConsoleSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), traceId: clip(v.traceId, 64), text: clip(v.text ?? v.preview, 180) }; };", - "const slimRunnerError = (item) => { const v = objectOrNull(item) || {}; const readiness = objectOrNull(v.lastReadiness); return { ts: v.ts ?? null, type: clip(v.type, 32), commandId: clip(v.commandId, 80), sampleSeq: v.sampleSeq ?? null, message: clip(v.message, 240), retry: clip(v.retry, 24), retryExhausted: v.retryExhausted === true, lastError: clip(v.lastError, 180), attemptCount: v.attemptCount ?? null, lastFailureKind: clip(v.lastFailureKind, 48), lastReadinessReason: clip(v.lastReadinessReason, 48), lastReadiness: readiness ? { reason: clip(readiness.reason, 48), path: clip(readiness.path, 96), readyState: clip(readiness.readyState, 24), workbenchShellVisible: readiness.workbenchShellVisible === true, sessionCreateVisible: readiness.sessionCreateVisible === true, commandInputPresent: readiness.commandInputPresent === true, activeTabPresent: readiness.activeTabPresent === true, warningPresent: readiness.warningPresent === true, loginVisible: readiness.loginVisible === true, bodyTextHash: clip(readiness.bodyTextHash, 80) } : null }; };", - "const slimRunnerErrorFromJsonl = (item) => { const v = objectOrNull(item) || {}; const error = objectOrNull(v.error) || {}; const attempts = Array.isArray(error.attempts) ? error.attempts : []; const lastAttempt = attempts.length > 0 ? objectOrNull(attempts[attempts.length - 1]) || {} : {}; const rawReadiness = objectOrNull(lastAttempt.readiness) || objectOrNull(error.navigationReadiness); const readiness = objectOrNull(rawReadiness?.snapshot) || rawReadiness; return { ts: v.ts ?? null, type: v.type ?? null, commandId: v.commandId ?? null, sampleSeq: v.sampleSeq ?? null, message: error.message ?? v.message ?? null, attemptCount: attempts.length, lastFailureKind: lastAttempt.failureKind ?? null, lastReadinessReason: rawReadiness?.reason ?? readiness?.reason ?? null, lastReadiness: readiness ?? null }; };", + "const slimRunnerError = (item) => { const v = objectOrNull(item) || {}; const readiness = objectOrNull(v.lastReadiness); return { ts: v.ts ?? null, type: clip(v.type, 32), commandId: clip(v.commandId, 80), sampleSeq: v.sampleSeq ?? null, message: clip(v.message, 240), retry: clip(v.retry, 24), retryExhausted: v.retryExhausted === true, lastError: clip(v.lastError, 180), attemptCount: v.attemptCount ?? null, lastFailureKind: clip(v.lastFailureKind, 48), lastReadinessReason: clip(v.lastReadinessReason, 48), lastReadiness: readiness ? { reason: clip(readiness.reason, 48), path: clip(readiness.path, 96), readyState: clip(readiness.readyState, 24), workbenchShellVisible: readiness.workbenchShellVisible === true, sessionCreatePresent: readiness.sessionCreatePresent === true, sessionCreateVisible: readiness.sessionCreateVisible === true, sessionRailPresent: readiness.sessionRailPresent === true, sessionRailCollapsed: readiness.sessionRailCollapsed ?? null, sessionCollapseTogglePresent: readiness.sessionCollapseTogglePresent === true, sessionCollapseToggleVisible: readiness.sessionCollapseToggleVisible === true, sessionCollapseToggleExpanded: readiness.sessionCollapseToggleExpanded ?? null, commandInputPresent: readiness.commandInputPresent === true, activeTabPresent: readiness.activeTabPresent === true, warningPresent: readiness.warningPresent === true, loginVisible: readiness.loginVisible === true, bodyTextHash: clip(readiness.bodyTextHash, 80) } : null }; };", + "const slimRunnerErrorFromJsonl = (item) => { const v = objectOrNull(item) || {}; const error = objectOrNull(v.error) || {}; const details = objectOrNull(error.details) || {}; const attempts = Array.isArray(error.attempts) ? error.attempts : Array.isArray(details.attempts) ? details.attempts : []; const lastAttempt = attempts.length > 0 ? objectOrNull(attempts[attempts.length - 1]) || {} : {}; const rawReadiness = objectOrNull(lastAttempt.readiness) || objectOrNull(lastAttempt.readinessBeforeClick) || objectOrNull(details.readinessBeforeClick) || objectOrNull(details.readinessAfterWait) || objectOrNull(error.navigationReadiness); const readiness = objectOrNull(rawReadiness?.snapshot) || rawReadiness; return { ts: v.ts ?? null, type: v.type ?? null, commandId: v.commandId ?? null, sampleSeq: v.sampleSeq ?? null, message: error.message ?? v.message ?? null, attemptCount: attempts.length, lastFailureKind: lastAttempt.failureKind ?? null, lastReadinessReason: rawReadiness?.reason ?? readiness?.reason ?? null, lastReadiness: readiness ?? null }; };", "const slimCommandFailure = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, commandId: clip(v.commandId, 80), type: clip(v.type, 32), source: clip(v.source, 24), durationMs: v.durationMs ?? null, beforePath: clip(v.beforePath, 80), afterPath: clip(v.afterPath, 80), name: clip(v.name, 48), failureKind: clip(v.failureKind, 48), sampleSeq: v.sampleSeq ?? null, failureSampleOk: v.failureSampleOk === true, message: clip(v.message, 240) }; };", "const slimJump = (item) => { const v = objectOrNull(item) || {}; return { columnLabel: v.columnLabel ?? null, pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, fromSeq: v.fromSeq ?? null, toSeq: v.toSeq ?? null, fromValue: v.fromValue ?? null, toValue: v.toValue ?? null, delta: v.delta ?? null, sampleDeltaSeconds: v.sampleDeltaSeconds ?? null, allowedIncreaseSeconds: v.allowedIncreaseSeconds ?? null, traceId: v.traceId ?? null }; };", "const slimTraceOrderAnomaly = (item) => { const v = objectOrNull(item) || {}; return { sampleIndex: v.sampleIndex ?? v.seq ?? null, seq: v.seq ?? null, timestamp: v.timestamp ?? v.ts ?? null, pageRole: clip(v.pageRole, 24), traceId: clip(v.traceId, 64), previousRowIndex: v.previousRowIndex ?? null, currentRowIndex: v.currentRowIndex ?? null, reasons: Array.isArray(v.reasons) ? v.reasons.slice(0, 6).map((x) => clip(x, 48)) : [], previousTotalSeconds: v.previousTotalSeconds ?? null, currentTotalSeconds: v.currentTotalSeconds ?? null, previousClockSeconds: v.previousClockSeconds ?? null, currentClockSeconds: v.currentClockSeconds ?? null, previousPreview: clip(v.previousPreview, 180), currentPreview: clip(v.currentPreview, 180) }; };",