303 lines
9.6 KiB
JavaScript
303 lines
9.6 KiB
JavaScript
"use strict";
|
|
|
|
const { chromium } = require("playwright");
|
|
|
|
const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
|
|
const REQUEST_TIMEOUT_MS = 120_000;
|
|
const UI_TIMEOUT_MS = 30_000;
|
|
const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录";
|
|
|
|
async function requestJsonWithCookieHeader(path, init = {}, cookieHeader = "") {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
try {
|
|
const response = await fetch(`${BASE_URL}${path}`, {
|
|
...init,
|
|
headers: {
|
|
...(init.data !== undefined ? { "content-type": "application/json" } : {}),
|
|
...(init.headers || {}),
|
|
...(cookieHeader ? { cookie: cookieHeader } : {}),
|
|
},
|
|
body: init.data !== undefined ? JSON.stringify(init.data) : init.body,
|
|
signal: controller.signal,
|
|
});
|
|
|
|
const text = await response.text();
|
|
let payload = null;
|
|
try {
|
|
payload = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
payload = text;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`${path} 请求失败: ${response.status} ${response.statusText} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`,
|
|
);
|
|
}
|
|
|
|
const contentType = response.headers.get("content-type") || "";
|
|
if (!contentType.includes("application/json")) {
|
|
throw new Error(`${path} 返回了非 JSON 内容:${String(text).slice(0, 200)}`);
|
|
}
|
|
|
|
return payload;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
async function requestJson(requestContext, path, init = {}) {
|
|
const response = await requestContext.fetch(`${BASE_URL}${path}`, {
|
|
...init,
|
|
headers:
|
|
init.data !== undefined
|
|
? {
|
|
"content-type": "application/json",
|
|
...(init.headers || {}),
|
|
}
|
|
: {
|
|
...(init.headers || {}),
|
|
},
|
|
timeout: REQUEST_TIMEOUT_MS,
|
|
});
|
|
|
|
const text = await response.text();
|
|
let payload = null;
|
|
try {
|
|
payload = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
payload = text;
|
|
}
|
|
|
|
if (!response.ok()) {
|
|
throw new Error(
|
|
`${path} 请求失败: ${response.status()} ${response.statusText()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`,
|
|
);
|
|
}
|
|
|
|
const contentType = response.headers()["content-type"] || "";
|
|
if (!contentType.includes("application/json")) {
|
|
throw new Error(`${path} 返回了非 JSON 内容:${String(text).slice(0, 200)}`);
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
async function ensureAuthenticated(page, requestContext) {
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const tryWhoAmI = async () => {
|
|
try {
|
|
return await requestJson(requestContext, "/api/auth/whoami", { method: "GET" });
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
let viewer = await tryWhoAmI();
|
|
if (viewer) return viewer;
|
|
|
|
const quickLoginButton = page.getByRole("button", { name: TEST_LOGIN_BUTTON_NAME });
|
|
await quickLoginButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
|
|
|
|
for (let i = 0; i < 20; i += 1) {
|
|
await sleep(500);
|
|
viewer = await tryWhoAmI();
|
|
if (viewer) return viewer;
|
|
}
|
|
|
|
throw new Error("测试账号快速登录后仍无法获取 whoami");
|
|
}
|
|
|
|
async function createTempDocument(requestContext) {
|
|
const payload = await requestJson(requestContext, "/api/documents/create", {
|
|
method: "POST",
|
|
data: { parentId: null },
|
|
});
|
|
assert(payload && typeof payload.id === "string", "创建临时页面失败:缺少 id");
|
|
assert(payload && typeof payload.workspace_id === "string", "创建临时页面失败:缺少 workspace_id");
|
|
return { documentId: payload.id, workspaceId: payload.workspace_id };
|
|
}
|
|
|
|
async function renameDocument(requestContext, documentId, workspaceId, title) {
|
|
return await requestJson(requestContext, "/api/documents/title", {
|
|
method: "POST",
|
|
data: { documentId, workspaceId, title, commandName: "page.head.updateTitle" },
|
|
});
|
|
}
|
|
|
|
async function saveDocument(requestContext, documentId, workspaceId, content) {
|
|
return await requestJson(requestContext, "/api/documents/save", {
|
|
method: "POST",
|
|
data: { documentId, workspaceId, content },
|
|
});
|
|
}
|
|
|
|
async function purgeTempDocument(requestContext, documentId) {
|
|
return await requestJson(requestContext, "/api/documents/purge", {
|
|
method: "POST",
|
|
data: { documentId },
|
|
});
|
|
}
|
|
|
|
async function runAiAgentDocsSmoke(requestContext, uniqueTitle, needleText, cookieHeader) {
|
|
const payload = await requestJsonWithCookieHeader("/api/ai-agent/run", {
|
|
method: "POST",
|
|
data: {
|
|
stream: false,
|
|
maxSteps: 4,
|
|
scope: "document",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: `<docs_search>{"query":"${uniqueTitle}","limit":5}</docs_search>`,
|
|
},
|
|
],
|
|
toolChoice: {
|
|
mode: "manual",
|
|
toolSets: ["toolset.docs_read"],
|
|
tools: ["docs_search", "docs_read"],
|
|
},
|
|
context: {
|
|
documentId: "smoke-doc-context",
|
|
documentBlocks: [],
|
|
},
|
|
options: {
|
|
ai: {
|
|
provider: "codex",
|
|
sessionId: "",
|
|
},
|
|
},
|
|
},
|
|
}, cookieHeader);
|
|
|
|
assert(Array.isArray(payload.events), "AI Agent 返回缺少 events");
|
|
const toolResults = payload.events.filter((event) => event && event.type === "tool_result");
|
|
assert(toolResults.length >= 1, "AI Agent 未返回任何 tool_result");
|
|
|
|
const searchResultEvent = toolResults.find((event) => event.data && event.data.tool === "docs_search" && event.data.ok === true);
|
|
assert(searchResultEvent, "docs_search 未成功执行");
|
|
const searchResults = searchResultEvent.data.result && Array.isArray(searchResultEvent.data.result.results)
|
|
? searchResultEvent.data.result.results
|
|
: [];
|
|
assert(searchResults.length >= 1, "docs_search 没有返回结果");
|
|
const top = searchResults[0];
|
|
assert(typeof top.id === "string" && top.id, "docs_search 首条结果缺少 documentId");
|
|
|
|
const readPayload = await requestJsonWithCookieHeader("/api/ai-agent/run", {
|
|
method: "POST",
|
|
data: {
|
|
stream: false,
|
|
maxSteps: 4,
|
|
scope: "document",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: `<docs_read>{"documentId":"${top.id}","maxChars":2500,"includeContent":false}</docs_read>`,
|
|
},
|
|
],
|
|
toolChoice: {
|
|
mode: "manual",
|
|
toolSets: ["toolset.docs_read"],
|
|
tools: ["docs_read"],
|
|
},
|
|
context: {
|
|
documentId: "smoke-doc-context",
|
|
documentBlocks: [],
|
|
},
|
|
options: {
|
|
ai: {
|
|
provider: "codex",
|
|
sessionId: "",
|
|
},
|
|
},
|
|
},
|
|
}, cookieHeader);
|
|
|
|
assert(Array.isArray(readPayload.events), "docs_read 返回缺少 events");
|
|
const readEvent = readPayload.events.find((event) => event && event.type === "tool_result" && event.data && event.data.tool === "docs_read" && event.data.ok === true);
|
|
assert(readEvent, "docs_read 未成功执行");
|
|
const rawText = String(readEvent.data.result?.rawText ?? "");
|
|
assert(rawText.includes(needleText), `docs_read 返回未命中预期正文片段:${needleText}`);
|
|
|
|
return {
|
|
searchDocumentId: top.id,
|
|
searchResultsCount: searchResults.length,
|
|
readRawTextLength: Number(readEvent.data.result?.rawTextLength ?? 0),
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
|
|
const page = await context.newPage();
|
|
let tempDocument = null;
|
|
let caughtError = null;
|
|
|
|
try {
|
|
await ensureAuthenticated(page, context.request);
|
|
tempDocument = await createTempDocument(context.request);
|
|
const uniqueSuffix = `${Date.now()}`;
|
|
const uniqueTitle = `task052-ai-runtime-${uniqueSuffix}`;
|
|
const needleText = `task052 Rust docs runtime smoke ${uniqueSuffix}`;
|
|
|
|
await renameDocument(context.request, tempDocument.documentId, tempDocument.workspaceId, uniqueTitle);
|
|
await saveDocument(context.request, tempDocument.documentId, tempDocument.workspaceId, [
|
|
{
|
|
id: `task052_block_${uniqueSuffix}`,
|
|
type: "paragraph",
|
|
props: {},
|
|
content: [{ type: "text", text: needleText }],
|
|
children: [],
|
|
},
|
|
]);
|
|
|
|
const cookies = await context.cookies(BASE_URL);
|
|
const cookieHeader = cookies.map((item) => `${item.name}=${item.value}`).join("; ");
|
|
const runtime = await runAiAgentDocsSmoke(context.request, uniqueTitle, needleText, cookieHeader);
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
workspaceId: tempDocument.workspaceId,
|
|
documentId: tempDocument.documentId,
|
|
title: uniqueTitle,
|
|
needleText,
|
|
...runtime,
|
|
}, null, 2));
|
|
} catch (error) {
|
|
caughtError = error;
|
|
} finally {
|
|
if (tempDocument?.documentId) {
|
|
try {
|
|
await purgeTempDocument(context.request, tempDocument.documentId);
|
|
} catch (cleanupError) {
|
|
if (!caughtError) {
|
|
caughtError = cleanupError;
|
|
} else {
|
|
console.error(`清理临时页面失败:${cleanupError instanceof Error ? cleanupError.stack || cleanupError.message : String(cleanupError)}`);
|
|
}
|
|
}
|
|
}
|
|
await page.close().catch(() => undefined);
|
|
await context.close().catch(() => undefined);
|
|
await browser.close().catch(() => undefined);
|
|
}
|
|
|
|
if (caughtError) {
|
|
throw caughtError;
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
process.exit(1);
|
|
});
|