feat(ai): switch page ai to hermes panel
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
openDocument,
|
||||
renameDocument,
|
||||
requestJson,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function assertPageAiPermissionFailureUi(page, target, suffix) {
|
||||
const sessionId = `mnote_smoke_permission_${suffix}`;
|
||||
const runId = `run_smoke_permission_${suffix}`;
|
||||
const sessionRoute = "**/api/hermes/client/sessions";
|
||||
const sessionDetailRoute = `**/api/hermes/client/sessions/${sessionId}`;
|
||||
const runsRoute = "**/api/hermes/client/runs";
|
||||
const eventsRoute = `**/api/hermes/client/events/${runId}`;
|
||||
|
||||
await page.route(sessionRoute, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId,
|
||||
title: "权限失败 UI 验证",
|
||||
traceId: `trace_permission_${suffix}`,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(sessionDetailRoute, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId,
|
||||
session: { sessionId, messages: [] },
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(runsRoute, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
upstream: { run_id: runId, status: "started" },
|
||||
traceId: `trace_permission_${suffix}`,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(eventsRoute, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream; charset=utf-8" },
|
||||
body:
|
||||
`data: ${JSON.stringify({ event: "tool.started", run_id: runId, session_id: sessionId, tool: "mnote.page.save" })}\n\n` +
|
||||
`data: ${JSON.stringify({ event: "tool.failed", run_id: runId, session_id: sessionId, tool: "mnote.page.save", error: { code: "permission_denied" } })}\n\n` +
|
||||
`data: ${JSON.stringify({ event: "run.completed", run_id: runId, session_id: sessionId, output: "permission_denied" })}\n\n`,
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await openDocument(page, target.workspaceId, target.documentId);
|
||||
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator("[data-page-ai-input]").fill("权限失败 UI 验证", { timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const text = document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "";
|
||||
return text.includes("mnote.page.save") && text.includes("permission_denied");
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
} finally {
|
||||
await page.unroute(sessionRoute).catch(() => undefined);
|
||||
await page.unroute(sessionDetailRoute).catch(() => undefined);
|
||||
await page.unroute(runsRoute).catch(() => undefined);
|
||||
await page.unroute(eventsRoute).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const suffix = Date.now().toString(36);
|
||||
const title = `TEST-HERMES-AI-write-${suffix}`;
|
||||
const marker = `TEST-HERMES-AI-WRITEBACK-${suffix}`;
|
||||
const createdIds = [];
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await ensureAuthenticated(page, context.request);
|
||||
const target = await createTempDocument(context.request);
|
||||
createdIds.push(target.documentId);
|
||||
await renameDocument(context.request, target.workspaceId, target.documentId, title);
|
||||
|
||||
const baseTool = {
|
||||
toolName: "mnote.page.save",
|
||||
workspaceId: target.workspaceId,
|
||||
documentId: target.documentId,
|
||||
actorId: "smoke-user",
|
||||
sessionId: `mnote_smoke_write_${suffix}`,
|
||||
runId: `run_smoke_write_${suffix}`,
|
||||
toolCallId: `call_smoke_write_${suffix}`,
|
||||
traceId: `trace_smoke_write_${suffix}`,
|
||||
idempotencyKey: `idem_smoke_write_${suffix}`,
|
||||
capabilityScope: ["page.write"],
|
||||
args: {
|
||||
mode: "replace",
|
||||
content: [
|
||||
{
|
||||
id: `block_${suffix}`,
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: marker }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const deniedResponse = await context.request.fetch(`${BASE_URL}/api/hermes/tools/mnote/call`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-mnote-actor-id": "smoke-user",
|
||||
"x-mnote-workspace-id": "ws_denied",
|
||||
},
|
||||
data: JSON.stringify({
|
||||
...baseTool,
|
||||
traceId: `trace_smoke_write_denied_${suffix}`,
|
||||
toolCallId: `call_smoke_write_denied_${suffix}`,
|
||||
idempotencyKey: `idem_smoke_write_denied_${suffix}`,
|
||||
}),
|
||||
});
|
||||
const deniedText = await deniedResponse.text();
|
||||
assert.equal(deniedResponse.status(), 403, "workspace 上下文冲突应返回 403");
|
||||
assert(deniedText.includes("workspace_context_conflict"), "权限失败应返回稳定 workspace_context_conflict");
|
||||
assert(!deniedText.includes(title), "权限失败响应不应泄露页面标题");
|
||||
assert(!deniedText.includes(marker), "权限失败响应不应泄露正文内容");
|
||||
await assertPageAiPermissionFailureUi(page, target, suffix);
|
||||
|
||||
const dryRun = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: { ...baseTool, dryRun: true, idempotencyKey: `idem_smoke_write_dry_${suffix}` },
|
||||
});
|
||||
assert.equal(dryRun.result.dryRun, true, "dryRun 不应写入");
|
||||
const dryRunContent = await requestJson(
|
||||
context.request,
|
||||
`/api/documents/content?documentId=${encodeURIComponent(target.documentId)}&workspaceId=${encodeURIComponent(target.workspaceId)}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
assert(!JSON.stringify(dryRunContent).includes(marker), "dryRun 后不应读到 AI 写入标记");
|
||||
|
||||
await openDocument(page, target.workspaceId, target.documentId);
|
||||
await page
|
||||
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]')
|
||||
.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS })
|
||||
.catch(() => undefined);
|
||||
|
||||
const write = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: { ...baseTool, dryRun: false },
|
||||
});
|
||||
assert.equal(write.ok, true, "写入 tool call 应成功");
|
||||
assert.equal(write.audit.effect, "write", "写入 audit effect 应为 write");
|
||||
assert.equal(write.result.commandName, "page.body.save", "写入必须走 page.body.save");
|
||||
assert(write.result.commandId, "写入结果必须包含 Rust commandId");
|
||||
assert.equal(write.audit.commandId, write.result.commandId, "audit commandId 必须指向 Rust commandId");
|
||||
await page.waitForFunction((expected) => (document.body.textContent || "").includes(expected), marker, {
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
const retry = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: { ...baseTool, dryRun: false },
|
||||
});
|
||||
assert.equal(retry.ok, true, "幂等重试 tool call 应成功");
|
||||
assert.equal(retry.result.commandId, write.result.commandId, "同一 idempotencyKey 重试必须返回同一个 commandId");
|
||||
assert.equal(retry.audit.commandId, write.audit.commandId, "同一 idempotencyKey 重试必须返回同一个 audit commandId");
|
||||
|
||||
const content = await requestJson(
|
||||
context.request,
|
||||
`/api/documents/content?documentId=${encodeURIComponent(target.documentId)}&workspaceId=${encodeURIComponent(target.workspaceId)}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
assert(JSON.stringify(content).includes(marker), "刷新读取内容后未找到 AI 写入标记");
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: target.documentId,
|
||||
workspaceId: target.workspaceId,
|
||||
marker,
|
||||
commandName: write.result.commandName,
|
||||
audit: write.audit,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await cleanupDocuments(context.request, createdIds).catch(() => undefined);
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user