Files
mnote/scripts/task-editor-delta-channel-smoke.js
T

222 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
"use strict";
/**
* task-editor-delta-channel-smoke.js
*
* 验证 Phase B — 编辑器增量 delta channel
* - blockDelta 出现在 Hermes 写工具响应中
* - blockDelta 可被前端拦截并推送给 leptos-tiptap 编辑器
* - 编辑器通过 CustomEvent 接收到 delta 后可应用(链式调用 ProseMirror
*
* 前提:运行中的 mnote-web (3000)、已登录浏览器、测试文档
*/
const assert = require("node:assert");
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");
const {
BASE_URL,
createTempDocument,
ensureAuthenticated,
openDocument,
cleanupDocuments,
requestJson,
} = require("./tree-shell-smoke-helpers");
const OUT_DIR = path.join(process.cwd(), "tmp", "editor-delta-channel-smoke");
const SUFFIX = `edc-${Date.now().toString(36)}`;
async function callTool(request, payload) {
return requestJson(request, "/api/hermes/tools/mnote/call", {
method: "POST",
headers: { "x-mnote-actor-id": payload.actorId || "smoke-user" },
data: payload,
});
}
async function main() {
await fs.mkdir(OUT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
const page = await context.newPage();
const request = page.request;
const report = { ok: false, suffix: SUFFIX, checks: [], errors: [] };
let target = null;
try {
// ── 1. 准备测试文档 ──
target = await createTempDocument(request, SUFFIX, {
workspaceName: `ws-editor-delta-${SUFFIX}`,
documentTitle: `测试-Delta-${SUFFIX}`,
content: [
{ type: "p", children: [{ text: `段落A ${SUFFIX}` }] },
{ type: "p", children: [{ text: `段落B ${SUFFIX}` }] },
{ type: "p", children: [{ text: `段落C ${SUFFIX}` }] },
],
});
report.documentId = target.documentId;
report.workspaceId = target.workspaceId;
console.log(`文档创建: ${target.documentId}`);
// ── 2. 打开文档页 ──
await ensureAuthenticated(page);
await openDocument(page, target.documentId, target.workspaceId);
await page.waitForTimeout(3000);
// ── 3. 注入 CustomEvent 监听器 ──
// 在浏览器中注册 block-delta 监听器,用于验证 blockDelta 推送链
const receivedDeltas = [];
await page.exposeFunction("__smoke_record_delta", (json) => {
try {
receivedDeltas.push(typeof json === "string" ? JSON.parse(json) : json);
} catch (e) {
console.error("delta parse error:", e);
}
});
await page.evaluate(() => {
window.addEventListener(
"mnote:editor:block-delta",
(event) => {
const detail = event.detail;
window.__smoke_record_delta(detail);
},
{ once: false },
);
});
// ── 4. 调用 block.replace,验证响应包含 blockDelta ──
const fetchRes = await callTool(request, {
toolName: "mnote.doc.fetch",
workspaceId: target.workspaceId,
documentId: target.documentId,
actorId: "smoke-user",
sessionId: `sess_fetch_${SUFFIX}`,
runId: `run_fetch_${SUFFIX}`,
toolCallId: `call_fetch_${SUFFIX}`,
traceId: `trace_fetch_${SUFFIX}`,
capabilityScope: ["page.read"],
args: { scope: "full", detail: "with_ids", maxBlocks: 20 },
});
const blocks = fetchRes.body?.blockDocument?.blocks || [];
assert.ok(blocks.length >= 3, `预期 ≥3 块,实际 ${blocks.length}`);
const blockIdB = blocks[1].blockId;
const replaceRes = await callTool(request, {
toolName: "mnote.block.replace",
workspaceId: target.workspaceId,
documentId: target.documentId,
actorId: "smoke-user",
sessionId: `sess_replace_${SUFFIX}`,
runId: `run_replace_${SUFFIX}`,
toolCallId: `call_replace_${SUFFIX}`,
traceId: `trace_replace_${SUFFIX}`,
idempotencyKey: `idem_replace_${SUFFIX}`,
capabilityScope: ["page.write", "page.read"],
args: {
blockId: blockIdB,
content: [
{ type: "paragraph", content: [{ type: "text", text: `段落B已替换 ${SUFFIX}` }] },
],
revision: fetchRes.body?.revision,
conflictDetectionKey: fetchRes.body?.conflictDetectionKey,
blockRevisionRef: blocks[1].revisionRef,
},
});
console.log("replace 响应 keys:", Object.keys(replaceRes).join(", "));
const hasBlockDelta = replaceRes.hasOwnProperty("blockDelta");
report.checks.push({
name: "replace 响应包含 blockDelta",
passed: hasBlockDelta,
details: hasBlockDelta
? `blockDelta 包含 ${replaceRes.blockDelta?.operations?.length || 0} 条操作`
: "响应中无 blockDelta 字段(可能 actor 未启用或未运行 mnote-web",
});
if (hasBlockDelta) {
const delta = replaceRes.blockDelta;
report.deltaReceived = delta;
// 验证 delta 结构
assert.ok(delta.documentId, "delta 应包含 documentId");
assert.ok(delta.revision > 0, "delta 应包含 revision");
assert.ok(
Array.isArray(delta.operations) && delta.operations.length > 0,
"delta 应包含至少一条操作",
);
report.checks.push({ name: "delta 结构有效", passed: true });
// ── 5. 在浏览器端主动触发 CustomEvent(模拟真实推送) ──
await page.evaluate(
(deltaJson) => {
const event = new CustomEvent("mnote:editor:block-delta", {
detail: deltaJson,
bubbles: true,
});
window.dispatchEvent(event);
},
delta,
);
await page.waitForTimeout(1000);
report.checks.push({
name: "CustomEvent 成功分派到 window",
passed: receivedDeltas.length > 0,
details: `收到 ${receivedDeltas.length} 条 delta 事件`,
});
// ── 6. 验证编辑器内容已更新(通过 runtime 回读) ──
const readbackRes = await callTool(request, {
toolName: "mnote.doc.fetch",
workspaceId: target.workspaceId,
documentId: target.documentId,
actorId: "smoke-user",
sessionId: `sess_readback_${SUFFIX}`,
runId: `run_readback_${SUFFIX}`,
toolCallId: `call_readback_${SUFFIX}`,
traceId: `trace_readback_${SUFFIX}`,
capabilityScope: ["page.read"],
args: { scope: "full", detail: "with_ids", maxBlocks: 20 },
});
const finalBlocks = readbackRes.body?.blockDocument?.blocks || [];
const hasReplaced = finalBlocks.some((b) => b.text?.includes("段落B已替换"));
report.checks.push({
name: "runtime 回读确认替换成功",
passed: hasReplaced,
details: finalBlocks.map((b) => b.text).join(" | "),
});
}
report.ok = report.checks.every((c) => c.passed);
report.passedCount = report.checks.filter((c) => c.passed).length;
report.totalCount = report.checks.length;
console.log(
`\n${report.ok ? "✅" : "⚠️"} Phase B smoke: ${report.passedCount}/${report.totalCount}`,
);
} catch (err) {
report.errors.push({ message: err.message, stack: err.stack });
console.error("❌ Phase B smoke 失败:", err);
} finally {
await fs.writeFile(
path.join(OUT_DIR, `${SUFFIX}.json`),
JSON.stringify(report, null, 2),
);
console.log(`报告: ${OUT_DIR}/${SUFFIX}.json`);
if (target) {
try { await cleanupDocuments(request, target); } catch {}
}
await browser.close();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});