#!/usr/bin/env node "use strict"; const assert = require("node:assert"); const { fetchWithTimeout, findFreePort, startGateway, waitForGateway, } = require("./task114-rust-web-gateway-entry-smoke.js"); const EXTERNAL_BASE_URL = (process.env.MNOTE_UI_BASE_URL || "").replace(/\/+$/, ""); const CONFIGURED_DOCUMENT_ID = (process.env.MNOTE_DOCUMENT_ID || "").trim(); const CONFIGURED_WORKSPACE_ID = (process.env.MNOTE_WORKSPACE_ID || "").trim(); const CONFIGURED_TITLE = (process.env.MNOTE_DOCUMENT_TITLE || "").trim(); async function readJsonResponse(response, label) { const text = await response.text(); let payload = null; try { payload = text ? JSON.parse(text) : null; } catch { throw new Error(`${label} 返回了非 JSON 内容: ${text.slice(0, 1200)}`); } assert(response.ok, `${label} 请求失败: ${response.status}; body: ${text.slice(0, 1200)}`); return payload; } async function createTempDocument(baseUrl) { const title = `task115-rust-web-${Date.now()}`; const requestBody = { action: "create", title, }; if (CONFIGURED_WORKSPACE_ID) { requestBody.workspaceId = CONFIGURED_WORKSPACE_ID; } const response = await fetchWithTimeout(`${baseUrl}/api/tree/commands`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(requestBody), }); const payload = await readJsonResponse(response, "创建临时文档"); const result = payload && typeof payload.result === "object" ? payload.result : null; const documentId = result && typeof result.documentId === "string" ? result.documentId : ""; const workspaceId = result && typeof result.workspaceId === "string" ? result.workspaceId : ""; assert(documentId, "创建临时文档缺少 documentId"); assert(workspaceId, "创建临时文档缺少 workspaceId"); return { documentId, workspaceId, title }; } async function purgeTempDocument(baseUrl, target) { const response = await fetchWithTimeout(`${baseUrl}/api/tree/commands`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "purge", workspaceId: target.workspaceId, documentId: target.documentId, }), }); await readJsonResponse(response, "清理临时文档"); } async function validateDocumentShell(baseUrl, target) { const targetPath = `/documents/${encodeURIComponent(target.documentId)}?workspaceId=${encodeURIComponent(target.workspaceId)}`; const response = await fetchWithTimeout(`${baseUrl}${targetPath}`); const html = await response.text(); assert.equal(response.status, 200, `文档 shell 请求失败: ${response.status}; body: ${html.slice(0, 1200)}`); assert.equal(response.headers.get("x-mnote-web-owner"), "mnote-web"); assert.equal(response.headers.get("x-mnote-web-shell"), "document"); assert.match(response.headers.get("content-type") || "", /text\/html/); assert.match(html, /data-mnote-shell="document"/); assert.match(html, /data-testid="wolai-sidebar"/); assert.match(html, /data-testid="wolai-topbar"/); assert.match(html, /data-testid="wolai-floating-ai"/); assert.match(html, /data-page-aggregate-snapshot="mnote\.page_aggregate\.v1"/); assert.match(html, /id="__MNOTE_PAGE_AGGREGATE__"/); assert.match(html, /data-editor-host="leptos_tiptap_island"/); assert.doesNotMatch(html, /mnote-web-document-shell/); assert.doesNotMatch(html, /convex_upstream_error|未登录/); const aggregate = await fetchWithTimeout( `${baseUrl}/api/page-aggregate/${encodeURIComponent(target.documentId)}?workspaceId=${encodeURIComponent(target.workspaceId)}`, ); const aggregateText = await aggregate.text(); assert.equal(aggregate.status, 200, `page aggregate 请求失败: ${aggregate.status}; body: ${aggregateText.slice(0, 1200)}`); const payload = JSON.parse(aggregateText); assert.equal(aggregate.headers.get("x-mnote-web-owner"), "mnote-web"); assert.equal(payload.owner, "mnote-web"); assert.equal(payload.schema, "mnote.page_aggregate.v1"); assert.equal(payload.result.identity.documentId, target.documentId); assert.equal(payload.result.identity.workspaceId, target.workspaceId); assert.equal(payload.result.schema, "mnote.page_aggregate.v1"); assert.equal(typeof payload.result.head.title, "string"); assert(payload.result.head.title.trim(), "Page Aggregate 标题不能为空"); if (target.expectedTitle) { assert.equal(payload.result.head.title, target.expectedTitle); } } async function main() { let gateway = null; let baseUrl = EXTERNAL_BASE_URL; let createdTarget = null; if (!baseUrl) { const port = await findFreePort(); baseUrl = `http://127.0.0.1:${port}`; gateway = startGateway(port); await waitForGateway(baseUrl); } try { const target = CONFIGURED_DOCUMENT_ID ? { documentId: CONFIGURED_DOCUMENT_ID, workspaceId: CONFIGURED_WORKSPACE_ID || "ws_demo", expectedTitle: CONFIGURED_TITLE || null, } : gateway ? { documentId: "doc_1", workspaceId: "ws_demo", expectedTitle: CONFIGURED_TITLE || "服务端页面", } : await createTempDocument(baseUrl); if (!CONFIGURED_DOCUMENT_ID && !gateway) { createdTarget = target; target.expectedTitle = target.title; } await validateDocumentShell(baseUrl, target); console.log( JSON.stringify( { ok: true, baseUrl, owner: "mnote-web", shell: "document", editorHost: "leptos_tiptap_island", documentId: target.documentId, workspaceId: target.workspaceId, }, null, 2, ), ); } finally { if (createdTarget) { await purgeTempDocument(baseUrl, createdTarget); } if (gateway && gateway.pid) { gateway.kill("SIGTERM"); setTimeout(() => { if (!gateway.killed) { gateway.kill("SIGKILL"); } }, 2000).unref(); } } } main().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : String(error)); process.exit(1); });