#!/usr/bin/env node "use strict"; const { loginViaAuthForm } = require('./lib/browser-auth-login'); const assert = require("node:assert"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { chromium } = require("playwright"); const BASE = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000"; const TIMEOUT = Number.parseInt(process.env.UI_TIMEOUT_MS || "120000", 10); const STAMP = Date.now(); const OUT = process.env.MNOTE_PI_WARMUP_BINDING_OUT || path.join(os.tmpdir(), `mnote-pi-warmup-binding-${STAMP}`); const ROOT_PATH = process.env.MNOTE_PI_WARMUP_BINDING_ROOT_PATH || "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space"; const ROOT_URI = process.env.MNOTE_PI_WARMUP_BINDING_ROOT_URI || `file://${ROOT_PATH}`; const WORKSPACE_ID = process.env.MNOTE_PI_WARMUP_BINDING_WORKSPACE_ID || "local-ws:mnote-e2e:my-space"; const PAGE_PATH = `pi-warmup-binding-${STAMP}.md`; const CHROMIUM_EXECUTABLE = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || (fs.existsSync("/usr/bin/chromium-browser") ? "/usr/bin/chromium-browser" : "") || (fs.existsSync("/usr/bin/chromium") ? "/usr/bin/chromium" : "") || (fs.existsSync("/usr/bin/google-chrome") ? "/usr/bin/google-chrome" : ""); async function requestJson(page, url, options = {}) { const response = await page.request.fetch(`${BASE}${url}`, { ...options, headers: { accept: "application/json", "content-type": "application/json", ...(options.headers || {}), }, timeout: options.timeout || TIMEOUT, }); const text = await response.text(); let body = {}; try { body = text ? JSON.parse(text) : {}; } catch { body = { raw: text }; } return { response, body }; } async function quickLogin(page) { // 7-76 P0: 标准表单登录(无测试快速登录按钮) const base = (typeof BASE_URL !== "undefined" && BASE_URL) || (typeof baseUrl !== "undefined" && baseUrl) || process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000"; const timeout = (typeof UI_TIMEOUT_MS !== "undefined" && UI_TIMEOUT_MS) || (typeof TIMEOUT !== "undefined" && TIMEOUT) || 30_000; if (!String(page.url() || "").includes("/auth")) { await page.goto(String(base).replace(/\/+$/, "") + "/auth", { waitUntil: "commit", timeout, }); } await loginViaAuthForm(page, { baseUrl: base, timeoutMs: timeout, gotoAuth: false, }); await page .waitForURL((url) => !String(url).includes("/auth"), { timeout }) .catch(() => {}); } async function cleanupPreviousSmokeSession(page) { const status = await requestJson(page, "/api/page-ai/pi/status"); assert(status.response.ok(), `status before cleanup should be OK, got ${status.response.status()}`); const session = status.body.session || {}; const sessionId = status.body.sessionId || session.sessionId; const pagePath = String(session.pagePath || ""); if (!sessionId || !pagePath.startsWith("pi-warmup-binding-")) return; await requestJson(page, "/api/page-ai/pi/abort", { method: "POST", data: { sessionId }, }).catch(() => null); await requestJson(page, `/api/page-ai/pi/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE", }).catch(() => null); } async function main() { fs.mkdirSync(OUT, { recursive: true }); fs.mkdirSync(ROOT_PATH, { recursive: true }); fs.writeFileSync(path.join(ROOT_PATH, PAGE_PATH), "# Pi warmup binding\n\nWarmup binding smoke.\n", "utf8"); const browser = await chromium.launch({ headless: process.env.MNOTE_PI_WARMUP_BINDING_HEADED === "1" ? false : true, executablePath: CHROMIUM_EXECUTABLE || undefined, }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 } }); await context.addInitScript((payload) => { window.__MNOTE_TEST_PAGE_CONTEXT__ = payload; }, { rootUri: ROOT_URI, workspaceId: WORKSPACE_ID, pagePath: PAGE_PATH, pageTitle: "Pi warmup binding", }); const page = await context.newPage(); const requests = []; const result = { base: BASE, out: OUT, rootUri: ROOT_URI, workspaceId: WORKSPACE_ID, pagePath: PAGE_PATH, statusBefore: null, statusAfter: null, startRequest: null, startBeforeSend: false, coldStartToastVisible: false, }; try { await quickLogin(page); await cleanupPreviousSmokeSession(page); const statusBefore = await page.request.get(`${BASE}/api/page-ai/pi/status`); assert(statusBefore.ok(), `status before should be OK, got ${statusBefore.status()}`); result.statusBefore = await statusBefore.json(); assert.equal(result.statusBefore.warmupRunning, true, "dev:hot warmup runtime should be running before UI send"); assert.equal(result.statusBefore.sessionId || null, null, "warmup session must not be exposed as current page session"); await page.route("**/api/page-ai/pi/send", async (route) => { const request = route.request(); requests.push({ url: request.url(), body: request.postDataJSON() }); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, schema: "mnote.page_ai_pi.send.v1", sessionId: request.postDataJSON().sessionId, accepted: true, intercepted: true, }), }); }); const documentId = `local-md:${PAGE_PATH.replaceAll("/", "~2F")}`; await page.goto( `${BASE}/documents/${encodeURIComponent(documentId)}?sourceKind=local_folder&rootUri=${encodeURIComponent(ROOT_URI)}&workspaceId=${encodeURIComponent(WORKSPACE_ID)}&path=${encodeURIComponent(PAGE_PATH)}`, { waitUntil: "commit", timeout: TIMEOUT }, ); await page.locator("[data-page-ai-pi-lab-launcher]").waitFor({ state: "visible", timeout: TIMEOUT }); const startResponsePromise = page.waitForResponse( (response) => response.url().includes("/api/page-ai/pi/start") && response.request().method() === "POST", { timeout: TIMEOUT }, ); await page.locator("[data-page-ai-pi-lab-launcher]").click(); await page.locator('[data-page-ai-pi-lab="drawer"]').waitFor({ state: "visible", timeout: TIMEOUT }); const startResponse = await startResponsePromise; result.startBeforeSend = true; assert(startResponse.ok(), `start response should be OK, got ${startResponse.status()}`); const startBody = await startResponse.json(); result.startRequest = startResponse.request().postDataJSON(); assert.equal(startBody.ok, true, "start body should be ok"); assert(startBody.session?.sessionId, "start should return a current page session"); assert(!String(startBody.session.sessionId).startsWith("pi_lab_dev_warm_"), "current page session should not reuse warmup prompt session id"); assert.equal(startBody.session.pagePath, PAGE_PATH, "current page session should bind current page path"); await page.waitForFunction( () => document.querySelector("[data-page-ai-pi-lab-status-text]")?.textContent?.includes("ready") || document.querySelector("[data-page-ai-pi-lab-status-text]")?.textContent?.includes("streaming"), null, { timeout: TIMEOUT }, ); await page.locator("[data-page-ai-pi-lab-input]").fill("warmup binding smoke"); await page.locator("[data-page-ai-pi-lab-btn-send]").click(); result.coldStartToastVisible = await page.locator("[data-page-ai-pi-lab-toast]", { hasText: "正在绑定当前页 Pi 会话,完成后自动发送" }).isVisible({ timeout: 500 }).catch(() => false); assert.equal(result.coldStartToastVisible, false, "send should not show current-page binding toast after drawer-open prestart"); const sendWaitStartedAt = Date.now(); while (requests.length === 0 && Date.now() - sendWaitStartedAt < 5000) { await page.waitForTimeout(100); } assert.equal(requests.length, 1, "send should be intercepted once after runtime binding"); assert.equal(requests[0].body.sessionId, startBody.session.sessionId, "send should use bound current page session"); const statusAfter = await page.request.get(`${BASE}/api/page-ai/pi/status`); assert(statusAfter.ok(), `status after should be OK, got ${statusAfter.status()}`); result.statusAfter = await statusAfter.json(); assert.equal(result.statusAfter.warmupRunning, true, "warmup diagnostics should remain visible after current session starts"); assert.equal(result.statusAfter.sessionId, startBody.session.sessionId, "status should now expose the current page session"); fs.writeFileSync(path.join(OUT, "result.json"), JSON.stringify(result, null, 2), "utf8"); console.log(`✅ Pi warmup binding browser smoke passed: ${path.join(OUT, "result.json")}`); } finally { await browser.close(); } } main().catch((error) => { console.error(error); process.exit(1); });