Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
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);
|
|
});
|