329 lines
12 KiB
JavaScript
329 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.7.1",
|
|
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].permission_check({
|
|
user: { id: "user" },
|
|
context: { origin: "webui" }
|
|
}), 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;
|
|
});
|