Files
mnote/scripts/task-editor-runtime-actor-smoke.js
Agent Board 262e66b02e feat: purge legacy agent hosts and land vault Chrome extension path
Retire ACP/Hermes/OpenCode surfaces and rename hermes_tools to
mnote_agent_tools so Page AI stays on Pi Lab only. Add Chrome vault
extension + extension token route, pre-release purge design, and soft-retire
legacy smokes for the small-group production cut.
2026-07-25 14:25:37 +08:00

239 lines
8.3 KiB
JavaScript
Raw Permalink 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-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);
});