Files
mnote/scripts/task773-page-ai-openhub-browser-smoke.js
T

554 lines
26 KiB
JavaScript

#!/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);
});