Lumi/plugins/okf/tests/verify.js
2026-06-25 14:10:04 +02:00

415 lines
18 KiB
JavaScript

const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const Database = require("better-sqlite3");
const { PRESERVED_PATHS } = require("../../../src/services/update-repository");
const {
accessForUser,
createEntry,
ensureTables,
getEntryBySlug,
grantPermission,
listEntries,
listPermissions,
listVersions,
revokePermission,
restoreVersion,
searchForAi,
setEntryWorkflow,
updateEntry
} = require("../backend/okf_store");
const {
ensureKnowledgeDirs,
getCommunityKnowledgeFile,
listCommunityKnowledgeFiles,
loadKnowledgeEntries,
listKnowledgePlaceholders,
saveCorrectionKnowledgeFile,
saveCommunityKnowledgeFile,
migrateSingleBracePlaceholders,
searchFileKnowledge
} = require("../backend/file_knowledge");
const { generateKnowledgeFiles } = require("../backend/generate_knowledge");
const { renderMarkdown } = require("../backend/markdown");
const db = new Database(":memory:");
db.exec(`
CREATE TABLE user_profiles (
id TEXT PRIMARY KEY,
internal_username TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`);
ensureTables(db);
const now = Date.now();
db.prepare("INSERT INTO user_profiles (id, internal_username, created_at, updated_at) VALUES (?, ?, ?, ?)").run("admin-1", "Admin", now, now);
db.prepare("INSERT INTO user_profiles (id, internal_username, created_at, updated_at) VALUES (?, ?, ?, ?)").run("mod-1", "Mod", now, now);
db.prepare("INSERT INTO user_profiles (id, internal_username, created_at, updated_at) VALUES (?, ?, ?, ?)").run("user-1", "User", now, now);
db.prepare("INSERT INTO user_profiles (id, internal_username, created_at, updated_at) VALUES (?, ?, ?, ?)").run("editor-1", "Editor", now, now);
const admin = { id: "admin-1", isAdmin: true, isMod: true };
const mod = { id: "mod-1", isMod: true };
const user = { id: "user-1" };
const editor = { id: "editor-1" };
assert(PRESERVED_PATHS.includes("knowledge/community"));
assert(PRESERVED_PATHS.includes("knowledge/corrections"));
const adminTemplate = fs.readFileSync(path.join(__dirname, "..", "views", "admin.ejs"), "utf8");
assert(adminTemplate.includes("General OKF"));
assert(adminTemplate.includes("Community OKF"));
assert(adminTemplate.includes("System-generated OKF"));
assert(adminTemplate.includes("tab=general"));
assert(adminTemplate.includes("tab=community"));
assert(adminTemplate.includes("tab=system"));
assert(adminTemplate.includes("selectedSystemFile"));
assert(adminTemplate.includes("System-generated OKF") && !adminTemplate.includes("Save system file"));
assert(adminTemplate.includes("data-okf-file-list"));
assert(adminTemplate.includes("data-okf-file-search"));
assert(adminTemplate.includes('data-okf-file-filter="category"'));
assert(adminTemplate.includes("No community OKF files match these filters."));
assert(adminTemplate.includes("No system-generated OKF files match these filters."));
const okfPluginSource = fs.readFileSync(path.join(__dirname, "..", "index.js"), "utf8");
assert(okfPluginSource.includes("preferSpecificKnowledgeChunks"));
assert(okfPluginSource.includes("trimForContext"));
assert(okfPluginSource.includes("remaining = 4200"));
grantPermission(db, { user_id: "editor-1", level: "edit", notes: "Trusted writer" }, admin);
assert.equal(accessForUser(db, editor).canEdit, true);
assert.equal(listPermissions(db).filter((grant) => !grant.revoked_at).length, 1);
const entry = createEntry(db, {
title: "Currency basics",
slug: "currency-basics",
category: "Community",
tags: "currency, coins",
aliases: "How do coins, bonuses, and payouts work?\nWhat is currency?",
summary: "Explains the community currency.",
user_markdown: "Users earn **coins** through activity.",
moderator_markdown: "Moderators may help users with missing transaction context.",
admin_markdown: "Admin-only economy configuration details.",
ai_facts_markdown: "The primary community currency is coins.",
source_links: "/commands",
visibility: "mod",
status: "published",
review_state: "approved"
}, admin);
assert.equal(entry.slug, "currency-basics");
assert.deepEqual(entry.aliases, ["How do coins, bonuses, and payouts work?", "What is currency?"]);
assert.equal(listEntries(db, {}, user).length, 0);
assert.equal(listEntries(db, {}, mod).length, 1);
assert.equal(getEntryBySlug(db, "currency-basics", user), null);
assert.equal(getEntryBySlug(db, "currency-basics", mod).moderator_markdown.includes("Moderators"), true);
assert.equal(getEntryBySlug(db, "currency-basics", admin).admin_markdown.includes("Admin-only"), true);
assert(searchForAi(db, "currency", mod).some((item) => item.facts.includes("Moderators")));
assert.equal(searchForAi(db, "currency", user).length, 0);
const publicEntry = createEntry(db, {
title: "Public help",
slug: "public-help",
category: "Support",
summary: "Public support answer.",
user_markdown: "Everyone can read this.",
admin_markdown: "Admin-only implementation notes.",
ai_facts_markdown: "Admin AI fact.",
visibility: "user",
status: "published",
review_state: "approved"
}, admin);
const publicUserContext = searchForAi(db, "public", user);
const publicAdminContext = searchForAi(db, "public", admin);
assert(publicUserContext.some((item) => item.id === publicEntry.id));
assert.equal(publicUserContext.some((item) => item.facts.includes("Admin-only")), false);
assert(publicAdminContext.some((item) => item.facts.includes("Admin-only implementation notes.")));
assert(publicAdminContext.some((item) => item.facts.includes("Admin AI fact.")));
const proposed = createEntry(db, {
title: "Draft from editor",
slug: "draft-from-editor",
summary: "Editor draft",
user_markdown: "Draft content."
}, editor);
assert.equal(proposed.status, "draft");
assert.equal(proposed.review_state, "review_pending");
assert.equal(listEntries(db, {}, user).some((item) => item.slug === "draft-from-editor"), false);
assert.equal(listEntries(db, {}, editor, { management: true }).some((item) => item.slug === "draft-from-editor"), true);
updateEntry(db, "draft-from-editor", {
title: "Draft from editor",
slug: "draft-from-editor",
summary: "Updated editor draft",
user_markdown: "Updated draft content.",
change_note: "Editor update."
}, editor);
assert.equal(listVersions(db, proposed.id).length, 2);
const restored = restoreVersion(db, proposed.id, 1, admin, "Test restore.");
assert.equal(restored.summary, "Editor draft");
assert.equal(restored.user_markdown, "Draft content.");
assert.equal(listVersions(db, proposed.id).length, 3);
assert.equal(searchForAi(db, "draft", admin).length, 0);
setEntryWorkflow(db, "draft-from-editor", "review", admin);
setEntryWorkflow(db, "draft-from-editor", "publish", admin);
assert.equal(getEntryBySlug(db, "draft-from-editor", user).summary, "Editor draft");
assert(searchForAi(db, "currency", mod).some((item) => item.slug === "currency-basics"));
assert.equal(searchForAi(db, "admin-only economy", user).length, 0);
assert(renderMarkdown("<script>alert(1)</script> **safe**").includes("&lt;script&gt;"));
assert.equal(renderMarkdown("[bad](javascript:alert(1))").includes("javascript:"), false);
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-okf-"));
ensureKnowledgeDirs(tempRoot);
const commandsKnowledgePath = path.join(tempRoot, "knowledge", "core", "commands.md");
const currencyKnowledgePath = path.join(tempRoot, "knowledge", "community", "currency.md");
const correctionKnowledgePath = path.join(tempRoot, "knowledge", "corrections", "currency.md");
fs.writeFileSync(commandsKnowledgePath, [
"---",
"id: core.commands",
"title: Core commands",
"scope: core",
"status: active",
"priority: 1",
"visibility: user",
"tags: commands, help",
"generated: true",
"editable: false",
"---",
"# Commands",
"Use !help to list available commands.",
"The public currency is {{community.currency.primary_name}}.",
"The admin-only reference is {{community.admin-secret.secret_value}}."
].join("\n"));
fs.writeFileSync(currencyKnowledgePath, [
"---",
"id: community.currency",
"title: Community currency",
"scope: community",
"status: active",
"priority: 5",
"visibility: user",
"tags: currency, coins",
"primary_name: coins",
"---",
"# Coins",
"The community currency is coins, not points."
].join("\n"));
fs.writeFileSync(correctionKnowledgePath, [
"---",
"id: correction.currency-name",
"title: Currency correction",
"scope: corrections",
"status: active",
"priority: 10",
"visibility: user",
"tags: currency",
"---",
"# Currency name",
"Always call the currency coins."
].join("\n"));
fs.writeFileSync(path.join(tempRoot, "knowledge", "community", "admin-secret.md"), [
"---",
"id: community.admin-secret",
"title: Admin secret",
"scope: community",
"status: active",
"visibility: admin",
"secret_value: hidden-admin-context",
"---",
"# Secret",
"Only admins should retrieve this."
].join("\n"));
fs.writeFileSync(path.join(tempRoot, "knowledge", "community", "people.md"), [
"---",
"id: community.people",
"title: Community people",
"scope: community",
"status: active",
"priority: 20",
"visibility: user",
"tags: people, identity, ookamikuntv, jenni",
"---",
"# Community people",
"## OokamiKunTV",
"OokamiKunTV is a known community contact in Lumi's local community knowledge."
].join("\n"));
const fileEntries = loadKnowledgeEntries(tempRoot);
assert.equal(fileEntries.length, 5);
assert(fileEntries.every((item) => item.chunks.length >= 1));
const userPlaceholders = listKnowledgePlaceholders({ user, rootDir: tempRoot, includeHidden: true });
assert(userPlaceholders.includes("{{community.currency.primary_name}}"));
assert.equal(userPlaceholders.includes("{{community.currency.scope}}"), false);
assert.equal(userPlaceholders.includes("{{plugins.plugin-throne-wishlist.scope}}"), false);
assert.equal(userPlaceholders.includes("{{community.admin-secret.secret_value}}"), false);
const adminPlaceholders = listKnowledgePlaceholders({ user: admin, rootDir: tempRoot, includeHidden: true });
assert(adminPlaceholders.includes("{{community.admin-secret.secret_value}}"));
const currencyResults = searchFileKnowledge({ query: "currency coins", user, rootDir: tempRoot, limit: 5 });
assert.equal(currencyResults[0].id, "correction.currency-name");
assert(currencyResults[0].source_metadata.path.endsWith("corrections/currency.md"));
assert(currencyResults[0].source_metadata.heading);
assert(Number.isFinite(currencyResults[0].source_metadata.score));
const identityResults = searchFileKnowledge({ query: "Who is OokamiKunTV?", user, rootDir: tempRoot, limit: 5 });
assert.equal(identityResults[0].id, "community.people");
assert(identityResults[0].facts.includes("OokamiKunTV is a known community contact"));
const commandUserResult = searchFileKnowledge({ query: "commands", user, rootDir: tempRoot, limit: 5 })[0];
assert(commandUserResult.facts.includes("The public currency is coins."));
assert(commandUserResult.facts.includes("[missing OKF reference]"));
const commandAdminResult = searchFileKnowledge({ query: "commands", user: admin, rootDir: tempRoot, limit: 5 })[0];
assert(commandAdminResult.facts.includes("The admin-only reference is hidden-admin-context."));
assert.equal(searchFileKnowledge({ query: "secret", user, rootDir: tempRoot, limit: 5 }).length, 0);
assert.equal(searchFileKnowledge({ query: "secret", user: admin, rootDir: tempRoot, limit: 5 })[0].id, "community.admin-secret");
const savedCommunity = saveCommunityKnowledgeFile(tempRoot, {
title: "Community roles",
slug: "community-roles",
id: "community.roles",
category: "Community",
visibility: "user",
status: "active",
priority: "7",
tags: "roles, community",
body: "# Roles\nCommunity roles are documented here."
});
assert.equal(savedCommunity.id, "community.roles");
assert.equal(getCommunityKnowledgeFile(tempRoot, "community-roles").title, "Community roles");
assert(listCommunityKnowledgeFiles(tempRoot).some((item) => item.slug === "community-roles"));
const renamedCommunity = saveCommunityKnowledgeFile(tempRoot, {
existing_slug: "community-roles",
title: "Community role guide",
slug: "role-guide",
id: "community.roles",
category: "Community",
visibility: "user",
status: "active",
tags: "roles",
body: "# Roles\nRenamed community role guide."
});
assert.equal(renamedCommunity.slug, "role-guide");
assert(fs.existsSync(path.join(tempRoot, "knowledge", "community", "role-guide.md")));
assert.equal(fs.existsSync(path.join(tempRoot, "knowledge", "community", "community-roles.md")), false);
fs.writeFileSync(path.join(tempRoot, "knowledge", "community", "generated.md"), [
"---",
"id: community.generated",
"title: Generated community file",
"scope: community",
"generated: true",
"editable: false",
"---",
"# Generated"
].join("\n"));
assert.throws(() => saveCommunityKnowledgeFile(tempRoot, {
existing_slug: "community-generated",
title: "Blocked",
body: "# Blocked"
}), /not editable/);
const savedCorrection = saveCorrectionKnowledgeFile(tempRoot, {
title: "Feedback correction",
slug: "feedback-correction",
source_feedback_id: "feedback-1",
source_feedback_url: "/admin/feedback?feedback=feedback-1",
visibility: "user",
priority: "120",
body: "# Feedback correction\nUse the corrected behavior from reviewed feedback."
});
assert.equal(savedCorrection.id, "correction.feedback-correction");
assert.equal(savedCorrection.path, "knowledge/corrections/feedback-correction.md");
assert.equal(savedCorrection.frontmatter.source_feedback_id, "feedback-1");
assert.equal(searchFileKnowledge({ query: "corrected behavior", user, rootDir: tempRoot, limit: 5 })[0].id, "correction.feedback-correction");
fs.writeFileSync(currencyKnowledgePath, [
"---",
"id: community.currency",
"title: Community currency",
"scope: community",
"status: active",
"priority: 5",
"visibility: user",
"tags: currency, coins",
"primary_name: gold stars",
"---",
"# Coins",
"The community currency is gold stars, not points."
].join("\n"));
assert(searchFileKnowledge({ query: "commands", user, rootDir: tempRoot, limit: 5 })[0].facts.includes("The public currency is gold stars."));
fs.rmSync(correctionKnowledgePath, { force: true });
assert.equal(searchFileKnowledge({ query: "currency coins", user, rootDir: tempRoot, limit: 5 }).some((item) => item.id === "correction.currency-name"), false);
fs.rmSync(tempRoot, { recursive: true, force: true });
const migrationRoot = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-okf-migrate-"));
ensureKnowledgeDirs(migrationRoot);
const legacyPlaceholderPath = path.join(migrationRoot, "knowledge", "community", "legacy.md");
fs.writeFileSync(legacyPlaceholderPath, [
"---",
"id: community.legacy",
"title: Legacy placeholder",
"scope: community",
"status: active",
"visibility: user",
"---",
"# Legacy",
"The old token is {community.currency.primary_name}.",
"The new token stays {{community.currency.primary_name}}."
].join("\n"));
const migratedPlaceholders = migrateSingleBracePlaceholders(migrationRoot);
assert.equal(migratedPlaceholders.changed, 1);
const migratedPlaceholderContent = fs.readFileSync(legacyPlaceholderPath, "utf8");
assert(migratedPlaceholderContent.includes("The old token is {{community.currency.primary_name}}."));
assert(migratedPlaceholderContent.includes("The new token stays {{community.currency.primary_name}}."));
fs.rmSync(migrationRoot, { recursive: true, force: true });
const generatedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-okf-generated-"));
fs.mkdirSync(path.join(generatedRoot, "plugins", "test-plugin"), { recursive: true });
fs.writeFileSync(path.join(generatedRoot, "package.json"), JSON.stringify({
name: "lumi-test",
version: "1.2.3",
description: "Generated test core."
}, null, 2));
fs.writeFileSync(path.join(generatedRoot, "README.md"), "# Lumi Test\n\nGenerated README.");
fs.mkdirSync(path.join(generatedRoot, "src", "web"), { recursive: true });
fs.writeFileSync(path.join(generatedRoot, "src", "web", "server.js"), "app.get('/admin/test', handler);\napp.post('/api/test', handler);\n");
fs.writeFileSync(path.join(generatedRoot, "plugins", "test-plugin", "plugin.json"), JSON.stringify({
id: "test-plugin",
name: "Test Plugin",
version: "0.0.1",
description: "Generated plugin knowledge."
}, null, 2));
fs.writeFileSync(path.join(generatedRoot, "plugins", "test-plugin", "index.js"), [
"router.get('/settings', handler);",
"commandRouter.registerCommands('test-plugin', [{ trigger: 'test', description: 'Test command.' }]);"
].join("\n"));
const generated = generateKnowledgeFiles(generatedRoot);
const normalizedGenerated = generated.map((file) => file.replace(/\\/g, "/"));
assert(normalizedGenerated.some((file) => file.endsWith("knowledge/core/lumi-core.md")));
assert(normalizedGenerated.some((file) => file.endsWith("knowledge/plugins/test-plugin.md")));
const generatedCore = fs.readFileSync(path.join(generatedRoot, "knowledge", "core", "lumi-core.md"), "utf8");
assert(generatedCore.includes("GET /admin/test"));
assert(generatedCore.includes("## Route Reference"));
assert(generatedCore.includes("### GET /admin/test"));
assert(generatedCore.includes("Response format:"));
const generatedPlugin = fs.readFileSync(path.join(generatedRoot, "knowledge", "plugins", "test-plugin.md"), "utf8");
assert(generatedPlugin.includes("Test Plugin"));
assert(generatedPlugin.includes("### GET /plugins/test-plugin/settings"));
assert(generatedPlugin.includes("Inputs:"));
assert(generatedPlugin.includes("test"));
fs.writeFileSync(path.join(generatedRoot, "knowledge", "plugins", "test-plugin.md"), [
"---",
"id: plugin.test-plugin",
"title: Manual Test Plugin",
"generated: false",
"editable: true",
"---",
"# Manual"
].join("\n"));
generateKnowledgeFiles(generatedRoot);
assert(fs.readFileSync(path.join(generatedRoot, "knowledge", "plugins", "test-plugin.md"), "utf8").includes("Manual Test Plugin"));
fs.rmSync(generatedRoot, { recursive: true, force: true });
const grant = listPermissions(db).find((row) => row.user_id === "editor-1" && !row.revoked_at);
revokePermission(db, grant.id, admin);
assert.equal(accessForUser(db, editor).canEdit, false);
db.close();
console.log("OKF plugin verification passed.");