#!/usr/bin/env node "use strict"; const assert = require("node:assert"); const fs = require("node:fs"); const path = require("node:path"); const { chromium } = require("playwright"); const ROOT = process.env.MNOTE_REPO_ROOT || "/mnt/Data1T/mnote"; const { BASE_URL, UI_TIMEOUT_MS } = require(path.join(ROOT, "scripts", "tree-shell-smoke-helpers")); const OUTPUT_DIR = path.join(ROOT, "tmp", "task531-lightrag-dashboard-ui-smoke"); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); const CHROME = process.env.MNOTE_PAGE_AI_CHROME || "/usr/bin/google-chrome"; async function fetchDashboardStatus(context) { const response = await context.request.get(`${BASE_URL}/api/knowledge-rag/status`, { headers: { "x-mnote-actor-id": "mnote-e2e", "x-mnote-actor-type": "user", "x-mnote-workspace-id": "local-ws:mnote-e2e:my-space", }, timeout: UI_TIMEOUT_MS, }); const text = await response.text(); assert(response.ok(), `/api/knowledge-rag/status 失败: ${response.status()} ${text.slice(0, 500)}`); const payload = JSON.parse(text); assert(payload.dashboardUrl, `status 缺少 dashboardUrl: ${text.slice(0, 500)}`); assert(payload.health?.healthy !== false, `LightRAG health 非 healthy: ${text.slice(0, 500)}`); return payload; } async function bodyText(page) { return await page.evaluate(() => document.body.innerText || ""); } async function screenshot(page, name) { const screenshotPath = path.join(OUTPUT_DIR, name); await page.screenshot({ path: screenshotPath, fullPage: true }); return screenshotPath; } async function main() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const browser = await chromium.launch({ headless: process.env.MNOTE_DASHBOARD_SMOKE_HEADED !== "1", executablePath: fs.existsSync(CHROME) ? CHROME : undefined, }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 } }); const page = await context.newPage(); const pageErrors = []; const consoleErrors = []; page.on("pageerror", (error) => pageErrors.push(String(error?.message || error))); page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); try { const statusPayload = await fetchDashboardStatus(context); const dashboardUrl = statusPayload.dashboardUrl; const documentSummaries = Array.isArray(statusPayload.documents?.documents) ? statusPayload.documents.documents : []; const rawStatusGroups = statusPayload.documents?.rawStatusGroups || {}; const processedCount = Number(rawStatusGroups.processed || 0); const webuiUrl = dashboardUrl.endsWith("/webui/") ? dashboardUrl : `${dashboardUrl.replace(/\/+$/, "")}/webui/`; await page.goto(webuiUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); await page.waitForLoadState("networkidle", { timeout: UI_TIMEOUT_MS }).catch(() => undefined); await page.waitForFunction( () => (document.body.innerText || "").includes("Document Management"), null, { timeout: UI_TIMEOUT_MS }, ); const documentsText = await bodyText(page); assert(documentsText.includes("Uploaded Documents"), "Documents 页缺少 Uploaded Documents"); if (processedCount > 0) { assert( documentsText.includes(`Completed (${processedCount})`), `Documents 页未显示当前 completed 计数 ${processedCount}: ${documentsText.slice(0, 1000)}`, ); } const visibleDocument = documentSummaries.find((doc) => { const id = String(doc.id || ""); const filePath = String(doc.filePath || ""); return (id && documentsText.includes(id)) || (filePath && documentsText.includes(filePath)); }); assert( visibleDocument, `Documents 页没有显示 status API 返回的任一当前文档: ${JSON.stringify(documentSummaries.slice(0, 5), null, 2)}\n${documentsText.slice(0, 1000)}`, ); const documentsScreenshot = await screenshot(page, "documents.png"); await page.getByRole("tab", { name: /Knowledge Graph/i }).click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const text = document.body.innerText || ""; return text.includes("Connected") && text.includes("D:"); }, null, { timeout: UI_TIMEOUT_MS }, ); const graphText = await bodyText(page); assert(graphText.includes("Connected"), `Graph 页缺少连接状态: ${graphText.slice(0, 1000)}`); const graphScreenshot = await screenshot(page, "graph.png"); await page.getByRole("tab", { name: /Retrieval/i }).click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => (document.body.innerText || "").includes("Start a retrieval by typing your query below"), null, { timeout: UI_TIMEOUT_MS }, ); const retrievalText = await bodyText(page); assert(retrievalText.includes("Query Mode"), "Retrieval 页缺少 Query Mode 参数"); assert(retrievalText.includes("KG Top K"), "Retrieval 页缺少 KG Top K 参数"); assert(retrievalText.includes("Connected"), "Retrieval 页缺少连接状态"); const retrievalScreenshot = await screenshot(page, "retrieval.png"); const result = { ok: true, baseUrl: BASE_URL, dashboardUrl, webuiUrl: page.url(), documents: { processedCount, completedVisible: processedCount > 0 ? documentsText.includes(`Completed (${processedCount})`) : null, visibleDocumentId: visibleDocument.id || null, visibleDocumentFilePath: visibleDocument.filePath || null, screenshot: documentsScreenshot, }, graph: { connectedVisible: graphText.includes("Connected"), screenshot: graphScreenshot, }, retrieval: { queryModeVisible: retrievalText.includes("Query Mode"), connectedVisible: retrievalText.includes("Connected"), screenshot: retrievalScreenshot, }, pageErrors, consoleErrors, }; fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8"); console.log(JSON.stringify(result, null, 2)); } catch (error) { await page.screenshot({ path: path.join(OUTPUT_DIR, "failure.png"), fullPage: true }).catch(() => undefined); fs.writeFileSync( path.join(OUTPUT_DIR, "failure.json"), `${JSON.stringify( { ok: false, error: error instanceof Error ? error.stack || error.message : String(error), pageErrors, consoleErrors, }, null, 2, )}\n`, "utf8", ); throw error; } finally { await browser.close().catch(() => undefined); } } main().catch((error) => { console.error(error.stack || error.message || String(error)); process.exit(1); });