#!/usr/bin/env node "use strict"; const assert = require("node:assert"); const { chromium } = require("playwright"); const { BASE_URL, UI_TIMEOUT_MS, cleanupDocuments, createTempDocument, ensureAuthenticated, renameDocument, } = require("./tree-shell-smoke-helpers"); async function main() { const suffix = Date.now().toString(36); const title = `TEST-HERMES-AI-smoke-${suffix}`; const sessionId = `mnote_smoke_${suffix}`; const runId = `run_smoke_${suffix}`; let sessionDetailHits = 0; const captured = []; const createdIds = []; const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 } }); const page = await context.newPage(); try { await page.route("**/api/ai-agent/run", async (route) => { throw new Error(`页面 AI 不应请求旧 /api/ai-agent/run: ${route.request().url()}`); }); await page.route("**/api/hermes/client/sessions", async (route) => { captured.push({ kind: "session", method: route.request().method(), body: route.request().postData() || "" }); await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, sessionId, title: "当前页问答", traceId: "trace_smoke", persistence: "hermes_on_first_run", }), }); }); await page.route(`**/api/hermes/client/sessions/${sessionId}`, async (route) => { sessionDetailHits += 1; captured.push({ kind: "session-detail", method: route.request().method(), body: "" }); await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, sessionId, traceId: "trace_restore", session: { sessionId, messages: [ { role: "user", content: `请总结 ${title}` }, { role: "tool", content: "mnote.page.get" }, { role: "assistant", content: "Smoke restored from Hermes session" }, ], }, }), }); }); await page.route("**/api/hermes/client/runs", async (route) => { captured.push({ kind: "run", method: route.request().method(), body: route.request().postData() || "" }); await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, sessionId, runId, events: [], traceId: "trace_smoke", }), }); }); await page.route(`**/api/hermes/client/events/${runId}`, async (route) => { captured.push({ kind: "events", method: route.request().method(), body: "" }); await route.fulfill({ status: 200, headers: { "content-type": "text/event-stream; charset=utf-8" }, body: `data: ${JSON.stringify({ event: "tool.started", run_id: runId, session_id: sessionId, name: "mnote.page.get" })}\n\n` + `data: ${JSON.stringify({ event: "tool.completed", run_id: runId, session_id: sessionId, name: "mnote.page.get" })}\n\n` + `data: ${JSON.stringify({ event: "message.delta", run_id: runId, session_id: sessionId, delta: "Smoke " })}\n\n` + `data: ${JSON.stringify({ event: "message.delta", run_id: runId, session_id: sessionId, delta: "response" })}\n\n` + `data: ${JSON.stringify({ event: "run.completed", run_id: runId, session_id: sessionId, output: "Smoke response" })}\n\n`, }); }); await ensureAuthenticated(page, context.request); const target = await createTempDocument(context.request); createdIds.push(target.documentId); await renameDocument(context.request, target.workspaceId, target.documentId, title); await page.goto( `${BASE_URL}/documents/${encodeURIComponent(target.documentId)}?workspaceId=${encodeURIComponent(target.workspaceId)}`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }, ); await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS }); await page.locator("[data-page-ai-input]").fill(`请总结 ${title}`, { timeout: UI_TIMEOUT_MS }); await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("Smoke response"), null, { timeout: UI_TIMEOUT_MS }, ); await page.waitForFunction( () => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("mnote.page.get"), null, { timeout: UI_TIMEOUT_MS }, ); await page.waitForFunction( () => (document.querySelector("[data-page-ai-last-tool]")?.textContent || "").includes("mnote.page.get"), null, { timeout: UI_TIMEOUT_MS }, ); const sessionRequest = captured.find((entry) => entry.kind === "session"); const runRequest = captured.find((entry) => entry.kind === "run"); const eventRequest = captured.find((entry) => entry.kind === "events"); assert(sessionRequest, "未捕获 /api/hermes/client/sessions 请求"); assert(runRequest, "未捕获 /api/hermes/client/runs 请求"); assert(eventRequest, "未捕获 /api/hermes/client/events 请求"); const runBody = JSON.parse(runRequest.body); assert.equal(runBody.sessionId, sessionId, "run 请求必须携带 Hermes sessionId"); assert.equal(runBody.documentId, target.documentId, "run 请求必须携带 documentId"); assert(runBody.pageContext, "run 请求必须携带 pageContext"); await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("Smoke restored from Hermes session"), null, { timeout: UI_TIMEOUT_MS }, ); await page.waitForFunction( () => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("mnote.page.get"), null, { timeout: UI_TIMEOUT_MS }, ); assert(sessionDetailHits > 0, "刷新后必须从 Hermes session detail 恢复消息,而不是从 mnote 本地消息数组恢复"); const persisted = await page.evaluate(() => { const keys = Object.keys(window.localStorage).filter((key) => key.startsWith("hermes_page_ai_session:")); return keys.map((key) => ({ key, value: window.localStorage.getItem(key) })); }); assert( persisted.every((entry) => !entry.value || !entry.value.includes("Smoke response")), "mnote localStorage 不应保存完整聊天消息内容", ); console.log( JSON.stringify( { ok: true, baseUrl: BASE_URL, documentId: target.documentId, workspaceId: target.workspaceId, sessionId, runId, capturedKinds: captured.map((entry) => entry.kind), sessionDetailHits, }, null, 2, ), ); } finally { await cleanupDocuments(context.request, createdIds).catch(() => undefined); await context.close().catch(() => undefined); await browser.close().catch(() => undefined); } } main().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : String(error)); process.exit(1); });