feat(ai): switch page ai to hermes panel
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
renameDocument,
|
||||
requestJson,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function callArtifact(request, target, suffix, toolName, args) {
|
||||
return requestJson(request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: {
|
||||
toolName,
|
||||
workspaceId: target.workspaceId,
|
||||
documentId: target.documentId,
|
||||
actorId: "smoke-user",
|
||||
sessionId: `mnote_smoke_artifact_${suffix}`,
|
||||
runId: `run_smoke_artifact_${suffix}`,
|
||||
toolCallId: `call_${toolName.replace(/\W+/g, "_")}_${suffix}`,
|
||||
traceId: `trace_smoke_artifact_${suffix}`,
|
||||
idempotencyKey: `idem_${toolName.replace(/\W+/g, "_")}_${suffix}`,
|
||||
dryRun: false,
|
||||
capabilityScope: ["artifact.write"],
|
||||
args,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function assertArtifactReferenceEdge(edgesPayload, sourceDocumentId, artifactDocumentId, artifactType) {
|
||||
const edges = Array.isArray(edgesPayload?.result?.edges) ? edgesPayload.result.edges : [];
|
||||
const edge = edges.find(
|
||||
(candidate) =>
|
||||
candidate &&
|
||||
candidate.fromNodeId === sourceDocumentId &&
|
||||
candidate.toNodeId === artifactDocumentId &&
|
||||
candidate.metadata?.kind === "ai_artifact_reference",
|
||||
);
|
||||
assert(edge, `未查询到 ${artifactDocumentId} 的 ai_artifact_reference edge`);
|
||||
assert.equal(edge.edgeType, "source_of", `${artifactDocumentId} reference edge 类型不对`);
|
||||
assert.equal(edge.metadata.artifactType, artifactType, `${artifactDocumentId} artifactType 不对`);
|
||||
assert.equal(edge.metadata.projectionOnlyGroup, "AI Artifacts", `${artifactDocumentId} projection group 不对`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const suffix = Date.now().toString(36);
|
||||
const title = `TEST-HERMES-AI-artifact-${suffix}`;
|
||||
const quickSessionId = `mnote_smoke_artifact_quick_${suffix}`;
|
||||
let quickRunIndex = 0;
|
||||
const quickRunBodies = [];
|
||||
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 page.route("**/api/hermes/tools/mnote/call", async (route) => {
|
||||
throw new Error(`页面 AI artifact 快捷入口不应直连 mnote tool route: ${route.request().url()}`);
|
||||
});
|
||||
await page.route("**/api/hermes/client/sessions", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId: quickSessionId,
|
||||
title: "当前页问答",
|
||||
traceId: `trace_quick_session_${suffix}`,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/hermes/client/sessions/${quickSessionId}`, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId: quickSessionId,
|
||||
session: { sessionId: quickSessionId, messages: [] },
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/hermes/client/runs", async (route) => {
|
||||
quickRunIndex += 1;
|
||||
quickRunBodies.push(JSON.parse(route.request().postData() || "{}"));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId: quickSessionId,
|
||||
runId: `run_quick_artifact_${suffix}_${quickRunIndex}`,
|
||||
traceId: `trace_quick_artifact_${suffix}_${quickRunIndex}`,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/hermes/client/events/**", async (route) => {
|
||||
const runId = route.request().url().split("/").pop();
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream; charset=utf-8" },
|
||||
body:
|
||||
`data: ${JSON.stringify({ event: "message.delta", run_id: runId, session_id: quickSessionId, delta: "artifact intent queued" })}\n\n` +
|
||||
`data: ${JSON.stringify({ event: "run.completed", run_id: runId, session_id: quickSessionId, output: "artifact intent queued" })}\n\n`,
|
||||
});
|
||||
});
|
||||
|
||||
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 summary = await callArtifact(context.request, target, suffix, "mnote.artifact.create_summary", {
|
||||
summary: `摘要 ${suffix}`,
|
||||
});
|
||||
const summaryRetry = await callArtifact(context.request, target, suffix, "mnote.artifact.create_summary", {
|
||||
summary: `摘要 ${suffix}`,
|
||||
});
|
||||
const aiNote = await callArtifact(context.request, target, suffix, "mnote.artifact.create_ai_note", {
|
||||
content: `AI Note ${suffix}`,
|
||||
});
|
||||
const aiNoteSecond = await callArtifact(context.request, target, `${suffix}_second`, "mnote.artifact.create_ai_note", {
|
||||
content: `AI Note second ${suffix}`,
|
||||
});
|
||||
const artifactDocumentIds = Array.from(
|
||||
new Set([
|
||||
summary.result.artifactDocumentId,
|
||||
aiNote.result.artifactDocumentId,
|
||||
aiNoteSecond.result.artifactDocumentId,
|
||||
]),
|
||||
);
|
||||
createdIds.push(...artifactDocumentIds);
|
||||
|
||||
for (const result of [summary, aiNote, aiNoteSecond]) {
|
||||
assert.equal(result.ok, true, `${result.toolName} 应成功`);
|
||||
assert.equal(result.audit.effect, "write", `${result.toolName} audit 应为 write`);
|
||||
assert.equal(result.result.commandName, "tree.node.create", `${result.toolName} 必须走 tree.node.create`);
|
||||
assert.equal(result.audit.commandId, result.result.commandId, `${result.toolName} audit commandId 必须指向 Rust commandId`);
|
||||
assert(result.result.artifactDocumentId, `${result.toolName} 缺少 artifactDocumentId`);
|
||||
assert.equal(result.result.referenceEdge?.from, target.documentId, `${result.toolName} reference edge from 不对`);
|
||||
assert.equal(result.result.referenceEdge?.to, result.result.artifactDocumentId, `${result.toolName} reference edge to 不对`);
|
||||
assert.equal(result.result.referenceEdge?.kind, "ai_artifact_reference", `${result.toolName} reference edge kind 不对`);
|
||||
assert(result.result.artifacts?.commandLog, `${result.toolName} 缺少 commandLog artifact`);
|
||||
assert(result.result.artifacts?.domainEvent, `${result.toolName} 缺少 domainEvent artifact`);
|
||||
assert.equal(
|
||||
result.result.artifacts.domainEvent.eventType,
|
||||
"tree.node.created",
|
||||
`${result.toolName} domain event 类型不对`,
|
||||
);
|
||||
}
|
||||
assert.equal(summaryRetry.result.commandId, summary.result.commandId, "summary 同 idempotencyKey 重试必须返回同一 commandId");
|
||||
assert.equal(
|
||||
summaryRetry.result.artifactDocumentId,
|
||||
summary.result.artifactDocumentId,
|
||||
"summary 同页面重试必须指向同一 summary document",
|
||||
);
|
||||
assert.notEqual(
|
||||
aiNoteSecond.result.artifactDocumentId,
|
||||
aiNote.result.artifactDocumentId,
|
||||
"ai_note 多次创建必须生成独立 artifact document",
|
||||
);
|
||||
|
||||
const edgePayload = await requestJson(
|
||||
context.request,
|
||||
`/api/kernel/edges?workspaceId=${encodeURIComponent(target.workspaceId)}&nodeId=${encodeURIComponent(target.documentId)}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
assertArtifactReferenceEdge(edgePayload, target.documentId, summary.result.artifactDocumentId, "summary");
|
||||
assertArtifactReferenceEdge(edgePayload, target.documentId, aiNote.result.artifactDocumentId, "ai_note");
|
||||
assertArtifactReferenceEdge(edgePayload, target.documentId, aiNoteSecond.result.artifactDocumentId, "ai_note");
|
||||
|
||||
const fileProjection = await requestJson(
|
||||
context.request,
|
||||
`/api/tree/projections/file?workspaceId=${encodeURIComponent(target.workspaceId)}&rootNodeId=${encodeURIComponent(target.documentId)}&depth=2`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
const projectionItems = Array.isArray(fileProjection?.result?.items) ? fileProjection.result.items : [];
|
||||
const aiArtifactsMeta = fileProjection?.result?.meta?.aiArtifacts || {};
|
||||
assert.equal(aiArtifactsMeta.title, "AI Artifacts", "AI Artifacts projection meta 标题不对");
|
||||
assert.equal(aiArtifactsMeta.projectionOnly, true, "AI Artifacts 必须是 projection-only 分组");
|
||||
assert.equal(aiArtifactsMeta.kernelNodeId, null, "AI Artifacts 不应是真实 kernel node");
|
||||
assert(
|
||||
!projectionItems.some((item) => item && item.nodeId === "AI Artifacts"),
|
||||
"AI Artifacts 不应出现在 projection items 中作为真实 node",
|
||||
);
|
||||
for (const artifactDocumentId of artifactDocumentIds) {
|
||||
assert(
|
||||
aiArtifactsMeta.artifactDocumentIds?.includes(artifactDocumentId),
|
||||
`AI Artifacts projection meta 缺少 ${artifactDocumentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const auditPayload = await requestJson(
|
||||
context.request,
|
||||
`/api/hermes/tools/mnote/audit?traceId=${encodeURIComponent(`trace_smoke_artifact_${suffix}`)}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
},
|
||||
);
|
||||
const auditEvents = Array.isArray(auditPayload.events) ? auditPayload.events : [];
|
||||
for (const result of [summary, aiNote]) {
|
||||
assert(
|
||||
auditEvents.some(
|
||||
(event) =>
|
||||
event &&
|
||||
event.phase === "completed" &&
|
||||
event.toolCallId === result.toolCallId &&
|
||||
event.audit?.commandId === result.result.commandId,
|
||||
),
|
||||
`${result.toolName} audit 未串到 Hermes tool call 和 Rust command`,
|
||||
);
|
||||
}
|
||||
|
||||
await page.goto(
|
||||
`${BASE_URL}/documents/${encodeURIComponent(target.documentId)}?workspaceId=${encodeURIComponent(target.workspaceId)}`,
|
||||
{ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-page-ai-intent="create-summary"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("artifact intent queued"),
|
||||
undefined,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.locator('[data-page-ai-intent="create-ai-note"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
const deadline = Date.now() + UI_TIMEOUT_MS;
|
||||
while (quickRunBodies.length < 2 && Date.now() < deadline) {
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
assert.equal(quickRunBodies.length, 2, "两个 artifact 快捷入口都必须发送 Hermes run intent");
|
||||
assert(
|
||||
quickRunBodies[0].message.includes("mnote.artifact.create_summary"),
|
||||
"创建 Summary 快捷入口必须发送 Hermes artifact summary intent",
|
||||
);
|
||||
assert(
|
||||
quickRunBodies[1].message.includes("mnote.artifact.create_ai_note"),
|
||||
"创建 AI Note 快捷入口必须发送 Hermes artifact ai_note intent",
|
||||
);
|
||||
assert(
|
||||
quickRunBodies.every((body) => body.documentId === target.documentId && body.sessionId === quickSessionId),
|
||||
"artifact 快捷入口必须携带当前页面 documentId 与 Hermes sessionId",
|
||||
);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: target.documentId,
|
||||
workspaceId: target.workspaceId,
|
||||
summaryArtifactType: summary.result.artifactType,
|
||||
aiNoteArtifactType: aiNote.result.artifactType,
|
||||
summaryCommand: summary.result.commandName,
|
||||
aiNoteCommand: aiNote.result.commandName,
|
||||
summaryArtifactDocumentId: summary.result.artifactDocumentId,
|
||||
aiNoteArtifactDocumentId: aiNote.result.artifactDocumentId,
|
||||
aiNoteSecondArtifactDocumentId: aiNoteSecond.result.artifactDocumentId,
|
||||
referenceEdgeCount: edgePayload.result.edges.length,
|
||||
aiArtifactsMeta,
|
||||
auditEventCount: auditEvents.length,
|
||||
quickIntentMessages: quickRunBodies.map((body) => body.message),
|
||||
},
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
renameDocument,
|
||||
requestJson,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function main() {
|
||||
const suffix = Date.now().toString(36);
|
||||
const title = `TEST-HERMES-AI-audit-${suffix}`;
|
||||
const marker = `TEST-HERMES-AI-AUDIT-${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 readTraceId = `trace_audit_read_${suffix}`;
|
||||
const writeTraceId = `trace_audit_write_${suffix}`;
|
||||
const deniedTraceId = `trace_audit_denied_${suffix}`;
|
||||
const readToolCallId = `call_audit_read_${suffix}`;
|
||||
const writeToolCallId = `call_audit_write_${suffix}`;
|
||||
const deniedToolCallId = `call_audit_denied_${suffix}`;
|
||||
|
||||
const read = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: {
|
||||
toolName: "mnote.page.get",
|
||||
workspaceId: target.workspaceId,
|
||||
documentId: target.documentId,
|
||||
actorId: "smoke-user",
|
||||
sessionId: `mnote_audit_${suffix}`,
|
||||
runId: `run_audit_read_${suffix}`,
|
||||
toolCallId: readToolCallId,
|
||||
traceId: readTraceId,
|
||||
capabilityScope: ["page.read"],
|
||||
},
|
||||
});
|
||||
assert.equal(read.audit.effect, "read", "读工具 audit effect 应为 read");
|
||||
|
||||
const write = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: {
|
||||
toolName: "mnote.page.save",
|
||||
workspaceId: target.workspaceId,
|
||||
documentId: target.documentId,
|
||||
actorId: "smoke-user",
|
||||
sessionId: `mnote_audit_${suffix}`,
|
||||
runId: `run_audit_write_${suffix}`,
|
||||
toolCallId: writeToolCallId,
|
||||
traceId: writeTraceId,
|
||||
idempotencyKey: `idem_audit_write_${suffix}`,
|
||||
dryRun: false,
|
||||
capabilityScope: ["page.write"],
|
||||
args: {
|
||||
mode: "replace",
|
||||
content: [
|
||||
{
|
||||
id: `audit_block_${suffix}`,
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: marker }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(write.audit.effect, "write", "写工具 audit effect 应为 write");
|
||||
assert(write.audit.commandId, "写工具 audit 必须包含 commandId");
|
||||
|
||||
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({
|
||||
toolName: "mnote.page.save",
|
||||
workspaceId: target.workspaceId,
|
||||
documentId: target.documentId,
|
||||
actorId: "smoke-user",
|
||||
sessionId: `mnote_audit_${suffix}`,
|
||||
runId: `run_audit_denied_${suffix}`,
|
||||
toolCallId: deniedToolCallId,
|
||||
traceId: deniedTraceId,
|
||||
idempotencyKey: `idem_audit_denied_${suffix}`,
|
||||
dryRun: false,
|
||||
capabilityScope: ["page.write"],
|
||||
args: {
|
||||
mode: "replace",
|
||||
content: [
|
||||
{
|
||||
id: `audit_denied_block_${suffix}`,
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: marker }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
const deniedText = await deniedResponse.text();
|
||||
assert.equal(deniedResponse.status(), 403, "权限失败 smoke 应返回 workspace_context_conflict");
|
||||
assert(!deniedText.includes(title), "权限失败响应不应包含页面标题");
|
||||
assert(!deniedText.includes(marker), "权限失败响应不应包含正文 marker");
|
||||
|
||||
const readAudit = await requestJson(
|
||||
context.request,
|
||||
`/api/hermes/tools/mnote/audit?traceId=${encodeURIComponent(readTraceId)}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
},
|
||||
);
|
||||
const writeAudit = await requestJson(
|
||||
context.request,
|
||||
`/api/hermes/tools/mnote/audit?traceId=${encodeURIComponent(writeTraceId)}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
},
|
||||
);
|
||||
const writePersistedAudit = await requestJson(
|
||||
context.request,
|
||||
`/api/hermes/tools/mnote/audit?traceId=${encodeURIComponent(writeTraceId)}&persistedOnly=true`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
},
|
||||
);
|
||||
const deniedPersistedAudit = await requestJson(
|
||||
context.request,
|
||||
`/api/hermes/tools/mnote/audit?traceId=${encodeURIComponent(deniedTraceId)}&persistedOnly=true`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
},
|
||||
);
|
||||
|
||||
const readPhases = new Set((readAudit.events || []).map((event) => event.phase));
|
||||
const writePhases = new Set((writeAudit.events || []).map((event) => event.phase));
|
||||
const writePersistedPhases = new Set((writePersistedAudit.events || []).map((event) => event.phase));
|
||||
const deniedPersistedPhases = new Set((deniedPersistedAudit.events || []).map((event) => event.phase));
|
||||
assert(readPhases.has("started"), "读工具 audit 缺少 started");
|
||||
assert(readPhases.has("completed"), "读工具 audit 缺少 completed");
|
||||
assert(writePhases.has("started"), "写工具 audit 缺少 started");
|
||||
assert(writePhases.has("completed"), "写工具 audit 缺少 completed");
|
||||
assert(writePersistedPhases.has("started"), "持久化写工具 audit 缺少 started");
|
||||
assert(writePersistedPhases.has("completed"), "持久化写工具 audit 缺少 completed");
|
||||
assert(deniedPersistedPhases.has("failed"), "持久化权限失败 audit 缺少 failed");
|
||||
assert(
|
||||
(writeAudit.events || []).some((event) => event.audit?.commandId === write.audit.commandId),
|
||||
"写工具 audit 查询结果未串到 Rust commandId",
|
||||
);
|
||||
assert(
|
||||
(writePersistedAudit.events || []).some((event) => event.audit?.commandId === write.audit.commandId),
|
||||
"持久化写工具 audit 查询结果未串到 Rust commandId",
|
||||
);
|
||||
assert(!JSON.stringify(writeAudit).includes(marker), "audit 查询结果不应包含完整正文内容");
|
||||
assert(!JSON.stringify(writePersistedAudit).includes(marker), "持久化 audit 不应包含完整正文内容");
|
||||
assert(!JSON.stringify(deniedPersistedAudit).includes(marker), "权限失败持久化 audit 不应包含完整正文内容");
|
||||
assert(!JSON.stringify(deniedPersistedAudit).includes(title), "权限失败持久化 audit 不应包含页面标题");
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: target.documentId,
|
||||
workspaceId: target.workspaceId,
|
||||
readTraceId,
|
||||
writeTraceId,
|
||||
deniedTraceId,
|
||||
writeCommandId: write.audit.commandId,
|
||||
readPhases: Array.from(readPhases),
|
||||
writePhases: Array.from(writePhases),
|
||||
writePersistedPhases: Array.from(writePersistedPhases),
|
||||
deniedPersistedPhases: Array.from(deniedPersistedPhases),
|
||||
},
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
renameDocument,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function main() {
|
||||
const suffix = Date.now().toString(36);
|
||||
const title = `TEST-HERMES-AI-baseline-${suffix}`;
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
|
||||
const page = await context.newPage();
|
||||
const captured = [];
|
||||
const createdIds = [];
|
||||
|
||||
page.on("request", (request) => {
|
||||
const url = request.url();
|
||||
if (url.includes("/api/ai-agent/run") || url.includes("/api/hermes/client/")) {
|
||||
captured.push({
|
||||
phase: "request",
|
||||
method: request.method(),
|
||||
url,
|
||||
postData: request.postData() || "",
|
||||
});
|
||||
}
|
||||
});
|
||||
page.on("response", async (response) => {
|
||||
const url = response.url();
|
||||
if (!url.includes("/api/ai-agent/run") && !url.includes("/api/hermes/client/")) {
|
||||
return;
|
||||
}
|
||||
let body = "";
|
||||
try {
|
||||
body = (await response.text()).slice(0, 1600);
|
||||
} catch {
|
||||
body = "<unreadable>";
|
||||
}
|
||||
captured.push({
|
||||
phase: "response",
|
||||
status: response.status(),
|
||||
url,
|
||||
body,
|
||||
headers: response.headers(),
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
await page.goto(
|
||||
`${BASE_URL}/documents/${encodeURIComponent(target.documentId)}?workspaceId=${encodeURIComponent(target.workspaceId)}`,
|
||||
{ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator("[data-page-ai-input]").fill("概括当前页面标题和第一段", { timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => document.querySelectorAll('[data-testid="wolai-page-ai-drawer"] .wolai-page-ai-message--assistant').length > 0,
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
const aiRequests = captured.filter((entry) => entry.phase === "request");
|
||||
assert(aiRequests.length > 0, "未捕获页面 AI 主请求");
|
||||
const firstRequest = aiRequests[0];
|
||||
const legacyRunHit = aiRequests.some((entry) => entry.url.includes("/api/ai-agent/run"));
|
||||
const hermesClientHit = aiRequests.some((entry) => entry.url.includes("/api/hermes/client/"));
|
||||
const providerHermes502 = captured.some(
|
||||
(entry) =>
|
||||
entry.phase === "response" &&
|
||||
entry.status === 502 &&
|
||||
/provider|ai_provider_bridge_unavailable|hermes/i.test(entry.body || ""),
|
||||
);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
title,
|
||||
documentId: target.documentId,
|
||||
workspaceId: target.workspaceId,
|
||||
firstRequestUrl: firstRequest.url,
|
||||
legacyRunHit,
|
||||
hermesClientHit,
|
||||
providerHermes502,
|
||||
captured,
|
||||
},
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
renameDocument,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function main() {
|
||||
const suffix = Date.now().toString(36);
|
||||
const title = `TEST-HERMES-AI-mobile-${suffix}`;
|
||||
const sessionId = `mnote_mobile_${suffix}`;
|
||||
const runId = `run_mobile_${suffix}`;
|
||||
const createdIds = [];
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 390, height: 844 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await page.route("**/api/ai-agent/run", async (route) => {
|
||||
throw new Error(`移动端页面 AI 不应请求旧 /api/ai-agent/run: ${route.request().url()}`);
|
||||
});
|
||||
await page.route("**/api/hermes/client/sessions", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId,
|
||||
title: "移动端问答",
|
||||
traceId: "trace_mobile",
|
||||
persistence: "hermes_on_first_run",
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/hermes/client/sessions/${sessionId}`, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId,
|
||||
traceId: "trace_mobile_restore",
|
||||
session: {
|
||||
sessionId,
|
||||
messages: [{ role: "assistant", content: "Mobile restored" }],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/hermes/client/runs", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId,
|
||||
runId,
|
||||
events: [],
|
||||
traceId: "trace_mobile",
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/hermes/client/events/${runId}`, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream; charset=utf-8" },
|
||||
body:
|
||||
`data: ${JSON.stringify({ event: "message.delta", run_id: runId, session_id: sessionId, delta: "Mobile " })}\n\n` +
|
||||
`data: ${JSON.stringify({ event: "run.completed", run_id: runId, session_id: sessionId, output: "Mobile response" })}\n\n`,
|
||||
});
|
||||
});
|
||||
|
||||
await ensureAuthenticated(page, context.request);
|
||||
const target = await createTempDocument(context.request);
|
||||
createdIds.push(target.documentId);
|
||||
await renameDocument(context.request, target.workspaceId, target.documentId, title);
|
||||
await page.goto(
|
||||
`${BASE_URL}/documents/${encodeURIComponent(target.documentId)}?workspaceId=${encodeURIComponent(target.workspaceId)}`,
|
||||
{ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
||||
const metrics = await page.locator('[data-testid="wolai-page-ai-drawer"]').evaluate((drawer) => {
|
||||
const panel = drawer.querySelector(".wolai-page-ai-panel");
|
||||
const drawerRect = drawer.getBoundingClientRect();
|
||||
const panelRect = panel ? panel.getBoundingClientRect() : drawerRect;
|
||||
return {
|
||||
viewportWidth: window.innerWidth,
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
drawerLeft: drawerRect.left,
|
||||
drawerRight: drawerRect.right,
|
||||
panelLeft: panelRect.left,
|
||||
panelRight: panelRect.right,
|
||||
};
|
||||
});
|
||||
assert(metrics.drawerLeft >= 0, `drawer 左侧溢出: ${JSON.stringify(metrics)}`);
|
||||
assert(metrics.drawerRight <= metrics.viewportWidth + 1, `drawer 右侧溢出: ${JSON.stringify(metrics)}`);
|
||||
assert(metrics.panelLeft >= 0, `panel 左侧溢出: ${JSON.stringify(metrics)}`);
|
||||
assert(metrics.panelRight <= metrics.viewportWidth + 1, `panel 右侧溢出: ${JSON.stringify(metrics)}`);
|
||||
assert(metrics.documentWidth <= metrics.viewportWidth + 1, `页面出现横向滚动: ${JSON.stringify(metrics)}`);
|
||||
|
||||
await page.locator("[data-page-ai-input]").fill(`请总结 ${title}`, { timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("Mobile response"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: target.documentId,
|
||||
workspaceId: target.workspaceId,
|
||||
sessionId,
|
||||
runId,
|
||||
metrics,
|
||||
},
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_TIMEOUT_MS || 20_000);
|
||||
|
||||
async function fetchWithTimeout(path, init = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
try {
|
||||
return await fetch(`${BASE_URL}${path}`, { ...init, signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const response = await fetchWithTimeout("/api/ai-agent/run", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
stream: true,
|
||||
messages: [{ role: "user", content: "ping" }],
|
||||
context: { documentId: "retirement_guard" },
|
||||
options: { ai: { provider: "hermes" } },
|
||||
}),
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch {
|
||||
payload = { raw: text };
|
||||
}
|
||||
|
||||
assert(
|
||||
response.status === 410,
|
||||
`/api/ai-agent/run legacy guard 应返回退场状态,实际 ${response.status}: ${text.slice(0, 500)}`,
|
||||
);
|
||||
const code = response.headers.get("x-error-code") || payload.code || "";
|
||||
const owner = response.headers.get("x-mnote-ai-execution-owner") || "";
|
||||
assert(
|
||||
code === "legacy_ai_agent_run_retired" && owner.includes("retired"),
|
||||
`legacy guard 不应静默 fallback,code=${code}, owner=${owner}, body=${text.slice(0, 500)}`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
status: response.status,
|
||||
code,
|
||||
owner,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
renameDocument,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function main() {
|
||||
const suffix = Date.now().toString(36);
|
||||
const title = `TEST-HERMES-AI-smoke-${suffix}`;
|
||||
const sessionId = `mnote_smoke_${suffix}`;
|
||||
const runId = `run_smoke_${suffix}`;
|
||||
let sessionDetailHits = 0;
|
||||
const captured = [];
|
||||
const createdIds = [];
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await page.route("**/api/ai-agent/run", async (route) => {
|
||||
throw new Error(`页面 AI 不应请求旧 /api/ai-agent/run: ${route.request().url()}`);
|
||||
});
|
||||
await page.route("**/api/hermes/client/sessions", async (route) => {
|
||||
captured.push({ kind: "session", method: route.request().method(), body: route.request().postData() || "" });
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId,
|
||||
title: "当前页问答",
|
||||
traceId: "trace_smoke",
|
||||
persistence: "hermes_on_first_run",
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/hermes/client/sessions/${sessionId}`, async (route) => {
|
||||
sessionDetailHits += 1;
|
||||
captured.push({ kind: "session-detail", method: route.request().method(), body: "" });
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId,
|
||||
traceId: "trace_restore",
|
||||
session: {
|
||||
sessionId,
|
||||
messages: [
|
||||
{ role: "user", content: `请总结 ${title}` },
|
||||
{ role: "tool", content: "mnote.page.get" },
|
||||
{ role: "assistant", content: "Smoke restored from Hermes session" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/hermes/client/runs", async (route) => {
|
||||
captured.push({ kind: "run", method: route.request().method(), body: route.request().postData() || "" });
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
sessionId,
|
||||
runId,
|
||||
events: [],
|
||||
traceId: "trace_smoke",
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/hermes/client/events/${runId}`, async (route) => {
|
||||
captured.push({ kind: "events", method: route.request().method(), body: "" });
|
||||
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, name: "mnote.page.get" })}\n\n` +
|
||||
`data: ${JSON.stringify({ event: "tool.completed", run_id: runId, session_id: sessionId, name: "mnote.page.get" })}\n\n` +
|
||||
`data: ${JSON.stringify({ event: "message.delta", run_id: runId, session_id: sessionId, delta: "Smoke " })}\n\n` +
|
||||
`data: ${JSON.stringify({ event: "message.delta", run_id: runId, session_id: sessionId, delta: "response" })}\n\n` +
|
||||
`data: ${JSON.stringify({ event: "run.completed", run_id: runId, session_id: sessionId, output: "Smoke response" })}\n\n`,
|
||||
});
|
||||
});
|
||||
|
||||
await ensureAuthenticated(page, context.request);
|
||||
const target = await createTempDocument(context.request);
|
||||
createdIds.push(target.documentId);
|
||||
await renameDocument(context.request, target.workspaceId, target.documentId, title);
|
||||
await page.goto(
|
||||
`${BASE_URL}/documents/${encodeURIComponent(target.documentId)}?workspaceId=${encodeURIComponent(target.workspaceId)}`,
|
||||
{ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator("[data-page-ai-input]").fill(`请总结 ${title}`, { timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("Smoke response"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("mnote.page.get"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
const sessionRequest = captured.find((entry) => entry.kind === "session");
|
||||
const runRequest = captured.find((entry) => entry.kind === "run");
|
||||
const eventRequest = captured.find((entry) => entry.kind === "events");
|
||||
assert(sessionRequest, "未捕获 /api/hermes/client/sessions 请求");
|
||||
assert(runRequest, "未捕获 /api/hermes/client/runs 请求");
|
||||
assert(eventRequest, "未捕获 /api/hermes/client/events 请求");
|
||||
const runBody = JSON.parse(runRequest.body);
|
||||
assert.equal(runBody.sessionId, sessionId, "run 请求必须携带 Hermes sessionId");
|
||||
assert.equal(runBody.documentId, target.documentId, "run 请求必须携带 documentId");
|
||||
assert(runBody.pageContext, "run 请求必须携带 pageContext");
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("Smoke restored from Hermes session"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("mnote.page.get"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
assert(sessionDetailHits > 0, "刷新后必须从 Hermes session detail 恢复消息,而不是从 mnote 本地消息数组恢复");
|
||||
const persisted = await page.evaluate(() => {
|
||||
const keys = Object.keys(window.localStorage).filter((key) => key.startsWith("hermes_page_ai_session:"));
|
||||
return keys.map((key) => ({ key, value: window.localStorage.getItem(key) }));
|
||||
});
|
||||
assert(
|
||||
persisted.every((entry) => !entry.value || !entry.value.includes("Smoke response")),
|
||||
"mnote localStorage 不应保存完整聊天消息内容",
|
||||
);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: target.documentId,
|
||||
workspaceId: target.workspaceId,
|
||||
sessionId,
|
||||
runId,
|
||||
capturedKinds: captured.map((entry) => entry.kind),
|
||||
sessionDetailHits,
|
||||
},
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
openDocument,
|
||||
openSectionView,
|
||||
renameDocument,
|
||||
requestJson,
|
||||
UI_TIMEOUT_MS,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function main() {
|
||||
const suffix = Date.now().toString(36);
|
||||
const originalTitle = `TEST-HERMES-AI-title-original-${suffix}`;
|
||||
const nextTitle = `TEST-HERMES-AI-title-updated-${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, originalTitle);
|
||||
|
||||
const common = {
|
||||
workspaceId: target.workspaceId,
|
||||
documentId: target.documentId,
|
||||
actorId: "smoke-user",
|
||||
sessionId: `mnote_smoke_title_${suffix}`,
|
||||
runId: `run_smoke_title_${suffix}`,
|
||||
traceId: `trace_smoke_title_${suffix}`,
|
||||
capabilityScope: ["page.write"],
|
||||
dryRun: false,
|
||||
};
|
||||
|
||||
const dryRunTitle = `TEST-HERMES-AI-title-dry-${suffix}`;
|
||||
const titleDryRun = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: {
|
||||
...common,
|
||||
dryRun: true,
|
||||
toolName: "mnote.page.update_title",
|
||||
toolCallId: `call_title_dry_${suffix}`,
|
||||
idempotencyKey: `idem_title_dry_${suffix}`,
|
||||
args: { title: dryRunTitle },
|
||||
},
|
||||
});
|
||||
assert.equal(titleDryRun.result.dryRun, true, "标题 dryRun 不应写入");
|
||||
const metaAfterDryRun = await requestJson(
|
||||
context.request,
|
||||
`/api/documents/meta?documentId=${encodeURIComponent(target.documentId)}&workspaceId=${encodeURIComponent(target.workspaceId)}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
assert(!JSON.stringify(metaAfterDryRun).includes(dryRunTitle), "标题 dryRun 后 meta 不应变化");
|
||||
|
||||
const titleResult = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: {
|
||||
...common,
|
||||
toolName: "mnote.page.update_title",
|
||||
toolCallId: `call_title_${suffix}`,
|
||||
idempotencyKey: `idem_title_${suffix}`,
|
||||
args: { title: nextTitle },
|
||||
},
|
||||
});
|
||||
assert.equal(titleResult.result.commandName, "page.head.updateTitle", "标题必须走 page.head.updateTitle");
|
||||
assert.equal(titleResult.audit.commandId, titleResult.result.commandId, "标题 audit commandId 必须指向 Rust commandId");
|
||||
const titleRetry = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: {
|
||||
...common,
|
||||
toolName: "mnote.page.update_title",
|
||||
toolCallId: `call_title_${suffix}`,
|
||||
idempotencyKey: `idem_title_${suffix}`,
|
||||
args: { title: nextTitle },
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
titleRetry.result.commandId,
|
||||
titleResult.result.commandId,
|
||||
"标题同一 idempotencyKey 重试必须返回同一个 commandId",
|
||||
);
|
||||
|
||||
const optionsResult = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: {
|
||||
...common,
|
||||
toolName: "mnote.page.update_options",
|
||||
toolCallId: `call_options_${suffix}`,
|
||||
idempotencyKey: `idem_options_${suffix}`,
|
||||
args: { options: { wideLayout: true, smallText: true, pageFont: "serif" } },
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
optionsResult.result.commandName,
|
||||
"page.layout.updateOptions",
|
||||
"页面设置必须走 page.layout.updateOptions",
|
||||
);
|
||||
assert.equal(
|
||||
optionsResult.audit.commandId,
|
||||
optionsResult.result.commandId,
|
||||
"页面设置 audit commandId 必须指向 Rust commandId",
|
||||
);
|
||||
assert(
|
||||
Array.isArray(optionsResult.result.ignoredOptions) &&
|
||||
optionsResult.result.ignoredOptions.includes("pageFont"),
|
||||
"planned/ui_only 页面设置字段必须明确返回 ignoredOptions",
|
||||
);
|
||||
assert(
|
||||
Array.isArray(optionsResult.result.warnings) &&
|
||||
optionsResult.result.warnings.some((warning) => warning && warning.code === "page_option_not_wired"),
|
||||
"planned/ui_only 页面设置字段必须明确返回 warning",
|
||||
);
|
||||
|
||||
const meta = await requestJson(
|
||||
context.request,
|
||||
`/api/documents/meta?documentId=${encodeURIComponent(target.documentId)}&workspaceId=${encodeURIComponent(target.workspaceId)}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
assert(JSON.stringify(meta).includes(nextTitle), "meta 未读到 AI 更新后的标题");
|
||||
assert(JSON.stringify(meta).includes("wide"), "meta 未读到页面设置更新结果");
|
||||
|
||||
await openDocument(page, target.workspaceId, target.documentId);
|
||||
await openSectionView(page);
|
||||
await page.waitForFunction(
|
||||
(title) => {
|
||||
const input = document.querySelector('[data-page-title-input="true"]');
|
||||
const current = document.querySelector('[data-page-title-current="true"]');
|
||||
return (input && input.value === title) || (current && current.textContent.includes(title));
|
||||
},
|
||||
nextTitle,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
({ documentId, title }) => {
|
||||
const selectors = [
|
||||
`#sidebar-tree-root .tree-row[data-node-id="${CSS.escape(documentId)}"] .tree-link-title`,
|
||||
`.tree-row[data-shell-mode="page"][data-node-id="${CSS.escape(documentId)}"] > .tree-link > .tree-link-title`,
|
||||
];
|
||||
return selectors.some((selector) => {
|
||||
const node = document.querySelector(selector);
|
||||
return node && (node.textContent || "").includes(title);
|
||||
});
|
||||
},
|
||||
{ documentId: target.documentId, title: nextTitle },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const uiState = await page.evaluate((documentId) => {
|
||||
const shell = document.querySelector(".document-shell");
|
||||
const sessionKey = document.documentElement.getAttribute("data-mnote-page-ai-session-key") || "";
|
||||
return {
|
||||
titleInput: document.querySelector('[data-page-title-input="true"]')?.value || "",
|
||||
currentTitle: document.querySelector('[data-page-title-current="true"]')?.textContent || "",
|
||||
sidebarTitle:
|
||||
document.querySelector(`#sidebar-tree-root .tree-row[data-node-id="${CSS.escape(documentId)}"] .tree-link-title`)
|
||||
?.textContent || "",
|
||||
wideLayout: shell?.getAttribute("data-page-wide-layout") || "",
|
||||
smallText: shell?.getAttribute("data-page-small-text") || "",
|
||||
pageAiSessionOwner: document.documentElement.getAttribute("data-mnote-page-ai-session-owner") || "",
|
||||
pageAiSessionKey: sessionKey,
|
||||
pageAiSessionStorage: sessionKey && window.localStorage ? window.localStorage.getItem(sessionKey) || "" : "",
|
||||
};
|
||||
}, target.documentId);
|
||||
assert(
|
||||
uiState.titleInput === nextTitle || uiState.currentTitle.includes(nextTitle),
|
||||
`刷新后页面 UI 未显示 AI 更新标题: ${JSON.stringify(uiState)}`,
|
||||
);
|
||||
assert(uiState.sidebarTitle.includes(nextTitle), `sidebar 未显示 AI 更新标题: ${JSON.stringify(uiState)}`);
|
||||
assert.equal(uiState.wideLayout, "true", "刷新后 document-shell 未应用 wideLayout");
|
||||
assert.equal(uiState.smallText, "true", "刷新后 document-shell 未应用 smallText");
|
||||
|
||||
await page.locator('[data-testid="wolai-floating-ai"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.documentElement.getAttribute("data-mnote-page-ai-session-owner") === "hermes" &&
|
||||
Boolean(document.documentElement.getAttribute("data-mnote-page-ai-session-key")),
|
||||
undefined,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const aiSessionState = await page.evaluate(() => {
|
||||
const key = document.documentElement.getAttribute("data-mnote-page-ai-session-key") || "";
|
||||
return {
|
||||
owner: document.documentElement.getAttribute("data-mnote-page-ai-session-owner") || "",
|
||||
key,
|
||||
stored: key && window.localStorage ? window.localStorage.getItem(key) || "" : "",
|
||||
pageTitle:
|
||||
document.querySelector('[data-page-title-input="true"]')?.value ||
|
||||
document.querySelector('[data-page-title-current="true"]')?.textContent ||
|
||||
"",
|
||||
};
|
||||
});
|
||||
assert.equal(aiSessionState.owner, "hermes", "页面 AI session owner 必须是 Hermes");
|
||||
const storedSessionState = JSON.parse(aiSessionState.stored || "{}");
|
||||
assert(storedSessionState.activeSessionId, "mnote 本地只应保存 Hermes activeSessionId");
|
||||
assert(!aiSessionState.stored.includes(nextTitle), "mnote 本地 AI session 状态不应保存或驱动页面标题真相");
|
||||
assert(aiSessionState.pageTitle.includes(nextTitle), "页面标题真相必须仍来自页面 UI / Page Aggregate");
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: target.documentId,
|
||||
workspaceId: target.workspaceId,
|
||||
title: nextTitle,
|
||||
titleCommand: titleResult.result.commandName,
|
||||
optionsCommand: optionsResult.result.commandName,
|
||||
ignoredOptions: optionsResult.result.ignoredOptions,
|
||||
warningCodes: optionsResult.result.warnings.map((warning) => warning.code),
|
||||
uiState,
|
||||
aiSessionState,
|
||||
},
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
renameDocument,
|
||||
requestJson,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function main() {
|
||||
const suffix = Date.now().toString(36);
|
||||
const title = `TEST-HERMES-AI-tool-${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 manifest = await requestJson(context.request, "/api/hermes/tools/mnote/manifest", {
|
||||
method: "GET",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
});
|
||||
const tools = manifest?.manifest?.tools || [];
|
||||
assert(tools.some((tool) => tool.name === "mnote.page.get"), "manifest 缺少 mnote.page.get");
|
||||
|
||||
const call = await requestJson(context.request, "/api/hermes/tools/mnote/call", {
|
||||
method: "POST",
|
||||
headers: { "x-mnote-actor-id": "smoke-user" },
|
||||
data: {
|
||||
toolName: "mnote.page.get",
|
||||
workspaceId: target.workspaceId,
|
||||
documentId: target.documentId,
|
||||
actorId: "smoke-user",
|
||||
sessionId: `mnote_smoke_tool_${suffix}`,
|
||||
runId: `run_smoke_tool_${suffix}`,
|
||||
toolCallId: `call_smoke_tool_${suffix}`,
|
||||
traceId: `trace_smoke_tool_${suffix}`,
|
||||
capabilityScope: ["page.read"],
|
||||
args: {
|
||||
includeBody: true,
|
||||
includeOptions: true,
|
||||
includeBlocks: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(call.ok, true, "tool call 应成功");
|
||||
assert.equal(call.toolName, "mnote.page.get", "toolName 不一致");
|
||||
assert.equal(call.toolCallId, `call_smoke_tool_${suffix}`, "toolCallId 不一致");
|
||||
assert.equal(call.result.documentId, target.documentId, "工具结果 documentId 不一致");
|
||||
assert.equal(call.result.workspaceId, target.workspaceId, "工具结果 workspaceId 不一致");
|
||||
assert(call.result.title, "工具结果缺少标题");
|
||||
assert(call.audit && call.audit.effect === "read", "工具结果缺少 read audit");
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: target.documentId,
|
||||
workspaceId: target.workspaceId,
|
||||
toolName: call.toolName,
|
||||
title: call.result.title,
|
||||
audit: call.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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -1,5 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
if (process.env.MNOTE_ALLOW_RETIRED_AI_AGENT_RUN_SMOKE !== "1") {
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
retired: true,
|
||||
script: "task052-ai-tools-runtime-smoke.js",
|
||||
reason: "旧 /api/ai-agent/run AI tools runtime smoke 已退役;当前请运行 scripts/task-hermes-page-ai-retirement-guard.js 验证 legacy guard。",
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
if (process.env.MNOTE_ALLOW_RETIRED_AI_AGENT_RUN_SMOKE !== "1") {
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
retired: true,
|
||||
script: "task111-phase7-document-ai-online-smoke.js",
|
||||
reason: "旧 /api/ai-agent/run phase7 online smoke 已退役;当前请运行 scripts/task-hermes-page-ai-retirement-guard.js 验证 legacy guard。",
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
if (process.env.MNOTE_ALLOW_RETIRED_AI_AGENT_RUN_SMOKE !== "1") {
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
retired: true,
|
||||
script: "task155-e27-ai-edit-smoke.js",
|
||||
reason: "旧 /api/ai-agent/run E27 AI 编辑 smoke 已退役;当前请运行 scripts/task-hermes-page-ai-retirement-guard.js 验证 legacy guard。",
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const assert = require("node:assert");
|
||||
const fs = require("node:fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
if (process.env.MNOTE_ALLOW_RETIRED_AI_AGENT_RUN_SMOKE !== "1") {
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
retired: true,
|
||||
script: "task156-e27-ai-writeback-smoke.js",
|
||||
reason: "旧 /api/ai-agent/run E27 AI 写回 smoke 已退役;当前请运行 scripts/task-hermes-page-ai-retirement-guard.js 验证 legacy guard。",
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const assert = require("node:assert");
|
||||
const fs = require("node:fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
if (process.env.MNOTE_ALLOW_RETIRED_AI_AGENT_RUN_SMOKE !== "1") {
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
retired: true,
|
||||
script: "task178-page-ai-local-subtree-context-smoke.js",
|
||||
reason: "旧 /api/ai-agent/run 页面 AI context smoke 已退役;当前请运行 scripts/task-hermes-page-ai-retirement-guard.js 验证 legacy guard。",
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const { fetchWithTimeout } = require("./task114-rust-web-gateway-entry-smoke.js");
|
||||
|
||||
Reference in New Issue
Block a user