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.
239 lines
8.3 KiB
JavaScript
239 lines
8.3 KiB
JavaScript
#!/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 < 800ms,Phase 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);
|
||
});
|