Files
jason.woltjeandClaude Opus 5.5 467a313c7d plugins: mosaic plugins and the plugins verbs switch a module through the human-only path (vikunja:19/305, row 145)
Jason-approved merge, rebased: git diff 813671596..f4d7b91a9 from
work/vikunja-19-305, stacked on 295. Business reviewer verdict 57d7f0b7
approve on f4d7b91a9; Jason resolved merge decision 0eebf83f via the CLI.

Conflict resolution only. packages/bus/src/broker.mjs conflicted with
row 123 (89fd2d6fe): both sides extend the limits branch of request().
Kept row 123's organization.set role path and ORGANIZATION_KEYS, and
added 305's plugins and plugins.set to the verb list and its keys lines.
plugins and plugins.set take the human-only path as in 305: role is true
only for organization.set, the actor is the human, and the verb reaches
the organization writer unchanged. organization.set is unchanged.
packages/plugins/tests/searxng.test.mjs conflicted with 307: both add a
test at the end of the file. Kept 307's AbortSignal test, then 305's
hasModule test. Three other paths that refactor also changed merged
without conflict, with the source commit's +/- lines; the other six
paths are the source blobs. Filbert reviews the rebased diff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 17:50:59 -05:00

112 lines
5.7 KiB
JavaScript

// REQ-PLUG-2 REQ-HARN-2: the searxng search-source module loads through the real loader and
// answers from a fake SearXNG. No test touches the network.
import test from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { createLoader } from "../src/loader.mjs";
import { WEB_REFUSAL, WebRefusal } from "../../discord/src/web.mjs";
const fakeNet = (status, body) => {
const requests = [];
return {
requests,
lookup: async () => [{ address: "93.184.216.34", family: 4 }],
httpsRequest: (opts) => {
const req = new EventEmitter();
req.destroy = () => {};
req.end = () => {
requests.push(`${opts.hostname}${opts.path}`);
const res = new EventEmitter();
res.statusCode = status;
res.headers = { "content-type": "application/json" };
res.destroy = () => {};
queueMicrotask(() => {
req.emit("response", res);
res.emit("data", Buffer.from(body));
res.emit("end");
});
};
return req;
},
};
};
test("REQ-PLUG-2 the real tree's searxng-url module switches on as the search-source and answers", async () => {
const logs = [];
const loader = createLoader({ log: (e) => logs.push(e) });
const { loaded, refused } = await loader.load({ "search-source": "searxng-url" });
assert.deepEqual(refused, []);
assert.deepEqual(loaded["search-source"], ["searxng-url"]);
const [mod] = loader.modules("search-source");
const net = fakeNet(200, JSON.stringify({ results: [{ title: " A ", url: "https://example.org/a", content: "about a" }, { url: "javascript:x" }] }));
const found = await mod.search({ searxng: "https://search.example.org" }, "mosaic", net);
assert.deepEqual(found, { query: "mosaic", total: 2, results: [{ title: "A", url: "https://example.org/a", snippet: "about a" }] });
assert.match(net.requests[0], /^search\.example\.org\/search\?q=mosaic&format=json$/);
});
test("REQ-PLUG-2 a SearXNG that refuses JSON (403) is search-down, a bad settings url throws", async () => {
const loader = createLoader({ log: () => {} });
await loader.load({ "search-source": "searxng-url" });
const [mod] = loader.modules("search-source");
await assert.rejects(mod.search({ searxng: "https://search.example.org" }, "x", fakeNet(403, "no")),
(e) => e instanceof WebRefusal && e.reason === WEB_REFUSAL.SEARCH_DOWN);
assert.throws(() => mod.search({ searxng: "http://search.example.org" }, "x"), /https/);
});
test("REQ-PLUG-2 search-source takes one name, not a list", async () => {
const loader = createLoader({ log: () => {} });
const { refused } = await loader.load({ "search-source": ["searxng-url"] });
assert.equal(refused[0].code, "bad-setting");
assert.deepEqual(loader.loaded()["search-source"], []);
});
test("REQ-PLUG-2 searxng-bundled queries the fixed loopback address and ignores web.searxng", async () => {
const loader = createLoader({ log: () => {} });
const { loaded } = await loader.load({ "search-source": "searxng-bundled" });
assert.deepEqual(loaded["search-source"], ["searxng-bundled"]);
const [mod] = loader.modules("search-source");
const body = JSON.stringify({ results: [{ title: "B", url: "https://example.org/b", content: "b" }] });
const net = fakeNet(200, body);
net.httpRequest = net.httpsRequest;
net.lookup = async () => [{ address: "127.0.0.1", family: 4 }];
const found = await mod.search({ searxng: "https://elsewhere.example" }, "q", net);
assert.equal(found.results.length, 1);
assert.match(net.requests[0], /^127\.0\.0\.1\/search\?q=q&format=json$/);
});
test("REQ-PLUG-2 harness-builtin switches on, serves no broker search, and refuses if called", async () => {
const loader = createLoader({ log: () => {} });
const { loaded, refused } = await loader.load({ "search-source": "harness-builtin" });
assert.deepEqual(refused, []);
assert.deepEqual(loaded["search-source"], ["harness-builtin"]);
const [mod] = loader.modules("search-source");
assert.throws(() => mod.search({}, "q"), (e) => e instanceof WebRefusal && e.reason === WEB_REFUSAL.SEARCH_DOWN);
});
test("the searxng modules end the request when the broker's AbortSignal fires", async () => {
for (const name of ["searxng-url", "searxng-bundled"]) {
const loader = createLoader({ log: () => {} });
await loader.load({ "search-source": name });
const [mod] = loader.modules("search-source");
let destroyed = 0;
const net = {
lookup: async () => [{ address: "127.0.0.1", family: 4 }],
httpsRequest: () => { const req = new EventEmitter(); req.destroy = () => { destroyed += 1; }; req.end = () => {}; return req; },
};
net.httpRequest = net.httpsRequest;
const controller = new AbortController();
const pending = mod.search({ searxng: "https://search.example.org" }, "q", net, { signal: controller.signal });
await new Promise((r) => setTimeout(r, 20));
controller.abort();
// webSearch reports any failed request as "search is unavailable"; the abort is what ended it.
await assert.rejects(pending, (e) => e instanceof WebRefusal && e.reason === WEB_REFUSAL.SEARCH_DOWN, name);
assert.ok(destroyed >= 1, name);
}
});
test("hasModule: true for a shipped module, false for a missing one, an unknown kind or a bad name", async () => {
const { hasModule } = await import("../src/loader.mjs");
for (const name of ["harness-builtin", "searxng-bundled", "searxng-url"]) assert.equal(hasModule("search-source", name), true, name);
for (const [kind, name] of [["search-source", "ghost"], ["made-up", "searxng-url"], ["search-source", "../x"], ["search-source", "Searxng"], ["usage-source", "searxng-url"]]) assert.equal(hasModule(kind, name), false, `${kind}/${name}`);
});