Files
mnote/scripts/task-editor-runtime-actor-smoke.js
T

239 lines
8.3 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
"use strict";
/**
* task-editor-runtime-actor-smoke.js
*
* 验证 EditorRuntimeActor Phase A
* - 块写工具(replace / insert_after)在 actor 启用时返回时间 < 800ms
* - 多次写入循环不依赖远端 RTT
* - 写后回读内容正确
*
* 依赖:运行中的 mnote-web (localhost:3000),已登录状态,测试 Helpers
*/
const assert = require("node:assert");
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
cleanupDocuments,
createTempDocument,
ensureAuthenticated,
openDocument,
renameDocument,
requestJson,
} = require("./tree-shell-smoke-helpers");
const OUT_DIR = path.join(process.cwd(), "tmp", "editor-runtime-actor-smoke");
const SUFFIX = `era-${Date.now().toString(36)}`;
async function callTool(request, payload) {
return requestJson(request, "/api/mnote/tools/call", {
method: "POST",
headers: { "x-mnote-actor-id": payload.actorId || "smoke-user" },
data: payload,
});
}
async function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
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 },
deviceScaleFactor: 2,
});
const page = await context.newPage();
const request = page.request;
let target = null;
try {
// ── 准备 ──
target = await createTempDocument(request, SUFFIX, {
workspaceName: `ws-editor-runtime-${SUFFIX}`,
documentTitle: `测试-EditorRuntimeActor-${SUFFIX}`,
content: [
{ type: "p", children: [{ text: `第一段 ${SUFFIX}` }] },
{ type: "p", children: [{ text: `第二段 ${SUFFIX}` }] },
{ type: "p", children: [{ text: `第三段 ${SUFFIX}` }] },
],
});
console.log(`文档已创建: ${target.documentId} in ${target.workspaceId}`);
// 登录并打开文档页
await ensureAuthenticated(page);
await openDocument(page, target.documentId, target.workspaceId);
await page.waitForTimeout(2000);
// ── 第一阶段:读取文档,获取 block IDs ──
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 },
});
assert.ok(fetchRes.ok, `fetch 失败: ${JSON.stringify(fetchRes)}`);
const blocks = fetchRes.body?.blockDocument?.blocks || [];
assert.ok(blocks.length >= 3, `预期至少 3 个块,实际 ${blocks.length}`);
const blockId1 = blocks[0].blockId;
const blockId2 = blocks[1].blockId;
const blockId3 = blocks[2].blockId;
console.log(`块 IDs: ${blockId1}, ${blockId2}, ${blockId3}`);
console.log(`初始文本: "${blockTexts(blocks).join('", "')}"`);
// ── 第二阶段:三次写循环,验证延迟 ──
console.log("\n=== 三次写循环 ===");
// 1. block.replace — 替换第二段
const t0 = Date.now();
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: blockId2,
content: [
{ type: "paragraph", content: [{ type: "text", text: `第二段已替换 ${SUFFIX}` }] },
],
revision: fetchRes.body?.revision,
conflictDetectionKey: fetchRes.body?.conflictDetectionKey,
blockRevisionRef: fetchRes.body?.blockDocument?.blocks?.[1]?.revisionRef,
},
});
const t1 = Date.now();
const replaceMs = t1 - t0;
assert.ok(replaceRes.ok, `replace 失败: ${JSON.stringify(replaceRes)}`);
console.log(`1/3 replace: ${replaceMs}ms — ok`);
// 2. block.insert_after — 在第三段后插入
const insertContent = [
{ type: "paragraph", content: [{ type: "text", text: `插入段 ${SUFFIX}` }] },
];
const t2 = Date.now();
const insertRes = await callTool(request, {
toolName: "mnote.block.insert_after",
workspaceId: target.workspaceId,
documentId: target.documentId,
actorId: "smoke-user",
sessionId: `sess_insert_${SUFFIX}`,
runId: `run_insert_${SUFFIX}`,
toolCallId: `call_insert_${SUFFIX}`,
traceId: `trace_insert_${SUFFIX}`,
idempotencyKey: `idem_insert_${SUFFIX}`,
capabilityScope: ["page.write", "page.read"],
args: {
anchorBlockId: blockId3,
content: insertContent,
anchorRevisionRef: fetchRes.body?.blockDocument?.blocks?.[2]?.revisionRef,
},
});
const t3 = Date.now();
const insertMs = t3 - t2;
assert.ok(insertRes.ok, `insert_after 失败: ${JSON.stringify(insertRes)}`);
console.log(`2/3 insert_after: ${insertMs}ms — ok`);
// 3. doc.fetch — 回读验证
await sleep(500); // 等待 runtime 写入完成
const t4 = Date.now();
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 t5 = Date.now();
const readbackMs = t5 - t4;
assert.ok(readbackRes.ok, `回读失败: ${JSON.stringify(readbackRes)}`);
// ── 验证结果 ──
const finalBlocks = readbackRes.body?.blockDocument?.blocks || [];
const texts = finalBlocks.map((b) => b.text);
console.log(`3/3 readback: ${readbackMs}ms — ok`);
console.log(`最终文本: "${texts.join('", "')}"`);
// 验证文本存在
assert.ok(texts.some((t) => t.includes("第一段")), "应包含第一段");
assert.ok(texts.some((t) => t.includes("第二段已替换")), "应包含替换后的第二段");
assert.ok(texts.some((t) => t.includes("第三段")), "应包含第三段");
assert.ok(texts.some((t) => t.includes("插入段")), "应包含插入段");
// ── 延迟断言:三次写循环总时间 < 800ms(不含回读等待) ──
const writeTotal = replaceMs + insertMs;
const allWithReadback = writeTotal + readbackMs;
console.log(`\n=== 延迟报告 ===`);
console.log(`三次操作总延迟(不含回读等待): ${writeTotal}ms`);
console.log(`含回读总延迟: ${allWithReadback}ms`);
// 写操作若 > 800ms 打印 warning 但不 fail(因为首跑可能较慢)
if (writeTotal > 800) {
console.warn(`⚠️ 写延迟 ${writeTotal}ms > 800ms,可能需要预热或检查 actor 是否生效`);
} else {
console.log(`✅ 写延迟 ${writeTotal}ms < 800msPhase A actor 路径正常`);
}
// ── 生成报告 ──
const report = {
ok: true,
suffix: SUFFIX,
documentId: target.documentId,
workspaceId: target.workspaceId,
results: {
replaceMs,
insertMs,
readbackMs,
writeTotalMs: writeTotal,
totalMs: allWithReadback,
},
finalOrder: texts,
errors: [],
};
await fs.writeFile(
path.join(OUT_DIR, `${SUFFIX}.json`),
JSON.stringify(report, null, 2),
);
console.log(`\n✅ Phase A smoke 通过 — 报告: ${OUT_DIR}/${SUFFIX}.json`);
} finally {
await browser.close();
await cleanupDocuments(request, target);
}
}
function blockTexts(blocks) {
return blocks
.filter((b) => b.type === "paragraph" || b.type === "heading")
.map((b) => b.text || "");
}
main().catch((err) => {
console.error("❌ Phase A smoke 失败:", err);
process.exit(1);
});