#!/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"); const EXPECTED_SETTINGS_URL = process.env.MNOTE_EXPECTED_HERMES_SETTINGS_URL || "http://127.0.0.1:3999/hermes/settings"; function deferred() { let resolve; const promise = new Promise((done) => { resolve = done; }); return { promise, resolve }; } async function main() { const suffix = Date.now().toString(36); const title = `TEST-HERMES-AI-runtime-${suffix}`; const sessionId = `mnote_runtime_${suffix}`; const runId = `run_runtime_${suffix}`; const createdIds = []; const sessionBodies = []; const runBodies = []; const abortBodies = []; const eventsRelease = deferred(); 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/profiles", async (route) => { await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, profiles: [{ name: "default", active: true, model: "gpt-5" }] }), }); }); await page.route("**/api/hermes/client/profile-memory**", async (route) => { await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, memory: "", user: "", soul: "" }), }); }); await page.route("**/api/hermes/client/skills**", async (route) => { await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, categories: [], archived: [] }), }); }); await page.route("**/api/hermes/client/capabilities**", async (route) => { await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, runtime: "mnote", categories: [], archived: [] }), }); }); await page.route("**/api/hermes/client/sessions", async (route) => { sessionBodies.push(JSON.parse(route.request().postData() || "{}")); await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, sessionId, title: "运行控制测试", profile: "default", traceId: `trace_session_${suffix}`, }), }); }); await page.route(`**/api/hermes/client/sessions/${sessionId}/resume`, async (route) => { await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, session: { sessionId, profile: "default", messages: [] } }), }); }); await page.route("**/api/hermes/client/runs", async (route) => { runBodies.push(JSON.parse(route.request().postData() || "{}")); await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, runId, sessionId, traceId: `trace_run_${suffix}` }), }); }); await page.route(`**/api/hermes/client/events/${runId}`, async (route) => { await eventsRelease.promise; await route.fulfill({ status: 200, headers: { "content-type": "text/event-stream; charset=utf-8" }, body: `data: ${JSON.stringify({ event: "run.aborted", run_id: runId, session_id: sessionId })}\n\n` + `data: ${JSON.stringify({ event: "message.delta", run_id: runId, session_id: sessionId, delta: "SHOULD_NOT_APPEAR_AFTER_ABORT" })}\n\n`, }); }); await page.route(`**/api/hermes/client/runs/${runId}/abort`, async (route) => { abortBodies.push(JSON.parse(route.request().postData() || "{}")); eventsRelease.resolve(); await route.fulfill({ status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, runId, status: "aborted", runtime: { sessionId, runId, status: "aborted" }, events: [ { event: "abort.started", runId }, { event: "abort.completed", runId }, ], }), }); }); 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('.wolai-page-ai-icon[data-page-ai-tab="agent"]').click({ timeout: UI_TIMEOUT_MS }); await page.locator('.wolai-page-ai-panel-section:not([hidden]) [data-page-ai-tab="runtime"]').click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( (expected) => window.__mnoteHermesSettingsUrl === expected, EXPECTED_SETTINGS_URL, { timeout: UI_TIMEOUT_MS }, ); await page.waitForFunction( () => (document.querySelector("[data-page-ai-tool-list]")?.textContent || "").includes("mnote.page.save"), null, { timeout: UI_TIMEOUT_MS }, ); const settingsDisabled = await page.locator('[data-page-ai-action="open-hermes-settings"]').isDisabled(); assert.equal(settingsDisabled, false, "配置 Hermes upstream 后设置入口不应禁用"); await page.locator('.wolai-page-ai-panel-section:not([hidden]) [data-page-ai-tab="agent"]').click({ timeout: UI_TIMEOUT_MS }); await page.locator("[data-page-ai-context-scope]").selectOption("options", { timeout: UI_TIMEOUT_MS }); await page.locator('.wolai-page-ai-panel-section:not([hidden]) [data-page-ai-tab="chat"]').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.documentElement.getAttribute("data-mnote-page-ai-run-status") === "running", null, { timeout: UI_TIMEOUT_MS }, ); await page.locator('[data-page-ai-action="stop-run"]').click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => document.documentElement.getAttribute("data-mnote-page-ai-run-status") === "aborted", null, { timeout: UI_TIMEOUT_MS }, ); await page.waitForTimeout(100); const drawerTextAfterAbort = await page.locator('[data-testid="wolai-page-ai-drawer"]').textContent({ timeout: UI_TIMEOUT_MS, }); assert( !drawerTextAfterAbort.includes("SHOULD_NOT_APPEAR_AFTER_ABORT"), "abort 后不应继续追加该 run 的 assistant delta", ); assert(sessionBodies.some((body) => body.profile === "default"), "创建 session 必须携带 profile"); assert(abortBodies.some((body) => body.reason === "page_ai_user_stop"), "停止 run 必须调用 Hermes abort API"); assert(runBodies.length > 0, "必须捕获 run 请求"); const runBody = runBodies[0]; assert.equal(runBody.contextScope, "options", "run 请求必须携带 contextScope"); assert.equal(runBody.pageContext.contextScope, "options", "pageContext 必须记录 contextScope"); assert.equal(runBody.pageContext.documentBlocks, null, "页面设置 scope 不应携带正文 blocks"); assert(runBody.pageContext.pageOptions, "页面设置 scope 必须携带 pageOptions"); console.log( JSON.stringify( { ok: true, baseUrl: BASE_URL, documentId: target.documentId, workspaceId: target.workspaceId, runId, settingsUrl: await page.evaluate(() => window.__mnoteHermesSettingsUrl), runContextScope: runBody.contextScope, abortCount: abortBodies.length, }, null, 2, ), ); } finally { eventsRelease.resolve(); 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); });