556 lines
22 KiB
JavaScript
556 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
||
"use strict";
|
||
|
||
const fs = require("node:fs");
|
||
const path = require("node:path");
|
||
const { chromium } = require("playwright");
|
||
|
||
const TASK = "task786-openhub-history-refresh-browser-smoke";
|
||
const BASE_URL = (process.env.BASE_URL || process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||
const STREAM_TIMEOUT_MS = Number(process.env.MNOTE_OPENHUB_STREAM_TIMEOUT_MS || 45_000);
|
||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task786-openhub-history-refresh-browser-smoke");
|
||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||
const FAILURE_PATH = path.join(OUTPUT_DIR, "failure.json");
|
||
const SCREENSHOT_PATH = path.join(OUTPUT_DIR, "openhub-history-refresh.png");
|
||
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", "/snap/bin/chromium"]
|
||
.find((candidate) => fs.existsSync(candidate));
|
||
|
||
class SmokeFailure extends Error {
|
||
constructor(layer, message, details = {}) {
|
||
super(message);
|
||
this.name = "SmokeFailure";
|
||
this.layer = layer;
|
||
this.details = details;
|
||
}
|
||
}
|
||
|
||
function jsonSnippet(value, limit = 800) {
|
||
if (typeof value === "string") return value.slice(0, limit);
|
||
return JSON.stringify(value, null, 2).slice(0, limit);
|
||
}
|
||
|
||
function openHubProxyUrl(pathname, scopeQuery, extraParams = {}) {
|
||
const url = new URL(`${BASE_URL}/page-ai/openhub/ai${pathname}`);
|
||
const scopeParams = new URLSearchParams(String(scopeQuery || "").replace(/^\?/, ""));
|
||
for (const [key, value] of scopeParams.entries()) {
|
||
url.searchParams.set(key, value);
|
||
}
|
||
for (const [key, value] of Object.entries(extraParams)) {
|
||
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
|
||
}
|
||
return url.toString();
|
||
}
|
||
|
||
async function parseJsonResponse(response, layer, label) {
|
||
const text = await response.text();
|
||
let payload = null;
|
||
try {
|
||
payload = text ? JSON.parse(text) : null;
|
||
} catch {
|
||
throw new SmokeFailure(layer, `${label} 返回非 JSON:HTTP ${response.status()}`, {
|
||
status: response.status(),
|
||
bodySnippet: text.slice(0, 800),
|
||
});
|
||
}
|
||
if (!response.ok()) {
|
||
throw new SmokeFailure(layer, `${label} 失败:HTTP ${response.status()}`, payload);
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
async function assertMNoteReachable() {
|
||
let response;
|
||
try {
|
||
response = await fetch(`${BASE_URL}/health`, { redirect: "manual", signal: AbortSignal.timeout(6_000) });
|
||
} catch (error) {
|
||
throw new SmokeFailure("mnote_service", `MNote 3000 服务不可达:${error.message}`, { baseUrl: BASE_URL });
|
||
}
|
||
if (!response.ok && response.status !== 303) {
|
||
throw new SmokeFailure("mnote_service", `MNote /health 返回异常:HTTP ${response.status}`, { baseUrl: BASE_URL });
|
||
}
|
||
}
|
||
|
||
async function signIn(requestContext) {
|
||
const response = await requestContext.post(`${BASE_URL}/api/auth`, {
|
||
data: {
|
||
action: "auth:signIn",
|
||
args: {
|
||
provider: "password",
|
||
params: {
|
||
account: TEST_EMAIL,
|
||
email: TEST_EMAIL,
|
||
password: TEST_PASSWORD,
|
||
flow: "signIn",
|
||
},
|
||
},
|
||
},
|
||
timeout: UI_TIMEOUT_MS,
|
||
});
|
||
const payload = await parseJsonResponse(response, "auth", "/api/auth 登录");
|
||
const whoami = await requestContext.get(`${BASE_URL}/api/auth/whoami`, { timeout: UI_TIMEOUT_MS });
|
||
const viewer = await parseJsonResponse(whoami, "auth", "/api/auth/whoami");
|
||
if (!viewer || !viewer.userId) {
|
||
throw new SmokeFailure("auth", "登录后 whoami 缺少 userId", { loginPayload: payload, whoami: viewer });
|
||
}
|
||
return viewer;
|
||
}
|
||
|
||
async function visibleAny(page, selectors, label) {
|
||
await page.waitForFunction((candidateSelectors) => {
|
||
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;
|
||
};
|
||
return candidateSelectors.some((selector) => Array.from(document.querySelectorAll(selector)).some(visible));
|
||
}, selectors, { timeout: UI_TIMEOUT_MS }).catch((error) => {
|
||
throw new SmokeFailure("mnote_page_ai_ui", `${label} 不可见`, { selectors, cause: error.message });
|
||
});
|
||
}
|
||
|
||
async function openMNoteOpenHubDrawer(page) {
|
||
await page.goto(BASE_URL, { waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
||
if (page.url().includes("/auth")) {
|
||
throw new SmokeFailure("auth", "打开 MNote 主页后仍被重定向到 /auth", { url: page.url() });
|
||
}
|
||
|
||
await page.evaluate(() => {
|
||
try {
|
||
localStorage.setItem("mnote.page_ai.openhub_host", "1");
|
||
} catch {}
|
||
});
|
||
|
||
await page.waitForFunction(() => (
|
||
typeof window.__mnoteSidebarPageAiRuntime?.openPageAiDrawer === "function"
|
||
|| document.querySelector("[data-testid='wolai-floating-ai']")
|
||
|| document.querySelector("[data-testid='wolai-page-ai-drawer']")
|
||
), null, { timeout: UI_TIMEOUT_MS });
|
||
|
||
const openedByRuntime = await page.evaluate(() => {
|
||
try {
|
||
localStorage.setItem("mnote.page_ai.openhub_host", "1");
|
||
if (typeof window.__mnoteSidebarPageAiRuntime?.openPageAiDrawer === "function") {
|
||
window.__mnoteSidebarPageAiRuntime.openPageAiDrawer();
|
||
return true;
|
||
}
|
||
} catch {}
|
||
return false;
|
||
});
|
||
|
||
if (!openedByRuntime) {
|
||
const floating = page.locator("[data-testid='wolai-floating-ai']").first();
|
||
if (await floating.isVisible({ timeout: 5_000 }).catch(() => false)) {
|
||
await floating.click({ timeout: UI_TIMEOUT_MS });
|
||
}
|
||
}
|
||
|
||
await visibleAny(page, ["[data-testid='wolai-page-ai-drawer']"], "Page AI drawer");
|
||
await page.waitForFunction(() => {
|
||
const drawer = document.querySelector("[data-testid='wolai-page-ai-drawer']");
|
||
return drawer instanceof HTMLElement && drawer.getAttribute("data-page-ai-openhub-host") === "true";
|
||
}, null, { timeout: UI_TIMEOUT_MS }).catch((error) => {
|
||
throw new SmokeFailure("mnote_page_ai_ui", "Page AI drawer 未切到 OpenHub host", {
|
||
cause: error.message,
|
||
drawerText: "",
|
||
});
|
||
});
|
||
|
||
await page.waitForFunction(() => {
|
||
const iframe = document.querySelector("iframe[data-page-ai-openhub-iframe]");
|
||
return iframe instanceof HTMLIFrameElement && (iframe.getAttribute("src") || "").includes("/page-ai/openhub/ai");
|
||
}, null, { timeout: UI_TIMEOUT_MS }).catch((error) => {
|
||
throw new SmokeFailure("mnote_page_ai_ui", "OpenHub iframe 未挂载或 src 未指向 /page-ai/openhub/ai", { cause: error.message });
|
||
});
|
||
}
|
||
|
||
async function getOpenHubFrame(page) {
|
||
const deadline = Date.now() + UI_TIMEOUT_MS;
|
||
let lastState = null;
|
||
while (Date.now() < deadline) {
|
||
const handle = await page.locator("iframe[data-page-ai-openhub-iframe]").first().elementHandle().catch(() => null);
|
||
if (handle) {
|
||
const frame = await handle.contentFrame();
|
||
lastState = {
|
||
iframeSrc: await handle.getAttribute("src").catch(() => ""),
|
||
frameUrl: frame ? frame.url() : "",
|
||
};
|
||
if (frame && frame.url().includes("/page-ai/openhub/ai")) {
|
||
await frame.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch(() => undefined);
|
||
return frame;
|
||
}
|
||
}
|
||
await page.waitForTimeout(300);
|
||
}
|
||
throw new SmokeFailure("openhub_iframe_ui", "无法取得 OpenHub iframe frame", lastState || {});
|
||
}
|
||
|
||
async function collectFrameState(frame, marker) {
|
||
return frame.evaluate((expectedMarker) => {
|
||
const text = (selector) => (document.querySelector(selector)?.textContent || "").replace(/\s+/g, " ").trim();
|
||
const bodyText = (document.body?.textContent || "").replace(/\s+/g, " ").trim();
|
||
const drawerText = Array.from(document.querySelectorAll(".ant-drawer, [role='dialog']"))
|
||
.map((node) => (node.textContent || "").replace(/\s+/g, " ").trim())
|
||
.filter(Boolean)
|
||
.join("\n");
|
||
const messageAreaText = text(".messages-area");
|
||
const historyButtons = Array.from(document.querySelectorAll("button"))
|
||
.map((button) => (button.textContent || button.getAttribute("title") || button.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim())
|
||
.filter(Boolean);
|
||
return {
|
||
url: location.href,
|
||
title: document.title,
|
||
bodySnippet: bodyText.slice(0, 1200),
|
||
iframeShellKind: document.body?.getAttribute("data-mnote-openhub-ai-shell") || "",
|
||
hasReactRoot: Boolean(document.getElementById("root")),
|
||
hasOpenHubTitle: bodyText.includes("OpenHub 平台"),
|
||
hasHistoryButton: historyButtons.some((entry) => entry.includes("历史记录")),
|
||
historyButtons,
|
||
drawerTextSnippet: drawerText.slice(0, 1200),
|
||
messageAreaSnippet: messageAreaText.slice(0, 1200),
|
||
bodyHasMarker: bodyText.includes(expectedMarker),
|
||
drawerHasMarker: drawerText.includes(expectedMarker),
|
||
messageAreaHasMarker: messageAreaText.includes(expectedMarker),
|
||
staticBoundaryVisible: /静态占位|static-boundary|最小 host\/bootstrap 占位/.test(bodyText),
|
||
loginVisible: /(登录\s*OpenHub|OpenHub\s*Login|Sign in to OpenHub|WeKnora\s*登录)/i.test(bodyText),
|
||
};
|
||
}, marker);
|
||
}
|
||
|
||
function scopeQueryFromFrameUrl(frameUrl) {
|
||
const url = new URL(frameUrl, BASE_URL);
|
||
const query = url.searchParams.toString();
|
||
if (!query || !url.searchParams.get("scope") || !url.searchParams.get("mnoteScope")) {
|
||
throw new SmokeFailure("openhub_scope", "OpenHub iframe URL 缺少 scope/mnoteScope", { frameUrl });
|
||
}
|
||
return `?${query}`;
|
||
}
|
||
|
||
function parseStreamEvents(streamText) {
|
||
const eventTypes = [];
|
||
let returnedSessionId = "";
|
||
let errorEvent = "";
|
||
for (const line of String(streamText || "").split(/\r?\n/)) {
|
||
if (!line.startsWith("data: ")) continue;
|
||
try {
|
||
const data = JSON.parse(line.slice(6));
|
||
const payload = data.payload && data.payload.type
|
||
? { type: data.payload.type, ...data.payload.properties }
|
||
: data;
|
||
if (payload.type) eventTypes.push(payload.type);
|
||
if (payload.conversation_id) returnedSessionId = payload.conversation_id;
|
||
if (payload.error) errorEvent = String(payload.error);
|
||
} catch {}
|
||
}
|
||
return {
|
||
eventTypes: [...new Set(eventTypes)],
|
||
returnedSessionId,
|
||
errorEvent,
|
||
snippet: String(streamText || "").slice(0, 1200),
|
||
};
|
||
}
|
||
|
||
async function sendMarkerMessage(requestContext, scopeQuery, sessionId, marker) {
|
||
const prompt = `${marker} Page AI OpenHub history refresh smoke. 请只回复 ${marker},不要解释。`;
|
||
const body = {
|
||
question: prompt,
|
||
conversation_id: sessionId,
|
||
agent: "build",
|
||
};
|
||
const url = openHubProxyUrl("/api/query/stream", scopeQuery);
|
||
try {
|
||
const response = await requestContext.post(url, {
|
||
data: body,
|
||
headers: { "content-type": "application/json" },
|
||
timeout: STREAM_TIMEOUT_MS,
|
||
});
|
||
const text = await response.text();
|
||
return {
|
||
ok: response.ok(),
|
||
status: response.status(),
|
||
prompt,
|
||
...parseStreamEvents(text),
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
ok: false,
|
||
status: 0,
|
||
prompt,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
};
|
||
}
|
||
}
|
||
|
||
async function fetchMessagesUntilMarker(requestContext, scopeQuery, sessionId, marker) {
|
||
let lastPayload = null;
|
||
let lastStatus = 0;
|
||
for (let attempt = 0; attempt < 12; attempt += 1) {
|
||
const response = await requestContext.get(openHubProxyUrl(`/api/sessions/${encodeURIComponent(sessionId)}/messages`, scopeQuery), {
|
||
timeout: UI_TIMEOUT_MS,
|
||
});
|
||
lastStatus = response.status();
|
||
const payload = await response.json().catch(async () => ({ nonJson: await response.text().catch(() => "") }));
|
||
lastPayload = payload;
|
||
const messages = Array.isArray(payload?.data) ? payload.data : [];
|
||
const userMessage = messages.find((message) => message.role === "user" && String(message.content || "").includes(marker));
|
||
if (response.ok() && userMessage) {
|
||
return {
|
||
ok: true,
|
||
status: response.status(),
|
||
count: messages.length,
|
||
roles: messages.map((message) => message.role),
|
||
userMessageContent: String(userMessage.content || ""),
|
||
lastMessages: messages.slice(-3).map((message) => ({
|
||
role: message.role,
|
||
content: String(message.content || "").slice(0, 300),
|
||
created_at: message.created_at,
|
||
})),
|
||
};
|
||
}
|
||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||
}
|
||
throw new SmokeFailure("openhub_api_messages", "OpenHub API 未读回带 marker 的 user message", {
|
||
sessionId,
|
||
marker,
|
||
status: lastStatus,
|
||
payloadSnippet: jsonSnippet(lastPayload),
|
||
});
|
||
}
|
||
|
||
async function fetchSessionsUntilMarker(requestContext, scopeQuery, sessionId, marker) {
|
||
let lastPayload = null;
|
||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||
const response = await requestContext.get(openHubProxyUrl("/api/sessions", scopeQuery, { page: 1, page_size: 10 }), {
|
||
timeout: UI_TIMEOUT_MS,
|
||
});
|
||
const payload = await response.json().catch(async () => ({ nonJson: await response.text().catch(() => "") }));
|
||
lastPayload = payload;
|
||
const sessions = Array.isArray(payload?.data) ? payload.data : [];
|
||
const found = sessions.find((session) => String(session.session_id || session.id || "") === sessionId);
|
||
const markerTitle = sessions.find((session) => String(session.title || "").includes(marker));
|
||
if (response.ok() && (found || markerTitle)) {
|
||
return {
|
||
ok: true,
|
||
status: response.status(),
|
||
total: payload?.pagination?.total,
|
||
foundSession: found || markerTitle,
|
||
markerInTitle: Boolean(markerTitle),
|
||
firstSessions: sessions.slice(0, 5).map((session) => ({
|
||
session_id: session.session_id || session.id || "",
|
||
title: session.title || "",
|
||
updated_at: session.updated_at || "",
|
||
})),
|
||
};
|
||
}
|
||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||
}
|
||
throw new SmokeFailure("openhub_api_history", "OpenHub sessions API 未读回对应 session/history", {
|
||
sessionId,
|
||
marker,
|
||
payloadSnippet: jsonSnippet(lastPayload),
|
||
});
|
||
}
|
||
|
||
async function assertOpenHubReactUsable(frame, marker) {
|
||
const state = await collectFrameState(frame, marker);
|
||
if (state.staticBoundaryVisible) {
|
||
throw new SmokeFailure("openhub_iframe_ui", "OpenHub iframe 仍是静态边界页,不能把 API-only 记为 UI 通过", state);
|
||
}
|
||
if (state.loginVisible) {
|
||
throw new SmokeFailure("openhub_iframe_ui", "OpenHub iframe 出现独立登录入口", state);
|
||
}
|
||
if (!state.hasOpenHubTitle && !state.hasHistoryButton) {
|
||
throw new SmokeFailure("openhub_iframe_ui", "OpenHub React AI 面板未渲染出历史入口", state);
|
||
}
|
||
return state;
|
||
}
|
||
|
||
async function openHistoryAndVerify(frame, sessionId, marker) {
|
||
let beforeHistoryState = await collectFrameState(frame, marker);
|
||
if (beforeHistoryState.messageAreaHasMarker) {
|
||
return {
|
||
uiMarkerVerified: true,
|
||
historyMarkerVerified: false,
|
||
currentMessageMarkerVerified: true,
|
||
beforeHistoryState,
|
||
afterHistoryState: beforeHistoryState,
|
||
afterSessionClickState: beforeHistoryState,
|
||
};
|
||
}
|
||
|
||
let historyButton = frame.getByRole("button", { name: /历史记录/ }).first();
|
||
if (!(await historyButton.isVisible({ timeout: 3_000 }).catch(() => false))) {
|
||
historyButton = frame.locator('button[title="历史记录"]').first();
|
||
}
|
||
if (!(await historyButton.isVisible({ timeout: 8_000 }).catch(() => false))) {
|
||
throw new SmokeFailure("openhub_history_ui", "OpenHub iframe 内未找到历史记录按钮", beforeHistoryState);
|
||
}
|
||
await historyButton.click({ timeout: UI_TIMEOUT_MS });
|
||
|
||
let afterHistoryState = null;
|
||
for (let attempt = 0; attempt < 12; attempt += 1) {
|
||
afterHistoryState = await collectFrameState(frame, marker);
|
||
if (afterHistoryState.drawerHasMarker || afterHistoryState.bodyHasMarker) break;
|
||
if (attempt === 2) {
|
||
const refreshButton = frame.getByRole("button", { name: /刷新/ }).first();
|
||
if (await refreshButton.isVisible({ timeout: 1_000 }).catch(() => false)) {
|
||
await refreshButton.click({ timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||
}
|
||
}
|
||
await frame.waitForTimeout(1000);
|
||
}
|
||
|
||
const historyMarkerVerified = Boolean(afterHistoryState && (afterHistoryState.drawerHasMarker || afterHistoryState.bodyHasMarker));
|
||
if (!historyMarkerVerified) {
|
||
throw new SmokeFailure("openhub_history_ui", "刷新后 OpenHub history/session UI 未显示 marker/session 标题", {
|
||
sessionId,
|
||
marker,
|
||
beforeHistoryState,
|
||
afterHistoryState,
|
||
});
|
||
}
|
||
|
||
const markerText = frame.getByText(marker, { exact: false }).first();
|
||
if (await markerText.isVisible({ timeout: 5_000 }).catch(() => false)) {
|
||
await markerText.click({ timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||
}
|
||
|
||
let afterSessionClickState = null;
|
||
for (let attempt = 0; attempt < 10; attempt += 1) {
|
||
afterSessionClickState = await collectFrameState(frame, marker);
|
||
if (afterSessionClickState.messageAreaHasMarker) break;
|
||
await frame.waitForTimeout(1000);
|
||
}
|
||
|
||
return {
|
||
uiMarkerVerified: historyMarkerVerified || Boolean(afterSessionClickState?.messageAreaHasMarker),
|
||
historyMarkerVerified,
|
||
currentMessageMarkerVerified: Boolean(afterSessionClickState?.messageAreaHasMarker),
|
||
beforeHistoryState,
|
||
afterHistoryState,
|
||
afterSessionClickState,
|
||
};
|
||
}
|
||
|
||
async function main() {
|
||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
const marker = `TASK786_MARKER_${suffix}`;
|
||
const sessionId = `task786-openhub-history-${suffix}`;
|
||
let browser;
|
||
let context;
|
||
let page;
|
||
let result = {
|
||
ok: false,
|
||
task: TASK,
|
||
baseUrl: BASE_URL,
|
||
sessionId,
|
||
marker,
|
||
screenshotPath: SCREENSHOT_PATH,
|
||
uiMarker: "",
|
||
historyMarker: "",
|
||
uiMarkerVerified: false,
|
||
historyMarkerVerified: false,
|
||
currentMessageMarkerVerified: false,
|
||
};
|
||
|
||
try {
|
||
await assertMNoteReachable();
|
||
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();
|
||
|
||
const viewer = await signIn(context.request);
|
||
await openMNoteOpenHubDrawer(page);
|
||
let frame = await getOpenHubFrame(page);
|
||
const initialFrameState = await assertOpenHubReactUsable(frame, marker);
|
||
const scopeQuery = scopeQueryFromFrameUrl(frame.url());
|
||
|
||
const sendStream = await sendMarkerMessage(context.request, scopeQuery, sessionId, marker);
|
||
const apiMessages = await fetchMessagesUntilMarker(context.request, scopeQuery, sessionId, marker);
|
||
const apiSessions = await fetchSessionsUntilMarker(context.request, scopeQuery, sessionId, marker);
|
||
|
||
await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
||
await openMNoteOpenHubDrawer(page);
|
||
frame = await getOpenHubFrame(page);
|
||
const refreshedFrameState = await assertOpenHubReactUsable(frame, marker);
|
||
const ui = await openHistoryAndVerify(frame, sessionId, marker);
|
||
|
||
if (!ui.uiMarkerVerified) {
|
||
throw new SmokeFailure("openhub_history_ui", "API 已读回 marker,但刷新后 UI 未恢复 marker/session", {
|
||
sessionId,
|
||
marker,
|
||
ui,
|
||
});
|
||
}
|
||
|
||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||
result = {
|
||
...result,
|
||
ok: true,
|
||
viewer,
|
||
scopeQueryKeys: Array.from(new URLSearchParams(scopeQuery.slice(1)).keys()),
|
||
sendStream,
|
||
apiMessageVerified: true,
|
||
apiHistoryVerified: true,
|
||
apiMessages,
|
||
apiSessions,
|
||
initialFrameState,
|
||
refreshedFrameState,
|
||
uiMarkerVerified: ui.uiMarkerVerified,
|
||
historyMarkerVerified: ui.historyMarkerVerified,
|
||
currentMessageMarkerVerified: ui.currentMessageMarkerVerified,
|
||
uiMarker: ui.currentMessageMarkerVerified ? marker : "",
|
||
historyMarker: ui.historyMarkerVerified ? marker : "",
|
||
beforeHistoryState: ui.beforeHistoryState,
|
||
afterHistoryState: ui.afterHistoryState,
|
||
afterSessionClickState: ui.afterSessionClickState,
|
||
resultPath: RESULT_PATH,
|
||
};
|
||
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||
if (fs.existsSync(FAILURE_PATH)) fs.rmSync(FAILURE_PATH, { force: true });
|
||
} catch (error) {
|
||
if (page) {
|
||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }).catch(() => undefined);
|
||
}
|
||
result = {
|
||
...result,
|
||
ok: false,
|
||
failureLayer: error instanceof SmokeFailure ? error.layer : "unexpected",
|
||
error: error instanceof Error ? error.stack || error.message : String(error),
|
||
details: error instanceof SmokeFailure ? error.details : undefined,
|
||
failurePath: FAILURE_PATH,
|
||
};
|
||
fs.writeFileSync(FAILURE_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||
if (fs.existsSync(RESULT_PATH)) fs.rmSync(RESULT_PATH, { force: true });
|
||
} finally {
|
||
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) => {
|
||
const failure = {
|
||
ok: false,
|
||
task: TASK,
|
||
baseUrl: BASE_URL,
|
||
failureLayer: "fatal",
|
||
error: error instanceof Error ? error.stack || error.message : String(error),
|
||
failurePath: FAILURE_PATH,
|
||
};
|
||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||
fs.writeFileSync(FAILURE_PATH, `${JSON.stringify(failure, null, 2)}\n`, "utf8");
|
||
console.error(JSON.stringify(failure, null, 2));
|
||
process.exit(1);
|
||
});
|