179 lines
7.1 KiB
JavaScript
179 lines
7.1 KiB
JavaScript
#!/usr/bin/env node
|
||
// Pi Lab mock-mode API smoke
|
||
// 需要以 MNOTE_PAGE_AI_PI_LAB_RUNTIME=mock 启动 mnote-web;Pi Lab 默认开启。
|
||
// 验证 start/send/abort/events、allowed roots、越界拒绝、当前页 read、文件 patch、receipt。
|
||
|
||
"use strict";
|
||
|
||
const fs = require("node:fs");
|
||
const os = require("node:os");
|
||
const path = require("node:path");
|
||
|
||
const BASE = process.env.MNOTE_PI_LAB_BASE || "http://127.0.0.1:3000";
|
||
const AUTH = process.env.MNOTE_PI_LAB_AUTH || "Bearer pi-lab-smoke";
|
||
const ROOT = process.env.MNOTE_PI_LAB_SMOKE_ROOT || fs.mkdtempSync(path.join(os.tmpdir(), "mnote-pi-lab-"));
|
||
|
||
function assert(condition, message) {
|
||
if (!condition) throw new Error(message);
|
||
}
|
||
|
||
async function fetchJson(url, options = {}) {
|
||
const headers = {
|
||
"Content-Type": "application/json",
|
||
Authorization: AUTH,
|
||
...(options.headers || {}),
|
||
};
|
||
const res = await fetch(url, { ...options, headers });
|
||
const text = await res.text();
|
||
let body = {};
|
||
try {
|
||
body = text ? JSON.parse(text) : {};
|
||
} catch {
|
||
body = { raw: text };
|
||
}
|
||
return { status: res.status, body, headers: res.headers };
|
||
}
|
||
|
||
async function main() {
|
||
fs.mkdirSync(ROOT, { recursive: true });
|
||
const pagePath = "page.md";
|
||
const pageFile = path.join(ROOT, pagePath);
|
||
fs.writeFileSync(pageFile, "# Pi Lab smoke\n\nOriginal body\n", "utf8");
|
||
const rootUri = `file://${ROOT}`;
|
||
|
||
console.log(`\n🧪 Pi Lab mock API smoke (base: ${BASE}, root: ${ROOT})\n`);
|
||
|
||
const status = await fetchJson(`${BASE}/api/page-ai/pi/status`);
|
||
assert(status.status === 200, `status endpoint returned ${status.status}`);
|
||
assert(status.body.schema === "mnote.page_ai_pi.status.v1", "status schema mismatch");
|
||
assert(status.body.enabled === true, "Pi Lab must be enabled for mock smoke");
|
||
assert(status.body.uiMode === "independent_mnote_native_drawer", "Pi Lab should report independent native drawer UI mode");
|
||
assert(status.body.runtimeMode === "mock", `runtimeMode must be mock, got ${status.body.runtimeMode}`);
|
||
console.log(" ✅ status enabled/mock");
|
||
|
||
const start = await fetchJson(`${BASE}/api/page-ai/pi/start`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
rootUri,
|
||
workspaceId: "pi-lab-smoke",
|
||
pagePath,
|
||
pageTitle: "Pi Lab smoke",
|
||
}),
|
||
});
|
||
assert(start.status === 200, `start returned ${start.status}: ${JSON.stringify(start.body)}`);
|
||
assert(start.body.schema === "mnote.page_ai_pi.start.v1", "start schema mismatch");
|
||
const sessionId = start.body.session && start.body.session.sessionId;
|
||
assert(sessionId, "start did not return sessionId");
|
||
assert(start.body.session.allowedRootsSnapshot, "start did not capture allowed roots snapshot");
|
||
console.log(` ✅ start session ${sessionId}`);
|
||
|
||
const events = await fetch(`${BASE}/api/page-ai/pi/events?sessionId=${encodeURIComponent(sessionId)}`, {
|
||
headers: { Authorization: AUTH, Accept: "text/event-stream" },
|
||
});
|
||
assert(events.status === 200, `events returned ${events.status}`);
|
||
assert((events.headers.get("content-type") || "").includes("text/event-stream"), "events is not SSE");
|
||
if (events.body && events.body.cancel) await events.body.cancel();
|
||
console.log(" ✅ events SSE");
|
||
|
||
const allowed = await fetchJson(`${BASE}/api/page-ai/pi/tool-call`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
sessionId,
|
||
toolName: "mnote.allowed_roots.describe",
|
||
params: {},
|
||
}),
|
||
});
|
||
assert(allowed.status === 200, `allowed roots returned ${allowed.status}`);
|
||
assert(allowed.body.ok === true, "allowed roots tool should be allowed");
|
||
assert(Array.isArray(allowed.body.result.allowedRoots), "allowed roots missing");
|
||
console.log(" ✅ allowed roots describe");
|
||
|
||
const currentPage = await fetchJson(`${BASE}/api/page-ai/pi/tool-call`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
sessionId,
|
||
toolName: "mnote.current_page.read",
|
||
params: { rootUri, pagePath },
|
||
}),
|
||
});
|
||
assert(currentPage.status === 200 && currentPage.body.ok === true, "current page read failed");
|
||
assert(String(currentPage.body.result.content).includes("Original body"), "current page content mismatch");
|
||
console.log(" ✅ current page read");
|
||
|
||
const denied = await fetchJson(`${BASE}/api/page-ai/pi/tool-call`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
sessionId,
|
||
toolName: "mnote.local_file.read",
|
||
params: { path: "/etc/hosts" },
|
||
}),
|
||
});
|
||
assert(denied.status === 200, `deny read returned ${denied.status}`);
|
||
assert(denied.body.ok === false, "out-of-root read should be denied");
|
||
assert(denied.body.receipt, "denied read should still write receipt");
|
||
console.log(" ✅ out-of-root read denied with receipt");
|
||
|
||
const patch = await fetchJson(`${BASE}/api/page-ai/pi/tool-call`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
sessionId,
|
||
toolName: "mnote.local_file.patch",
|
||
params: {
|
||
rootUri,
|
||
path: pagePath,
|
||
operations: [{ op: "replace", old: "Original body", new: "Patched body" }],
|
||
},
|
||
}),
|
||
});
|
||
assert(patch.status === 200 && patch.body.ok === true, "local file patch failed");
|
||
assert(patch.body.result.polling === false, "patch must not request polling");
|
||
assert(String(patch.body.result.refresh || "").includes("watcher"), "patch must declare watcher refresh");
|
||
assert(fs.readFileSync(pageFile, "utf8").includes("Patched body"), "patched file content mismatch");
|
||
console.log(" ✅ local file patch + watcher refresh metadata");
|
||
|
||
const rag = await fetchJson(`${BASE}/api/page-ai/pi/tool-call`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
sessionId,
|
||
toolName: "mnote.knowledge_rag.query",
|
||
params: { rootUri, query: "Pi Lab smoke", topK: 1 },
|
||
}),
|
||
});
|
||
assert(rag.status === 200, `knowledge RAG facade returned HTTP ${rag.status}`);
|
||
assert(typeof rag.body.ok === "boolean", "knowledge RAG facade did not return tool envelope");
|
||
console.log(` ✅ LightRAG facade exercised (${rag.body.ok ? "ok" : "provider unavailable/denied"})`);
|
||
|
||
const reference = await fetchJson(`${BASE}/api/page-ai/pi/tool-call`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
sessionId,
|
||
toolName: "mnote.reference.open",
|
||
params: { rootUri, filePath: pagePath },
|
||
}),
|
||
});
|
||
assert(reference.status === 200, `reference.open facade returned HTTP ${reference.status}`);
|
||
assert(typeof reference.body.ok === "boolean", "reference.open facade did not return tool envelope");
|
||
console.log(` ✅ citation/open-reference facade exercised (${reference.body.ok ? "ok" : "provider unavailable/denied"})`);
|
||
|
||
const send = await fetchJson(`${BASE}/api/page-ai/pi/send`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ sessionId, message: "hello from smoke" }),
|
||
});
|
||
assert(send.status === 200 && send.body.accepted === true, "send did not accept prompt");
|
||
console.log(" ✅ send prompt");
|
||
|
||
const abort = await fetchJson(`${BASE}/api/page-ai/pi/abort`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ sessionId }),
|
||
});
|
||
assert(abort.status === 200 && abort.body.aborted === true, "abort failed");
|
||
console.log(" ✅ abort");
|
||
|
||
console.log("\n✅ Pi Lab mock API smoke passed.\n");
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error(`\n❌ Pi Lab mock API smoke failed: ${error.message}`);
|
||
process.exit(1);
|
||
});
|