192 lines
6.5 KiB
JavaScript
192 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|||
|
|
// S5: RAG status health + summary shape + FileTree tooltip static contract.
|
||
|
|
// Requires mnote-web with MNOTE_WEB_ALLOW_DEV_FIXTURES=1 (LightRAG may be down — status still ok:true).
|
||
|
|
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const fs = require("node:fs");
|
||
|
|
const os = require("node:os");
|
||
|
|
const path = require("node:path");
|
||
|
|
|
||
|
|
const BASE = process.env.MNOTE_S5_BASE || process.env.MNOTE_PI_LAB_BASE || "http://127.0.0.1:3017";
|
||
|
|
const ACTOR_ID = process.env.MNOTE_S5_ACTOR_ID || "s5-rag-admin";
|
||
|
|
const AUTH = process.env.MNOTE_S5_AUTH || "Bearer s5-rag";
|
||
|
|
const ROOT =
|
||
|
|
process.env.MNOTE_S5_ROOT ||
|
||
|
|
fs.mkdtempSync(path.join(os.tmpdir(), "mnote-s5-rag-"));
|
||
|
|
|
||
|
|
function assert(condition, message) {
|
||
|
|
if (!condition) throw new Error(message);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchJson(url, options = {}) {
|
||
|
|
const headers = {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
Authorization: AUTH,
|
||
|
|
"x-mnote-actor-id": ACTOR_ID,
|
||
|
|
"x-mnote-actor-type": "admin",
|
||
|
|
...(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 };
|
||
|
|
}
|
||
|
|
|
||
|
|
async function seedWorkspace(rootUri, rootPath) {
|
||
|
|
const seed = await fetchJson(`${BASE}/api/dev/seed`, {
|
||
|
|
method: "POST",
|
||
|
|
body: JSON.stringify({
|
||
|
|
seeds: [
|
||
|
|
{
|
||
|
|
kind: "setupWorkspace",
|
||
|
|
user_id: ACTOR_ID,
|
||
|
|
email: `${ACTOR_ID}@example.com`,
|
||
|
|
username: ACTOR_ID,
|
||
|
|
display_name: ACTOR_ID,
|
||
|
|
role: "admin",
|
||
|
|
password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!",
|
||
|
|
workspace_id: `local-ws:${ACTOR_ID}:s5`,
|
||
|
|
workspace_name: "S5 RAG",
|
||
|
|
root_uri: rootUri,
|
||
|
|
root_path: rootPath,
|
||
|
|
source_kind: "local_folder",
|
||
|
|
permission: "write",
|
||
|
|
capabilities: ["ai"],
|
||
|
|
grant_source: "s5_rag_smoke",
|
||
|
|
grant_created_by: ACTOR_ID,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
if (seed.status === 403 && seed.body && seed.body.code === "dev_seed_disabled") {
|
||
|
|
throw new Error(
|
||
|
|
"S5 smoke 需要 /api/dev/seed;请以 MNOTE_WEB_ALLOW_DEV_FIXTURES=1 启动 mnote-web。",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
assert(
|
||
|
|
seed.status === 200 && seed.body.ok === true,
|
||
|
|
`/api/dev/seed failed: ${seed.status} ${JSON.stringify(seed.body)}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function staticChecks() {
|
||
|
|
const liveApply = path.join(
|
||
|
|
__dirname,
|
||
|
|
"..",
|
||
|
|
"rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js",
|
||
|
|
);
|
||
|
|
const aiAdmin = path.join(
|
||
|
|
__dirname,
|
||
|
|
"..",
|
||
|
|
"rust/crates/mnote-web/src/ssr/pages/ai_admin.rs",
|
||
|
|
);
|
||
|
|
const liveSrc = fs.readFileSync(liveApply, "utf8");
|
||
|
|
const adminSrc = fs.readFileSync(aiAdmin, "utf8");
|
||
|
|
|
||
|
|
assert(liveSrc.includes("indexStatusLabel"), "FileTree must compute indexStatusLabel");
|
||
|
|
assert(
|
||
|
|
liveSrc.includes("已索引") && liveSrc.includes("索引中") && liveSrc.includes("索引失败"),
|
||
|
|
"FileTree tooltip labels for indexed/indexing/failed",
|
||
|
|
);
|
||
|
|
assert(
|
||
|
|
liveSrc.includes("linkTitle") &&
|
||
|
|
(liveSrc.includes("title=\"") || liveSrc.includes("title='") || liveSrc.includes("title=\"") || liveSrc.includes("title=") || liveSrc.includes("title=")),
|
||
|
|
"FileTree rows should set title/linkTitle for tooltip",
|
||
|
|
);
|
||
|
|
// Prefer title attribute wiring for tooltip.
|
||
|
|
assert(
|
||
|
|
/linkTitle[\s\S]{0,400}title=/.test(liveSrc) ||
|
||
|
|
/title=[\s\S]{0,200}linkTitle/.test(liveSrc) ||
|
||
|
|
liveSrc.includes("escapeHtml(linkTitle)"),
|
||
|
|
"linkTitle must be wired into element title attribute",
|
||
|
|
);
|
||
|
|
|
||
|
|
assert(
|
||
|
|
adminSrc.includes("/api/knowledge-rag/status"),
|
||
|
|
"AI admin knowledge panel must fetch /api/knowledge-rag/status",
|
||
|
|
);
|
||
|
|
assert(
|
||
|
|
adminSrc.includes("data-ai-admin-knowledge-summary") ||
|
||
|
|
adminSrc.includes("knowledge-summary"),
|
||
|
|
"AI admin must render knowledge summary region",
|
||
|
|
);
|
||
|
|
console.log(" [static] FileTree tooltip + admin knowledge summary OK");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
staticChecks();
|
||
|
|
|
||
|
|
fs.mkdirSync(ROOT, { recursive: true });
|
||
|
|
fs.writeFileSync(path.join(ROOT, "s5.md"), "# S5 RAG\n", "utf8");
|
||
|
|
const rootUri = `file://${ROOT}`;
|
||
|
|
const workspaceId = `local-ws:${ACTOR_ID}:s5`;
|
||
|
|
|
||
|
|
console.log(`\n🧪 S5 RAG health + summary (base=${BASE})\n`);
|
||
|
|
|
||
|
|
await seedWorkspace(rootUri, ROOT);
|
||
|
|
|
||
|
|
const bare = await fetchJson(`${BASE}/api/knowledge-rag/status`);
|
||
|
|
assert(bare.status === 200 && bare.body.ok === true, `status bare: ${bare.status} ${JSON.stringify(bare.body).slice(0, 300)}`);
|
||
|
|
assert(
|
||
|
|
String(bare.body.schema) === "mnote.knowledge_rag.provider_status.v1",
|
||
|
|
`schema: ${bare.body.schema}`,
|
||
|
|
);
|
||
|
|
assert(bare.body.health !== undefined, "status must include health");
|
||
|
|
assert(bare.body.pipeline !== undefined || bare.body.documents !== undefined, "status must include pipeline or documents");
|
||
|
|
console.log(
|
||
|
|
` bare: provider=${bare.body.provider} health.ok=${bare.body.health && bare.body.health.ok}`,
|
||
|
|
);
|
||
|
|
|
||
|
|
const scoped = await fetchJson(
|
||
|
|
`${BASE}/api/knowledge-rag/status?workspaceId=${encodeURIComponent(workspaceId)}&rootUri=${encodeURIComponent(rootUri)}`,
|
||
|
|
);
|
||
|
|
assert(
|
||
|
|
scoped.status === 200 && scoped.body.ok === true,
|
||
|
|
`status scoped: ${scoped.status} ${JSON.stringify(scoped.body).slice(0, 300)}`,
|
||
|
|
);
|
||
|
|
// Scoped should attach registry when root is readable.
|
||
|
|
assert(
|
||
|
|
scoped.body.registry !== undefined || scoped.body.registryDiagnostics !== undefined,
|
||
|
|
"scoped status should include registry or diagnostics",
|
||
|
|
);
|
||
|
|
console.log(
|
||
|
|
` scoped: registry=${scoped.body.registry ? "yes" : "no"} knowledgeBases=${scoped.body.knowledgeBases ? "yes" : "no"}`,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Admin UI page should mention knowledge status (SSR shell).
|
||
|
|
const adminPage = await fetch(`${BASE}/ai-admin`, {
|
||
|
|
headers: {
|
||
|
|
Authorization: AUTH,
|
||
|
|
"x-mnote-actor-id": ACTOR_ID,
|
||
|
|
"x-mnote-actor-type": "admin",
|
||
|
|
},
|
||
|
|
redirect: "manual",
|
||
|
|
});
|
||
|
|
// May 200 or redirect depending on auth shell — only soft-check when 200.
|
||
|
|
if (adminPage.status === 200) {
|
||
|
|
const html = await adminPage.text();
|
||
|
|
assert(
|
||
|
|
html.includes("knowledge-rag") ||
|
||
|
|
html.includes("data-ai-admin-knowledge") ||
|
||
|
|
html.includes("知识"),
|
||
|
|
"ai-admin HTML should include knowledge panel markers",
|
||
|
|
);
|
||
|
|
console.log(" ai-admin HTML knowledge panel markers present");
|
||
|
|
} else {
|
||
|
|
console.log(` ai-admin HTTP ${adminPage.status} (skip HTML; API shape verified)`);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log("\n✅ S5 RAG health + summary + FileTree smoke passed\n");
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((err) => {
|
||
|
|
console.error("\n❌ S5 smoke failed:", err.message || err);
|
||
|
|
process.exit(1);
|
||
|
|
});
|