Lumi/plugins/lumi_ai_web_search/tests/verify.js
2026-06-14 04:18:36 +02:00

333 lines
12 KiB
JavaScript

const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { SearchProvider } = require("../backend/provider_adapter");
const { formatResults } = require("../backend/result_formatter");
const { WebSearchTool } = require("../backend/search_tool");
const { readSettings, writeSettings } = require("../backend/settings");
const { evaluateUrl, matchesRule } = require("../backend/url_policy");
const { ToolRegistry } = require("../../lumi_ai/backend/tool_router");
const { ToolInstaller } = require("../../lumi_ai/backend/tool_installer");
const { ToolLoader } = require("../../lumi_ai/backend/tool_loader");
const plugin = require("../index");
const PUBLIC_DNS = async () => ["93.184.216.34"];
async function run() {
await verifyPolicy();
await verifyRedirectPolicy();
verifyFormatting();
await verifySearchFlow();
await verifyLoaderLifecycle();
verifyRegistrationAvailability();
verifyStaticFiles();
console.log("Lumi AI Web Search verification passed.");
}
async function verifyLoaderLifecycle() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-web-loader-"));
const pluginsDir = path.join(root, "plugins");
const toolDir = path.join(pluginsDir, "lumi_ai_web_search");
copyDirectory(path.resolve(__dirname, ".."), toolDir, new Set(["data"]));
fs.mkdirSync(path.join(toolDir, "data"), { recursive: true });
const installer = new ToolInstaller({
pluginsDir,
stagingRoot: path.join(root, "staging"),
repoClient: {}
});
const registry = new ToolRegistry(() => {});
const loader = new ToolLoader({
registry,
installer,
settings: { getSetting: (_key, fallback) => fallback },
stateFile: path.join(root, "enabled.json"),
lumiAiVersion: "0.8.0",
lumiVersion: "0.1.0"
});
const unavailable = await loader.enable("lumi_ai_web_search");
assert.equal(unavailable.unavailable, true);
assert.equal(registry.tools.has("lumi_ai_web_search.search"), false);
writeSettings(path.join(toolDir, "data"), providerSettings());
const enabled = await loader.enable("lumi_ai_web_search");
assert.equal(enabled.loaded, true);
assert.equal(registry.tools.has("lumi_ai_web_search.search"), true);
await loader.disable("lumi_ai_web_search");
assert.equal(registry.tools.has("lumi_ai_web_search.search"), false);
fs.rmSync(root, { recursive: true, force: true });
}
async function verifyPolicy() {
let result = await evaluateUrl("https://docs.example.com/guide", {
mode: "whitelist",
rules: ["*.example.com/*"],
resolveHost: PUBLIC_DNS
});
assert.equal(result.allowed, true);
result = await evaluateUrl("https://unrelated.test/guide", {
mode: "whitelist",
rules: ["*.example.com/*"],
resolveHost: PUBLIC_DNS
});
assert.equal(result.allowed, false);
assert.equal(result.reason, "not_whitelisted");
result = await evaluateUrl("https://ads.example.com/tracker", {
mode: "blacklist",
rules: ["*.example.com/tracker*"],
resolveHost: PUBLIC_DNS
});
assert.equal(result.allowed, false);
result = await evaluateUrl("https://docs.example.org/", {
mode: "blacklist",
rules: ["*.example.com/tracker*"],
resolveHost: PUBLIC_DNS
});
assert.equal(result.allowed, true);
for (const target of [
"http://127.0.0.1/",
"http://10.1.2.3/",
"http://169.254.169.254/latest/meta-data/",
"http://localhost/",
"file:///etc/passwd"
]) {
result = await evaluateUrl(target, {
mode: "blacklist",
rules: [],
resolveHost: PUBLIC_DNS
});
assert.equal(result.allowed, false, target);
}
result = await evaluateUrl("https://dns-rebind.example/", {
mode: "blacklist",
rules: [],
resolveHost: async () => ["10.0.0.8"]
});
assert.equal(result.allowed, false);
assert.equal(matchesRule(new URL("https://docs.example.com/guide/start"), "example.com/guide"), true);
assert.equal(matchesRule(new URL("https://example.com/"), "http://example.com/"), false);
}
async function verifyRedirectPolicy() {
const provider = new SearchProvider({
resolveHost: PUBLIC_DNS,
fetch: async () => response({
status: 302,
headers: { location: "http://127.0.0.1/private" }
})
});
await assert.rejects(
() => provider.search("test", providerSettings()),
/blocked by policy/i
);
let calls = 0;
const crossOrigin = new SearchProvider({
resolveHost: PUBLIC_DNS,
fetch: async () => {
calls += 1;
return response({
status: 302,
headers: { location: "https://other-provider.example/search" }
});
}
});
await assert.rejects(
() => crossOrigin.search("test", { ...providerSettings(), provider_api_key: "secret" }),
/cross_origin_provider_redirect/i
);
assert.equal(calls, 1);
}
function verifyFormatting() {
const rows = [
result("Official docs", "https://docs.example.com/guide", "A detailed official answer for the requested subject.", "documentation"),
result("Community post", "https://community.example.com/post", "A secondary explanation with useful context.", "web"),
result("Recent update", "https://news.example.com/update", "A recently published update.", "news", "2026-06-12")
];
const settings = {
max_results: 5,
show_source_links: true,
twitch_output_chars: 180,
discord_output_chars: 700,
webui_output_chars: 3000,
other_output_chars: 500
};
const fact = formatResults(rows, { reason: "fact_lookup", origin: "twitch", settings });
const resource = formatResults(rows, { reason: "resource_lookup", origin: "discord", settings });
const webui = formatResults(rows, { reason: "documentation_lookup", origin: "webui", settings });
assert(fact.condensed_text.length <= 180);
assert(fact.results.length <= 2);
assert(resource.condensed_text.length <= 700);
assert(webui.condensed_text.length > fact.condensed_text.length);
assert.equal(webui.results[0].source_type, "documentation");
}
async function verifySearchFlow() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-web-search-"));
const settings = {
...providerSettings(),
enabled: true,
policy_mode: "whitelist",
url_rules: ["*.example.com/*"],
allowed_origins: ["webui", "discord", "twitch"],
cache_ttl_seconds: 60
};
writeSettings(root, settings);
let calls = 0;
const provider = {
resolveHost: PUBLIC_DNS,
async search() {
calls += 1;
return [
result("<b>Verified fact</b>", "https://docs.example.com/fact", "The <em>answer</em> is current.", "documentation"),
result("Blocked local", "http://127.0.0.1/private", "Must never be returned.", "web")
];
},
async fetchPage() {
return { url: "https://docs.example.com/fact", text: "Expanded public page text." };
}
};
const tool = new WebSearchTool({ dataDir: root, provider });
const first = await tool.run({
query: "current fact",
reason: "fact_lookup",
user: { id: "user-1" },
ctx: { origin: "webui", server_id: "server-1" }
});
assert.equal(first.status, "ok");
assert.equal(first.result_count, 1);
assert.equal(first.results[0].title, "Verified fact");
assert.equal(first.results.some((entry) => entry.url?.includes("127.0.0.1")), false);
const cached = await tool.run({
query: "current fact",
reason: "fact_lookup",
user: { id: "user-1" },
ctx: { origin: "webui", server_id: "server-1" }
});
assert.equal(cached.cache_hit, true);
assert.equal(calls, 1);
const twitch = await tool.run({
query: "current fact twitch",
reason: "resource_lookup",
user: { id: "user-1" },
ctx: { origin: "twitch", channel_id: "channel-1" }
});
assert(twitch.condensed_text.length <= readSettings(root).twitch_output_chars);
assert.equal(calls, 2);
writeSettings(root, { ...readSettings(root), allowed_origins: ["webui"] });
const blockedOrigin = await tool.run({
query: "current fact",
reason: "fact_lookup",
origin: "webui",
user: { id: "user-1" },
ctx: { origin: "discord" }
});
assert.equal(blockedOrigin.status, "blocked");
assert.equal(blockedOrigin.blocked_reason, "origin_not_allowed");
const failing = new WebSearchTool({
dataDir: root,
provider: {
resolveHost: PUBLIC_DNS,
async search() { throw new Error("provider secret https://provider.example/api?token=secret"); }
}
});
const failed = await failing.run({
query: "failure",
reason: "general_lookup",
user: { id: "user-2" },
ctx: { origin: "webui" }
});
assert.equal(failed.status, "unavailable");
assert.equal(failed.error.includes("provider.example"), false);
const audit = fs.readFileSync(path.join(root, "audit.jsonl"), "utf8")
.trim().split(/\r?\n/).map(JSON.parse);
assert(audit.some((entry) =>
entry.query === "current fact" &&
entry.actor === "user-1" &&
entry.origin === "webui" &&
typeof entry.timing_ms === "number"
));
fs.rmSync(root, { recursive: true, force: true });
}
function verifyRegistrationAvailability() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-web-register-"));
assert.equal(plugin.checkAvailability({ paths: { data: root } }).available, false);
writeSettings(root, { ...providerSettings(), enabled: true });
assert.equal(plugin.checkAvailability({ paths: { data: root } }).available, true);
const definitions = [];
plugin.register({
paths: { data: root },
registerTool: (definition) => definitions.push(definition)
});
assert.equal(definitions.length, 1);
assert.equal(definitions[0].tool_id, "lumi_ai_web_search.search");
assert.equal(definitions[0].read_only, true);
assert.equal(definitions[0].origin_check({
context: { origin: "discord", permission_context: { webui_actions_allowed: false } }
}), true);
assert.equal(definitions[0].permission_check({
user: { id: "user" },
context: { origin: "discord", permission_context: { webui_actions_allowed: false } }
}), true);
fs.rmSync(root, { recursive: true, force: true });
}
function verifyStaticFiles() {
const root = path.resolve(__dirname, "..");
const metadata = JSON.parse(fs.readFileSync(path.join(root, "tool_info.json"), "utf8"));
assert.equal(metadata.tool_id, "lumi_ai_web_search");
assert.equal(metadata.settings_schema.policy_mode.default, "whitelist");
assert(fs.existsSync(path.join(root, "readme.md")));
assert(fs.readFileSync(path.join(root, "views", "settings-modal.ejs"), "utf8").includes("settings_schema"));
}
function providerSettings() {
return {
...Object.fromEntries(
Object.entries(require("../tool_info.json").settings_schema).map(([key, field]) => [key, structuredClone(field.default)])
),
provider_endpoint: "https://search.example.net/search",
enabled: true,
allowed_origins: ["webui", "discord", "twitch"],
url_rules: ["*.example.com/*"]
};
}
function result(title, url, snippet, sourceType, date = null) {
return { title, url, snippet, source_type: sourceType, date, relevance_score: 0.9 };
}
function response({ status = 200, headers = {}, body = "" }) {
const normalized = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
return {
ok: status >= 200 && status < 300,
status,
headers: {
get(name) { return normalized[String(name).toLowerCase()] || null; }
},
async arrayBuffer() { return Buffer.from(body); }
};
}
function copyDirectory(source, destination, ignored = new Set()) {
fs.mkdirSync(destination, { recursive: true });
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
if (ignored.has(entry.name)) continue;
const from = path.join(source, entry.name);
const to = path.join(destination, entry.name);
if (entry.isDirectory()) copyDirectory(from, to);
else if (entry.isFile()) fs.copyFileSync(from, to);
}
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});