feat: add pi lab ai management integration
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user