#!/usr/bin/env node "use strict"; const fs = require("node:fs"); const path = require("node:path"); const { chromium } = require("playwright"); const { BASE_URL, UI_TIMEOUT_MS, ensureAuthenticated, getViewerIdentity, } = require("./tree-shell-smoke-helpers"); const TASK = "task773-page-ai-openhub-browser-smoke"; const OUTPUT_DIR = path.join(process.cwd(), "tmp", "7-68-runtime"); const SCREENSHOT_PATH = path.join(OUTPUT_DIR, "page-ai-openhub-browser.png"); const RESULT_PATH = path.join(OUTPUT_DIR, "page-ai-openhub-browser-result.json"); const TEST_EMAIL = "mnote.e2e@example.com"; const TEST_PASSWORD = "MnoteE2E123!"; const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || ["/usr/bin/chromium-browser", "/usr/bin/chromium", "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome"] .find((candidate) => fs.existsSync(candidate)); class SmokeFailure extends Error { constructor(kind, message, details = {}) { super(message); this.name = "SmokeFailure"; this.kind = kind; this.details = details; } } function visibleSelectorScript(selectors) { return selectors.some((selector) => { const nodes = Array.from(document.querySelectorAll(selector)); return nodes.some((node) => { if (!(node instanceof HTMLElement)) return false; const style = window.getComputedStyle(node); const rect = node.getBoundingClientRect(); return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0; }); }); } async function assertServiceReachable(baseUrl) { let response; try { response = await fetch(`${baseUrl}/health`, { redirect: "manual", signal: AbortSignal.timeout(6_000) }); } catch (error) { throw new SmokeFailure("service_unreachable", `MNote 3000 服务不可达:${error.message}`, { baseUrl }); } if (!response.ok && response.status !== 303) { throw new SmokeFailure("service_unreachable", `MNote /health 返回异常:HTTP ${response.status}`, { baseUrl }); } } async function loginWithUiFirst(page, requestContext) { await page.goto(`${BASE_URL}/auth`, { waitUntil: "commit", timeout: UI_TIMEOUT_MS }); if (!page.url().includes("/auth")) { return getViewerIdentity(requestContext); } const quickLogin = page.getByRole("button", { name: "测试账号快速登录" }); if (await quickLogin.isVisible({ timeout: 8_000 }).catch(() => false)) { await quickLogin.click({ timeout: UI_TIMEOUT_MS }); await page.waitForURL((url) => !url.toString().includes("/auth"), { timeout: UI_TIMEOUT_MS, waitUntil: "commit" }).catch(() => undefined); } if (page.url().includes("/auth")) { const account = page.locator('input[name="account"], input[type="email"], input[data-auth-field="account"]').first(); const password = page.locator('input[name="password"], input[type="password"]').first(); const submit = page.getByRole("button", { name: /^登录$|账号登录|登录$/ }).first(); if (!(await account.isVisible({ timeout: 2_000 }).catch(() => false)) || !(await password.isVisible({ timeout: 2_000 }).catch(() => false))) { throw new SmokeFailure("auth_failed", "认证页未出现快速登录,也找不到账号密码输入框", { url: page.url() }); } await account.fill(TEST_EMAIL, { timeout: UI_TIMEOUT_MS }); await password.fill(TEST_PASSWORD, { timeout: UI_TIMEOUT_MS }); await submit.click({ timeout: UI_TIMEOUT_MS }); await page.waitForURL((url) => !url.toString().includes("/auth"), { timeout: UI_TIMEOUT_MS, waitUntil: "commit" }).catch(() => undefined); } if (page.url().includes("/auth")) { throw new SmokeFailure("auth_failed", "测试账号登录后仍停留在 /auth", { url: page.url() }); } try { return await getViewerIdentity(requestContext); } catch (error) { throw new SmokeFailure("auth_failed", `登录后 whoami 仍不可用:${error.message}`, { url: page.url() }); } } async function waitForVisibleAny(page, selectors, label) { try { await page.waitForFunction(visibleSelectorScript, selectors, { timeout: UI_TIMEOUT_MS }); } catch (error) { throw new SmokeFailure("selector_missing", `${label} 不可见。候选 selector: ${selectors.join(", ")}`, { selectors, cause: error.message, }); } } async function openPageAiDrawer(page) { await page.goto(BASE_URL, { waitUntil: "commit", timeout: UI_TIMEOUT_MS }); if (page.url().includes("/auth")) { throw new SmokeFailure("auth_failed", "打开主页后被重定向到 /auth,登录态未生效", { url: page.url() }); } await page.evaluate(() => { try { localStorage.removeItem("mnote.page_ai.openhub_host"); localStorage.setItem("mnote.page_ai.openhub_host", "1"); } catch {} }); await waitForVisibleAny(page, ["[data-testid='wolai-floating-ai']", "[data-testid='wolai-page-ai-drawer']"], "Page AI 入口"); const drawerVisible = await page.locator("[data-testid='wolai-page-ai-drawer']").first().isVisible().catch(() => false); if (!drawerVisible) { await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS }); } await waitForVisibleAny(page, ["[data-testid='wolai-page-ai-drawer']"], "Page AI drawer"); } async function validateOpenHubQuickActions(page) { const frameHandle = await page.locator("iframe[data-page-ai-openhub-iframe]").elementHandle({ timeout: UI_TIMEOUT_MS }); const frame = frameHandle ? await frameHandle.contentFrame() : null; if (!frame) { throw new SmokeFailure("quick_action_failed", "OpenHub iframe frame 不可用", { reason: "iframe_frame_missing" }); } const tabButton = frame.locator("[data-mnote-openhub-current-tab-toggle]").first(); const folderButton = frame.locator("[data-mnote-openhub-current-folder-toggle]").first(); await tabButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await folderButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); const fillVisibleInput = async (value) => frame.evaluate((nextValue) => { const selectors = [ "textarea[placeholder*='输入']", "textarea", "[contenteditable='true']", "input[type='text'][placeholder*='输入']", "input[type='text']", ".ant-input", ]; const visible = (node) => { if (!(node instanceof HTMLElement)) return false; const style = window.getComputedStyle(node); const rect = node.getBoundingClientRect(); return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0; }; for (const selector of selectors) { const nodes = Array.from(document.querySelectorAll(selector)).filter(visible); if (!nodes.length) continue; const node = nodes[nodes.length - 1]; node.focus(); if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) { const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), "value"); if (descriptor && typeof descriptor.set === "function") { descriptor.set.call(node, nextValue); } else { node.value = nextValue; } } else { node.textContent = nextValue; } node.dispatchEvent(new InputEvent("input", { bubbles: true, data: nextValue, inputType: "insertText" })); node.dispatchEvent(new Event("change", { bubbles: true })); return true; } return false; }, value); const readVisibleInput = async () => frame.evaluate(() => { const selectors = [ "textarea[placeholder*='输入']", "textarea", "[contenteditable='true']", "input[type='text'][placeholder*='输入']", "input[type='text']", ".ant-input", ]; const visible = (node) => { if (!(node instanceof HTMLElement)) return false; const style = window.getComputedStyle(node); const rect = node.getBoundingClientRect(); return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0; }; for (const selector of selectors) { const nodes = Array.from(document.querySelectorAll(selector)).filter(visible); if (!nodes.length) continue; const node = nodes[nodes.length - 1]; if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) return node.value || ""; return node.textContent || ""; } return ""; }); const hasInput = await fillVisibleInput(""); if (!hasInput) { throw new SmokeFailure("quick_action_failed", "OpenHub iframe 内未找到可见聊天输入框", {}); } const prompt = `MNOTE_NATIVE_CONTEXT_SMOKE_${Date.now()}`; let capturedBody = null; await page.route("**/page-ai/openhub/ai/api/query/stream**", async (route) => { capturedBody = JSON.parse(route.request().postData() || "{}"); await route.fulfill({ status: 200, headers: { "content-type": "text/event-stream; charset=utf-8" }, body: [ `data: ${JSON.stringify({ type: "session", conversation_id: "task773-native-context", done: false })}`, "", `data: ${JSON.stringify({ type: "message_complete", done: true })}`, "", ].join("\n"), }); }); await fillVisibleInput(prompt); await tabButton.click({ timeout: UI_TIMEOUT_MS }); const inputTextAfterToggle = (await readVisibleInput()).trim(); const tabSelected = await tabButton.evaluate((node) => node.classList.contains("ant-btn-primary") || node.getAttribute("type") === "button" && node.matches(".ant-btn-primary")); const folderSelectedAfterTab = await folderButton.evaluate((node) => node.classList.contains("ant-btn-primary")); await page.keyboard.press("Enter"); await page.waitForFunction(() => window.__mnoteOpenHubTask773RequestCaptured === true, undefined, { timeout: 100 }).catch(() => undefined); const started = Date.now(); while (!capturedBody && Date.now() - started < UI_TIMEOUT_MS) { await page.waitForTimeout(100); } const activeEditor = await page.evaluate(() => window.__mnoteDocumentPaneRuntime && typeof window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot === "function" ? window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot()?.activeEditor || null : null); const result = { ok: Boolean(capturedBody && capturedBody.mnote_context && capturedBody.mnote_context.value), tabButtonVisible: await tabButton.isVisible().catch(() => false), folderButtonVisible: await folderButton.isVisible().catch(() => false), tabSelected, folderSelectedAfterTab, inputTextAfterToggle, requestQuestion: capturedBody && capturedBody.question, mnoteContext: capturedBody && capturedBody.mnote_context, activeEditor, }; if (!result.ok) { throw new SmokeFailure("quick_action_failed", "OpenHub 当前 Tab/文件夹上下文未随发送请求进入后台", result); } if (!result.tabSelected || result.folderSelectedAfterTab) { throw new SmokeFailure("quick_action_selection_invalid", "当前 Tab 按钮没有呈现单选选中态", result); } if (result.inputTextAfterToggle !== prompt) { throw new SmokeFailure("quick_action_leaked_to_input", "当前 Tab/文件夹地址不应直接写入用户可见输入框", result); } if (result.mnoteContext.kind !== "tab" || !/^https?:\/\/.+\/documents\//.test(result.mnoteContext.value)) { throw new SmokeFailure("quick_action_tab_context_invalid", "当前 Tab 发送上下文不是 MNote 文档地址", result); } return result; } async function collectState(page) { return page.evaluate(() => { const visible = (node) => { if (!node || node.nodeType !== 1) return false; const ownerWindow = node.ownerDocument && node.ownerDocument.defaultView ? node.ownerDocument.defaultView : window; const style = ownerWindow.getComputedStyle(node); const rect = node.getBoundingClientRect(); return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0; }; const rectOf = (node) => { if (!node || node.nodeType !== 1 || typeof node.getBoundingClientRect !== "function") return null; const rect = node.getBoundingClientRect(); return { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height), top: Math.round(rect.top), bottom: Math.round(rect.bottom), }; }; const visibleElements = (selector) => Array.from(document.querySelectorAll(selector)).filter(visible); const visibleAny = (selector) => visibleElements(selector).length > 0; const text = (selector) => (document.querySelector(selector)?.textContent || "").trim(); const iframe = document.querySelector("iframe[data-page-ai-openhub-iframe]"); const iframeDoc = iframe instanceof HTMLIFrameElement ? iframe.contentDocument : null; const iframeBodyText = (iframeDoc && iframeDoc.body ? iframeDoc.body.textContent || "" : "").trim(); const iframeShellKind = iframeDoc && iframeDoc.body ? iframeDoc.body.getAttribute("data-mnote-openhub-ai-shell") || "" : ""; const drawer = Array.from(document.querySelectorAll("[data-testid='wolai-page-ai-drawer']")).find(visible) || null; const openhubHost = document.querySelector("[data-page-ai-openhub-host='true']"); const outerOpenHubHeaderVisible = visibleAny(".wolai-page-ai-opencode-header") || visibleAny(".wolai-page-ai-header-copy") || visibleAny("[data-page-ai-openhub-runtime-status]"); const diagnostics = document.querySelector("[data-page-ai-openhub-bootstrap-copy]"); const diagnosticsVisible = Boolean(diagnostics && visible(diagnostics)); const diagnosticsRect = diagnosticsVisible ? rectOf(diagnostics) : null; const diagnosticsOpen = diagnostics instanceof HTMLDetailsElement ? diagnostics.open : false; const debugChromeRects = diagnosticsVisible ? [{ selector: "[data-page-ai-openhub-bootstrap-copy]", text: (diagnostics.textContent || "").replace(/\s+/g, " ").trim().slice(0, 160), rect: diagnosticsRect, open: diagnosticsOpen, }] : []; const debugChromeTotalHeight = diagnosticsRect?.height || 0; const fallbackActionSelector = "[data-page-ai-action='openhub-use-opencode-fallback']"; const fallbackActionVisibleInIframe = iframeDoc ? Array.from(iframeDoc.querySelectorAll(fallbackActionSelector)).some(visible) : false; const pageText = (document.body.textContent || "").replace(/\s+/g, " ").trim(); const loginTextPattern = /(登录\s*OpenHub|OpenHub\s*Login|WeKnora\s*登录|登录\s*WeKnora|Sign in to OpenHub|OpenHub account|WeKnora account)/i; const reactSelectorMarkers = [ "[data-openhub-ai-panel]", "[data-testid='openhub-ai-panel']", "[data-testid='openhub-chat']", "[data-openhub-conversation]", "[data-openhub-session-history]", ".openhub-ai-panel", ".chat-message-list", ".ant-layout", ".ant-menu", ".ant-input", ].filter((selector) => Array.from(document.querySelectorAll(selector)).some(visible) || (iframeDoc && Array.from(iframeDoc.querySelectorAll(selector)).some(visible))); const reactTextMarkers = [ "OpenHub 平台", "开始对话", "历史记录", "技能管理", "选择模型", ].filter((marker) => iframeBodyText.includes(marker)); const conflictEntryPattern = /(文件管理|知识库|时光机|智能体|协作任务|团队状态)/; const quickActionTab = iframeDoc ? iframeDoc.querySelector("[data-mnote-openhub-current-tab-toggle]") : null; const quickActionFolder = iframeDoc ? iframeDoc.querySelector("[data-mnote-openhub-current-folder-toggle]") : null; const smokeInput = iframeDoc ? iframeDoc.querySelector("[data-mnote-openhub-smoke-input]") : null; return { url: location.href, title: document.title, bodySnippet: pageText.slice(0, 800), drawerVisible: Boolean(drawer), drawerRect: rectOf(drawer), openhubHost: Boolean(openhubHost), openhubHostRect: rectOf(openhubHost), outerOpenHubHeaderVisible, debugChromeVisible: debugChromeRects.length > 0, debugChromeRects, debugChromeTotalHeight, debugChromeSqueezesContent: diagnosticsOpen || debugChromeTotalHeight > 80, hostChromeVisible: Boolean(Array.from(document.querySelectorAll("[data-page-ai-openhub-bootstrap-copy]")).find(visible)), iframeVisible: iframe instanceof HTMLIFrameElement && visible(iframe), iframeRect: rectOf(iframe), iframeSrc: iframe instanceof HTMLIFrameElement ? iframe.getAttribute("src") || "" : "", iframeBodySnippet: iframeBodyText.slice(0, 800), iframeShellKind, runtimeStatus: text("[data-page-ai-openhub-runtime-status]"), authTruth: text("[data-page-ai-openhub-auth-truth]"), workspaceScope: text("[data-page-ai-openhub-workspace-scope]"), routeGuard: text("[data-page-ai-openhub-route-guard]"), fallback: text("[data-page-ai-openhub-fallback]"), loginPageVisible: loginTextPattern.test(pageText) || loginTextPattern.test(iframeBodyText), staticBoundaryVisible: iframeShellKind === "static-boundary" || /静态占位|static-boundary|最小 host\/bootstrap 占位/.test(iframeBodyText), fallbackActionVisible: visibleAny(fallbackActionSelector) || fallbackActionVisibleInIframe, fallbackActionCount: document.querySelectorAll(fallbackActionSelector).length + (iframeDoc ? iframeDoc.querySelectorAll(fallbackActionSelector).length : 0), reactAiMarkers: [...reactSelectorMarkers, ...reactTextMarkers.map((marker) => `text:${marker}`)], reactTextMarkers, conflictEntryVisible: conflictEntryPattern.test(iframeBodyText), quickActionTabVisible: Boolean(quickActionTab && visible(quickActionTab)), quickActionFolderVisible: Boolean(quickActionFolder && visible(quickActionFolder)), quickActionTabText: quickActionTab ? (quickActionTab.textContent || "").trim() : "", quickActionFolderText: quickActionFolder ? (quickActionFolder.textContent || "").trim() : "", quickActionTabLabel: quickActionTab ? quickActionTab.getAttribute("aria-label") || "" : "", quickActionFolderLabel: quickActionFolder ? quickActionFolder.getAttribute("aria-label") || "" : "", quickActionTabSelected: quickActionTab ? quickActionTab.classList.contains("ant-btn-primary") : false, quickActionFolderSelected: quickActionFolder ? quickActionFolder.classList.contains("ant-btn-primary") : false, quickActionSmokeInputValue: smokeInput instanceof HTMLTextAreaElement ? smokeInput.value : "", }; }); } function assertOpenHubState(state) { if (!state.drawerVisible) { throw new SmokeFailure("selector_missing", "Page AI drawer 未保持可见", state); } if (!state.openhubHost) { throw new SmokeFailure("selector_missing", "Page AI drawer 未切到 OpenHub host", state); } if (!state.iframeVisible || !state.iframeSrc.includes("/page-ai/openhub/ai")) { throw new SmokeFailure("selector_missing", "OpenHub iframe 不可见或 src 未指向 /page-ai/openhub/ai", state); } if (state.outerOpenHubHeaderVisible) { throw new SmokeFailure("outer_header_visible", "OpenHub drawer 仍显示 MNote 外层 OpenHub AI 标题栏", state); } if (!state.iframeRect || state.iframeRect.height < 420) { throw new SmokeFailure("iframe_too_short", "OpenHub iframe 高度不足,可能被顶部 debug/status 区挤压", state); } if (state.debugChromeSqueezesContent) { throw new SmokeFailure("debug_chrome_visible", "OpenHub 顶部 debug/status chrome 仍可见且挤占空间", state); } if (state.fallbackActionVisible || state.fallbackActionCount > 0) { throw new SmokeFailure("legacy_fallback_visible", "OpenHub drawer 仍存在用户可见 opencode fallback 入口", state); } if (state.loginPageVisible) { throw new SmokeFailure("unexpected_upstream_login", "页面出现 OpenHub/WeKnora 登录入口", state); } if (!state.iframeBodySnippet && !state.reactAiMarkers.length) { throw new SmokeFailure("selector_missing", "OpenHub iframe 已出现,但 iframe 内 shell 内容不可见", state); } if (state.conflictEntryVisible) { throw new SmokeFailure("unexpected_conflict_entry", "OpenHub iframe 嵌入态仍显示 MNote 真相冲突入口", state); } if (!state.quickActionTabVisible || !state.quickActionFolderVisible) { throw new SmokeFailure("quick_action_missing", "OpenHub iframe 内缺少当前 Tab/文件夹快捷按钮", state); } } async function main() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const consoleMessages = []; let browser; let context; let page; let result; let directHostRoute = null; let legacyOpencodeRoute = null; const networkEvents = []; try { await assertServiceReachable(BASE_URL); browser = await chromium.launch({ headless: process.env.HEADFUL !== "1", ...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}), }); context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" }); page = await context.newPage(); page.on("console", (message) => { if (["error", "warning"].includes(message.type())) { consoleMessages.push({ type: message.type(), text: message.text() }); } }); page.on("response", (response) => { const url = response.url(); if (url.includes("/page-ai/openhub")) { networkEvents.push({ url, status: response.status(), contentType: response.headers()["content-type"] || "" }); } }); const viewer = await loginWithUiFirst(page, context.request).catch(async (error) => { if (error instanceof SmokeFailure) throw error; await ensureAuthenticated(page, context.request); return getViewerIdentity(context.request); }); legacyOpencodeRoute = await context.request.get(`${BASE_URL}/page-ai/opencode`, { timeout: UI_TIMEOUT_MS }).then(async (response) => ({ url: `${BASE_URL}/page-ai/opencode`, status: response.status(), ok: response.ok(), contentType: response.headers()["content-type"] || "", bodySnippet: (await response.text()).slice(0, 240), })).catch((error) => ({ url: `${BASE_URL}/page-ai/opencode`, status: 0, ok: false, error: error.message, })); if (legacyOpencodeRoute.status !== 410) { throw new SmokeFailure("legacy_fallback_route_enabled", "登录后 /page-ai/opencode legacy fallback 页面仍可访问", legacyOpencodeRoute); } directHostRoute = await context.request.get(`${BASE_URL}/page-ai/openhub/ai`, { timeout: UI_TIMEOUT_MS }).then(async (response) => ({ url: `${BASE_URL}/page-ai/openhub/ai`, status: response.status(), ok: response.ok(), contentType: response.headers()["content-type"] || "", bodySnippet: (await response.text()).slice(0, 240), })).catch((error) => ({ url: `${BASE_URL}/page-ai/openhub/ai`, status: 0, ok: false, error: error.message, })); await openPageAiDrawer(page); await waitForVisibleAny( page, [ "[data-page-ai-openhub-host='true']", "[data-page-ai-openhub-bootstrap-copy]", "iframe[data-page-ai-openhub-iframe]", ], "OpenHub host", ); await page.waitForFunction(() => { const frame = document.querySelector("iframe[data-page-ai-openhub-iframe]"); return frame instanceof HTMLIFrameElement && (frame.getAttribute("src") || "").includes("/page-ai/openhub/ai"); }, undefined, { timeout: UI_TIMEOUT_MS }); await page.waitForFunction(() => { const frame = document.querySelector("iframe[data-page-ai-openhub-iframe]"); return frame instanceof HTMLIFrameElement && (frame.getAttribute("src") || "").includes("mnoteScope="); }, undefined, { timeout: UI_TIMEOUT_MS }).catch(() => undefined); await page.waitForTimeout(800); await page.waitForFunction(() => { const iframe = document.querySelector("iframe[data-page-ai-openhub-iframe]"); const doc = iframe instanceof HTMLIFrameElement ? iframe.contentDocument : null; return Boolean(doc && doc.querySelector("[data-mnote-openhub-current-tab-toggle]") && doc.querySelector("[data-mnote-openhub-current-folder-toggle]")); }, undefined, { timeout: UI_TIMEOUT_MS }); const quickActions = await validateOpenHubQuickActions(page); const state = await collectState(page); await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); assertOpenHubState(state); const openhubReactConnected = state.reactAiMarkers.length > 0; result = { ok: true, task: TASK, baseUrl: BASE_URL, viewer, screenshot: SCREENSHOT_PATH, directHostRoute, legacyOpencodeRoute, quickActions, state, openhubReactConnected, staticBoundaryOnly: state.staticBoundaryVisible && !openhubReactConnected, reactAiNote: openhubReactConnected ? "OpenHub React UI marker 已出现" : "未发现 OpenHub React AI marker;本次只验证 MNote OpenHub host drawer/iframe/shell 边界,不把静态 shell 记为 React AI 已接入", networkEvents, consoleMessages, }; } catch (error) { const state = page ? await collectState(page).catch(() => null) : null; if (page) { await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }).catch(() => undefined); } result = { ok: false, task: TASK, baseUrl: BASE_URL, screenshot: fs.existsSync(SCREENSHOT_PATH) ? SCREENSHOT_PATH : null, failureKind: error instanceof SmokeFailure ? error.kind : "unexpected_error", error: error instanceof Error ? error.stack || error.message : String(error), errorDetails: error instanceof SmokeFailure ? error.details : null, directHostRoute, legacyOpencodeRoute, state, networkEvents, consoleMessages, }; } finally { fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8"); if (context) await context.close().catch(() => undefined); if (browser) await browser.close().catch(() => undefined); } console.log(JSON.stringify(result, null, 2)); if (!result.ok) process.exit(1); } main().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : String(error)); process.exit(1); });