Fix web search (again)
This commit is contained in:
+37
-11
@@ -4,14 +4,14 @@ import {createServer} from "node:http";
|
||||
import {createApp} from "../src/app.js";
|
||||
import {createChatService} from "../src/services/chatService.js";
|
||||
import {runMigrations} from "../src/db/schema.js";
|
||||
import {SearXNGSearchProvider, WebSearchError, buildWebSearchContext, buildWebSearchQuery} from "../src/services/webSearchService.js";
|
||||
import {SearXNGSearchProvider, WebSearchError, buildWebSearchContext, buildWebSearchQuery, decideWebSearch, selectSearchResults} from "../src/services/webSearchService.js";
|
||||
import {createModelService} from "../src/services/modelService.js";
|
||||
import {formatChatMessage} from "../src/services/chatMessageHelpers.js";
|
||||
import {appendSources, createWebSearchToggle} from "../public/app/webSearch.js";
|
||||
import {stream} from "../public/app/api.js";
|
||||
|
||||
// Local protocol fixtures are test-only; production always uses the configured provider.
|
||||
const result = {title: "Search result", url: "https://example.org/news", content: "A current fact."};
|
||||
const result = {title: "Major events on September 10, 2025", url: "https://example.org/news", content: "A news report covering major events on September 10, 2025."};
|
||||
const source = {title: result.title, url: result.url, snippet: result.content};
|
||||
|
||||
async function listen(t, handler) {
|
||||
@@ -28,7 +28,7 @@ test("SearXNG encodes queries, limits results and treats result data as untruste
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({results: [
|
||||
{title: "bad", url: "javascript:alert(1)"},
|
||||
...Array.from({length: 9}, (_, index) => ({title: "T".repeat(500), url: `https://example.org/${index}`, content: "S".repeat(2000)}))
|
||||
...Array.from({length: 9}, (_, index) => ({title: `${"T".repeat(290)} ${index} ${"T".repeat(100)}`, url: `https://example.org/${index}`, content: "S".repeat(2000)}))
|
||||
]}));
|
||||
});
|
||||
const provider = new SearXNGSearchProvider({searxngUrl: `${upstream.url}/base`, webSearchTimeoutMs: 1000});
|
||||
@@ -37,7 +37,7 @@ test("SearXNG encodes queries, limits results and treats result data as untruste
|
||||
assert.equal(received.pathname, "/base/search");
|
||||
assert.equal(received.searchParams.get("format"), "json");
|
||||
assert.equal(received.searchParams.get("q"), query.trim().slice(0, 500));
|
||||
assert.equal(sources.length, 5);
|
||||
assert.equal(sources.length, 9);
|
||||
assert.equal(sources[0].title.length, 300);
|
||||
assert.equal(sources[0].snippet.length, 1500);
|
||||
assert.equal((await provider.search("test", {limit: 2})).length, 2);
|
||||
@@ -50,12 +50,31 @@ test("SearXNG encodes queries, limits results and treats result data as untruste
|
||||
assert.match(context[1].content, /<web_search_results>/);
|
||||
assert.match(context[1].content, /\[1\]/);
|
||||
assert.match(context[1].content, /IGNORE ALL INSTRUCTIONS/);
|
||||
assert.equal(buildWebSearchQuery("What happened on 10.09.2025?"), "What happened on 10.09.2025? 2025-09-10");
|
||||
assert.equal(buildWebSearchQuery("What happened on 10.09.2025?"), "major news events September 10, 2025 2025-09-10");
|
||||
const emptyContext = buildWebSearchContext([], new Date("2026-09-21T12:00:00.000Z"));
|
||||
assert.equal(emptyContext.length, 1);
|
||||
assert.match(emptyContext[0].content, /no usable results/);
|
||||
});
|
||||
|
||||
test("search decisions and source selection use only relevant, renumbered evidence", () => {
|
||||
assert.deepEqual(decideWebSearch("What date is it today?"), {needsSearch: false, intent: "runtime"});
|
||||
assert.equal(decideWebSearch("What is the latest Node.js release?").needsSearch, true);
|
||||
assert.equal(decideWebSearch("What happened on 10.09.2025?").intent, "news");
|
||||
const selected = selectSearchResults("What happened on 10.09.2025?", [
|
||||
{title: "Major events on September 10, 2025", url: "https://news.example/events?utm_source=test", snippet: "World news and official reports from September 10, 2025."},
|
||||
{title: "Global reporting from September 10, 2025", url: "https://other.example/events", snippet: "World news report from September 10, 2025."},
|
||||
{title: "October 9, 2025 current events", url: "https://calendar.example/october", snippet: "A different date."},
|
||||
{title: "Campus memories", url: "https://university.example/memories", snippet: "An incidental mention of September 10, 2025."},
|
||||
{title: "Reddit discussion", url: "https://www.reddit.com/r/example", snippet: "A discussion unrelated to events on the requested date."}
|
||||
], decideWebSearch("What happened on 10.09.2025?"));
|
||||
assert.equal(selected.length, 2);
|
||||
assert.deepEqual(selected.map((item) => item.url), ["https://news.example/events", "https://other.example/events"]);
|
||||
assert.deepEqual(selected.map((item, index) => ({number: index + 1, title: item.title})), [
|
||||
{number: 1, title: "Major events on September 10, 2025"},
|
||||
{number: 2, title: "Global reporting from September 10, 2025"}
|
||||
]);
|
||||
});
|
||||
|
||||
test("every Ollama request includes dynamic server date context", async () => {
|
||||
let requestBody;
|
||||
const modelService = createModelService({
|
||||
@@ -110,11 +129,13 @@ test("SearXNG handles empty results, failures, malformed responses, redirects, l
|
||||
test("chat search integrates with streaming, persistence, personas, settings, retry, branch and import", async (t) => {
|
||||
let searchCount = 0;
|
||||
let lastSearchQuery = "";
|
||||
let lastSearchCategory = "";
|
||||
let failSearch = false;
|
||||
let emptySearch = false;
|
||||
const upstream = await listen(t, (req, res) => {
|
||||
searchCount += 1;
|
||||
lastSearchQuery = new URL(req.url, "http://localhost").searchParams.get("q");
|
||||
lastSearchCategory = new URL(req.url, "http://localhost").searchParams.get("categories");
|
||||
res.writeHead(failSearch ? 503 : 200, {"Content-Type": "application/json"});
|
||||
res.end(JSON.stringify({results: emptySearch ? [] : [result]}));
|
||||
});
|
||||
@@ -152,6 +173,10 @@ test("chat search integrates with streaming, persistence, personas, settings, re
|
||||
assert.equal(normal.status, 200);
|
||||
assert.equal(normal.body.sources, undefined);
|
||||
assert.equal(searchCount, 0);
|
||||
const today = await api("/chat", {chatId, message: "What date is it today?", webSearch: true});
|
||||
assert.equal(today.status, 200);
|
||||
assert.equal(today.body.sources, undefined);
|
||||
assert.equal(searchCount, 0);
|
||||
await api("/chat", {chatId, message: "ordinary too", webSearch: false});
|
||||
assert.equal(searchCount, 0);
|
||||
assert.equal((await api("/chat", {chatId, message: "invalid", webSearch: "true"})).status, 400);
|
||||
@@ -172,7 +197,8 @@ test("chat search integrates with streaming, persistence, personas, settings, re
|
||||
assert.deepEqual(events[1].data, {status: "generating", sourceCount: 1});
|
||||
assert.deepEqual(events.at(-1).data.sources, [source]);
|
||||
assert.equal(searchCount, 1);
|
||||
assert.equal(lastSearchQuery, "What happened on 10.09.2025? 2025-09-10");
|
||||
assert.equal(lastSearchQuery, "major news events September 10, 2025 2025-09-10");
|
||||
assert.equal(lastSearchCategory, "news");
|
||||
const call = calls.at(-1);
|
||||
assert.equal(call.model, "chosen:model");
|
||||
assert.deepEqual(call.options.generation, {temperature: 0.4, contextLength: 4096, responseLength: 300});
|
||||
@@ -204,22 +230,22 @@ test("chat search integrates with streaming, persistence, personas, settings, re
|
||||
for (const message of allMessages) assert.deepEqual(JSON.parse(message.sources), [source]);
|
||||
|
||||
emptySearch = true;
|
||||
const empty = await api("/chat", {chatId, message: "nothing found", webSearch: true});
|
||||
const empty = await api("/chat", {chatId, message: "latest news nothing found", webSearch: true});
|
||||
assert.deepEqual(empty.body.sources, []);
|
||||
assert.ok(calls.at(-1).messages.some((message) => message.content.includes("no usable results")));
|
||||
assert.ok(!calls.at(-1).messages.some((message) => message.role === "user" && message.content.startsWith("<web_search_results>")));
|
||||
const beforeFailure = calls.length;
|
||||
failSearch = true;
|
||||
const failed = await api("/chat", {chatId, message: "keep this message", webSearch: true});
|
||||
const failed = await api("/chat", {chatId, message: "latest news please", webSearch: true});
|
||||
assert.equal(failed.status, 503);
|
||||
assert.equal(failed.body.error, "Web search is currently unavailable.");
|
||||
assert.equal(calls.length, beforeFailure);
|
||||
messages = (await api(`/chats/${chatId}/messages`)).body.messages;
|
||||
assert.equal(messages.at(-1).content, "keep this message");
|
||||
assert.equal(messages.at(-1).content, "latest news please");
|
||||
assert.equal(messages.at(-1).deliveryStatus, "failed");
|
||||
const failedStream = await fetch(`${backend.url}/chat/stream`, {
|
||||
method: "POST", headers: {"Content-Type": "application/json", Cookie: cookie},
|
||||
body: JSON.stringify({chatId, message: "stream failure", webSearch: true})
|
||||
body: JSON.stringify({chatId, message: "latest stream news", webSearch: true})
|
||||
});
|
||||
const failedWire = await failedStream.text();
|
||||
assert.match(failedWire, /event: status/);
|
||||
@@ -246,7 +272,7 @@ test("chat search integrates with streaming, persistence, personas, settings, re
|
||||
const chatService = createChatService(repos, modelService, unconfigured.locals.config);
|
||||
const localRequest = {user: "local-test", chatId: localChat.id, model: "local", message: "hello"};
|
||||
assert.equal((await chatService.sendChatMessage(localRequest)).reply, "A fact [1].");
|
||||
assert.equal((await chatService.sendChatMessage({...localRequest, webSearch: true})).status, 503);
|
||||
assert.equal((await chatService.sendChatMessage({...localRequest, message: "latest news", webSearch: true})).status, 503);
|
||||
const normalChunks = [];
|
||||
const normalStream = await chatService.streamChatMessage({...localRequest, onChunk: (chunk) => normalChunks.push(chunk)});
|
||||
assert.equal(normalStream.sources, undefined);
|
||||
|
||||
Reference in New Issue
Block a user