102 lines
5.4 KiB
JavaScript
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);
|
|
});
|