Files
KrisHD_AI/test/automaticWebSearch.test.js
Fretux 7be654af11
CI / check (22) (push) Successful in 9m35s
CI / check (24) (push) Successful in 9m34s
Enable automatic web search by default; improve toggle behavior and search planning.
2026-10-06 08:50:03 +02:00

102 lines
5.4 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {createServer} from "node:http";
import {createApp} from "../src/app.js";
async function listen(t, handler) {
const server = createServer(handler);
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
t.after(() => new Promise(resolve => { server.closeAllConnections(); server.close(resolve); }));
return `http://127.0.0.1:${server.address().port}`;
}
test("automatic search defaults on for sends, follow-ups, retries and edited branches; opt-out bypasses planning", async t => {
const order = [], plans = [], answers = [], queries = [];
let search = true;
const query = "Swiss rail pass visitor comparison";
const searxngUrl = await listen(t, (req, res) => {
order.push("search");
queries.push(new URL(req.url, "http://localhost").searchParams.get("q"));
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({results: [{title: "Swiss rail pass comparison", url: "https://example.org/passes",
content: "Compare Swiss rail pass options for visitors and their trips."}]}));
});
const app = createApp({config: {dbPath: ":memory:", testMode: true, sessionSecret: "automatic-search-test", searxngUrl,
chatSummaryUpdateEveryMessages: 0, eventLoopLagMonitorEnabled: false, slowRequestLoggingEnabled: false,
structuredRequestLoggingEnabled: false}, modelOverrides: {fetch: async (_url, options) => {
const body = JSON.parse(options.body);
if (body.format?.properties?.search) {
order.push("plan");
plans.push(body);
return new Response(JSON.stringify({message: {content: JSON.stringify({search, query: search ? query : ""})}, done: true}));
}
order.push("answer");
answers.push(body);
return new Response(JSON.stringify({message: {content: "A helpful answer."}, done: true}));
}}});
t.after(() => app.locals.close());
const base = await listen(t, app);
let cookie;
const request = async (path, body) => {
const response = await fetch(base + path, {method: body ? "POST" : "GET",
headers: {"Content-Type": "application/json", ...(cookie ? {Cookie: cookie} : {})},
body: body ? JSON.stringify(body) : undefined});
if (response.headers.get("set-cookie")) cookie = response.headers.get("set-cookie").split(";")[0];
assert.equal(response.status, 200);
return response;
};
const api = async (path, body) => (await request(path, body)).json();
await api("/register", {username: "automatic-search", password: "test-password"});
const chatId = (await api("/chats", {title: "Rail passes"})).chat.id;
const question = "Which rail pass suits my trip to Switzerland?";
const wire = await (await request("/chat/stream", {chatId, message: question, model: "qwen3:14b", reasoning: true})).text();
assert.deepEqual(order, ["plan", "search", "answer"]);
assert.match(wire, /"status":"searching"/);
assert.match(wire, /"searchState":"succeeded"/);
assert.doesNotMatch(wire, /"query"|"search":true/);
assert.equal(plans[0].model, "qwen3:14b");
assert.equal(plans[0].think, false);
assert.equal(answers[0].think, true);
assert.ok(answers[0].messages.some(message => message.content.startsWith("<web_search_results>")));
let messages = (await api(`/chats/${chatId}/messages`)).messages;
assert.deepEqual(messages.map(message => message.content), [question, "A helpful answer."]);
assert.equal(messages[1].sources.length, 1);
const botId = messages[1].id, userId = messages[0].id;
const followup = await api("/chat", {chatId, message: "And which one should I choose?"});
assert.equal(followup.searchState, "succeeded");
assert.deepEqual(queries, [query, query]);
assert.equal(JSON.parse(plans.at(-1).messages.at(-1).content).history[0].content, question);
search = false;
order.length = 0;
const skipped = await api("/chat", {chatId, message: "Write a fictional breaking news story"});
assert.equal(skipped.searchState, "skipped");
assert.equal(skipped.sources, undefined);
assert.deepEqual(order, ["plan", "answer"]);
assert.ok(!answers.at(-1).messages.some(message => message.content.startsWith("<web_search_results>")));
search = true;
order.length = 0;
const disabled = await api("/chat", {chatId, message: "Search the web for rail passes", webSearch: false});
assert.equal(disabled.searchState, "skipped");
assert.deepEqual(order, ["answer"]);
for (const endpoint of [`/chats/${chatId}/messages/${botId}/retry/stream`, `/chats/${chatId}/messages/by-index/1/retry`]) {
order.length = 0;
await (await request(endpoint, {})).text();
assert.deepEqual(order, ["plan", "search", "answer"]);
const context = JSON.parse(plans.at(-1).messages.at(-1).content);
assert.equal(context.message, question);
assert.deepEqual(context.history, [], "A retry must not use later conversation turns");
}
order.length = 0;
const edited = await api(`/chats/${chatId}/messages/${userId}/edit-resend`, {
chatId, message: "Which rail pass is suitable for my Swiss trip?", confirmed: true
});
assert.notEqual(edited.chat.id, chatId);
assert.deepEqual(order, ["plan", "search", "answer"]);
messages = (await api(`/chats/${edited.chat.id}/messages`)).messages;
assert.equal(messages.at(-1).sources.length, 1);
});