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(""))); 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(""))); 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); });