#!/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 { setupWorkspaceAccess, seedAiRuntime, } = require(path.join(ROOT, "scripts", "lib", "control-plane-dev-seed")); const TASK = "task557-page-ai-run-resume-smoke"; const OUTPUT_DIR = path.join(ROOT, "tmp", TASK); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); const SCREENSHOT_PATH = path.join(OUTPUT_DIR, "page-ai-run-resume.png"); const CHROME = process.env.MNOTE_PAGE_AI_CHROME || "/usr/bin/google-chrome"; const RESUME_TIMEOUT_MS = Number(process.env.MNOTE_PAGE_AI_RESUME_TIMEOUT_MS || 90_000); const ACTOR_ID = "mnote-e2e"; const WORKSPACE_ID = "local-ws:mnote-e2e:my-space"; const ROOT_PATH = "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space"; const ROOT_URI = `file://${ROOT_PATH}`; const OWNER_REL = "knowledge-rag-fixtures-7-57/PageAiRunResumeSmoke.md"; const DOCUMENT_ID = `local-md:${OWNER_REL.replaceAll("/", "~2F")}`; function ensureFixture() { const ownerPath = path.join(ROOT_PATH, OWNER_REL); fs.mkdirSync(path.dirname(ownerPath), { recursive: true }); fs.writeFileSync(ownerPath, "# Page AI Run Resume Smoke\n\n用于验证 Page AI host run journal afterSeq 恢复。\n", "utf8"); } async function seedRun(requestContext) { const stamp = Date.now(); const runId = `run_task557_resume_${stamp}`; const sessionId = `mnote_task557_resume_${stamp}`; const runtimeJson = JSON.stringify({ runId, status: "running", lastEvent: "message.delta" }); const payloadJson = JSON.stringify({ requestId: `task557_resume_${stamp}`, agentId: "reasonix", message: "resume smoke user prompt", }); await seedAiRuntime(requestContext, BASE_URL, { id: `arr_${stamp}`, userId: ACTOR_ID, workspaceId: WORKSPACE_ID, documentId: DOCUMENT_ID, sessionId, runId, title: "Resume smoke", profile: "reasonix", acpRuntime: "reasonix", traceId: `trace_task557_${stamp}`, status: "running", runtimeJson: JSON.parse(runtimeJson), payloadJson: JSON.parse(payloadJson), events: [ { id: `are_${stamp}_1`, eventType: "message.delta", payloadJson: { delta: "old-token" } }, { id: `are_${stamp}_2`, eventType: "message.delta", payloadJson: { delta: "resumed-token" } }, ], timeoutMs: UI_TIMEOUT_MS, }); return { runId, sessionId }; } async function signIn(context) { const response = await context.request.post(`${BASE_URL}/api/auth`, { data: { action: "auth:signIn", args: { provider: "password", params: { account: ACTOR_ID, password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!", flow: "signIn", }, }, }, timeout: UI_TIMEOUT_MS, }); assert(response.ok(), `/api/auth 登录失败: ${response.status()} ${await response.text()}`); } async function main() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); ensureFixture(); const browser = await chromium.launch({ headless: process.env.MNOTE_PAGE_AI_VERIFY_HEADED !== "1", executablePath: fs.existsSync(CHROME) ? CHROME : undefined, }); const context = await browser.newContext({ viewport: { width: 1360, height: 900 } }); const page = await context.newPage(); const pageErrors = []; const consoleErrors = []; const apiRequests = []; const apiResponses = []; page.on("pageerror", (error) => pageErrors.push(String(error?.message || error))); page.on("console", (message) => { if (["error", "warning"].includes(message.type())) consoleErrors.push(`${message.type()}: ${message.text()}`); }); page.on("request", (request) => { if (/\/api\/(page-ai|hermes\/client)/.test(request.url())) { apiRequests.push({ method: request.method(), url: request.url() }); } }); page.on("response", (response) => { if (/\/api\/(page-ai|hermes\/client)/.test(response.url())) { apiResponses.push({ status: response.status(), url: response.url() }); } }); try { await signIn(context); await setupWorkspaceAccess(context.request, BASE_URL, { actorId: ACTOR_ID, email: "mnote.e2e@example.com", workspaceId: WORKSPACE_ID, workspaceName: "MNote E2E Space", rootPath: ROOT_PATH, rootUri: ROOT_URI, capabilities: ["ai"], timeoutMs: UI_TIMEOUT_MS, }); const seeded = await seedRun(context.request); await page.addInitScript(({ documentId, runId, sessionId }) => { window.localStorage.setItem(`hermes_page_ai_session:${documentId}:active-run`, JSON.stringify({ schema: "mnote.page_ai_active_run_snapshot.v1", hostRunId: runId, sessionId, lastSeq: "000000000000000001", status: "running", createdAt: Date.now(), updatedAt: Date.now(), })); }, { documentId: DOCUMENT_ID, runId: seeded.runId, sessionId: seeded.sessionId }); const documentUrl = new URL(`${BASE_URL}/documents/${encodeURIComponent(DOCUMENT_ID)}`); documentUrl.searchParams.set("sourceKind", "local_folder"); documentUrl.searchParams.set("rootUri", ROOT_URI); documentUrl.searchParams.set("workspaceId", WORKSPACE_ID); await page.goto(documentUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS }); try { await page.waitForFunction( (runId) => document.documentElement.getAttribute("data-mnote-page-ai-run-journal-resumed") === runId, seeded.runId, { timeout: RESUME_TIMEOUT_MS }, ); await page.waitForFunction( () => document.documentElement.getAttribute("data-mnote-page-ai-active-run-last-seq") === "000000000000000002", null, { timeout: RESUME_TIMEOUT_MS }, ); } catch (error) { const debugState = await page.evaluate(() => { const drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]'); return { attrs: Object.fromEntries(Array.from(document.documentElement.attributes).map((attr) => [attr.name, attr.value])), drawerHidden: drawer ? drawer.hidden : null, drawerText: drawer ? (drawer.textContent || "").slice(0, 1000) : "", storageKeys: Object.keys(window.localStorage || {}).filter((key) => key.includes("page_ai") || key.includes("hermes_page_ai")), }; }).catch((stateError) => ({ stateError: String(stateError) })); debugState.apiRequests = apiRequests; debugState.apiResponses = apiResponses; debugState.pageErrors = pageErrors; debugState.consoleErrors = consoleErrors; await page.screenshot({ path: path.join(OUTPUT_DIR, "failure.png"), fullPage: true }).catch(() => undefined); fs.writeFileSync(path.join(OUTPUT_DIR, "failure-state.json"), `${JSON.stringify(debugState, null, 2)}\n`, "utf8"); throw error; } const state = await page.evaluate((runId) => { const drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]'); const text = drawer ? drawer.textContent || "" : ""; return { resumedRunId: document.documentElement.getAttribute("data-mnote-page-ai-run-journal-resumed") || "", activeHostRunId: document.documentElement.getAttribute("data-mnote-page-ai-active-host-run-id") || "", lastSeq: document.documentElement.getAttribute("data-mnote-page-ai-active-run-last-seq") || "", hasResumedToken: text.includes("resumed-token"), hasSkippedToken: text.includes("old-token"), runStatus: document.documentElement.getAttribute("data-mnote-page-ai-run-status") || "", drawerText: text.slice(0, 500), expectedRunId: runId, }; }, seeded.runId); await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); assert.equal(state.resumedRunId, seeded.runId, `未标记 journal resumed: ${JSON.stringify(state, null, 2)}`); assert.equal(state.activeHostRunId, seeded.runId, `active host run id 不匹配: ${JSON.stringify(state, null, 2)}`); assert.equal(state.lastSeq, "000000000000000002", `lastSeq 未推进: ${JSON.stringify(state, null, 2)}`); assert.equal(state.hasResumedToken, true, `未渲染 afterSeq 后的新事件: ${JSON.stringify(state, null, 2)}`); assert.equal(state.hasSkippedToken, false, `重复渲染了 afterSeq 之前的事件: ${JSON.stringify(state, null, 2)}`); assert.deepEqual(pageErrors, [], `页面异常: ${pageErrors.join("\n")}`); const result = { ok: true, task: TASK, baseUrl: BASE_URL, documentId: DOCUMENT_ID, runId: seeded.runId, sessionId: seeded.sessionId, state, consoleErrors, apiRequests, apiResponses, screenshot: SCREENSHOT_PATH, }; fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8"); console.log(JSON.stringify(result, null, 2)); } finally { await browser.close().catch(() => undefined); } } main().catch((error) => { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); fs.writeFileSync(path.join(OUTPUT_DIR, "failure.json"), `${JSON.stringify({ ok: false, task: TASK, error: error.stack || error.message || String(error) }, null, 2)}\n`, "utf8"); console.error(error.stack || error.message || String(error)); process.exit(1); });