Files
mnote/scripts/task-pi-lab-mock-api-smoke.js
T

179 lines
7.1 KiB
JavaScript
Raw Normal View History

2026-07-04 21:47:42 +08:00
#!/usr/bin/env node
// Pi Lab mock-mode API smoke
// 需要以 MNOTE_PAGE_AI_PI_LAB_RUNTIME=mock 启动 mnote-webPi 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);
});