import assert from "node:assert/strict"; import {mkdtempSync, rmSync, writeFileSync} from "node:fs"; import http from "node:http"; import {tmpdir} from "node:os"; import path from "node:path"; import {spawn} from "node:child_process"; const root = process.cwd(); const temporaryDirectory = mkdtempSync(path.join(tmpdir(), "xiteng-chat-history-test-")); const databasePath = path.join(temporaryDirectory, "chat.db"); const tokenPath = path.join(temporaryDirectory, "service-token"); writeFileSync(tokenPath, "history-test-token"); function availablePort() { return new Promise((resolve, reject) => { const server = http.createServer(); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const port = server.address().port; server.close(() => resolve(port)); }); }); } async function waitForServer(url, child) { const deadline = Date.now() + 30000; while (Date.now() < deadline) { if (child.exitCode !== null) throw new Error(`Bun server exited with code ${child.exitCode}`); try { if ((await fetch(url)).ok) return; } catch {} await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error("Timed out waiting for history test server"); } async function startServer() { const port = await availablePort(); const child = spawn(process.execPath, [path.join(root, "src/server.ts")], { cwd: root, env: { ...process.env, PORT: String(port), STATIC_ROOT: path.join(root, "dist"), CHAT_DATABASE_PATH: databasePath, KEY_VAULT_URL: "http://127.0.0.1:1", KEY_VAULT_TOKEN_FILE: tokenPath }, stdio: "ignore" }); await waitForServer(`http://127.0.0.1:${port}/api/health`, child); return {child, origin: `http://127.0.0.1:${port}`}; } function stopServer(child) { if (child.exitCode !== null) return Promise.resolve(); return new Promise((resolve) => { child.once("exit", resolve); child.kill("SIGTERM"); }); } async function api(origin, identity, pathname, init = {}) { const response = await fetch(`${origin}${pathname}`, { ...init, headers: { "X-Authentik-Username": identity.username, "X-Authentik-Uid": identity.sub, ...(init.body ? {"Content-Type": "application/json"} : {}), ...(init.headers || {}) } }); const payload = response.status === 204 ? null : await response.json(); return {response, payload}; } const owner = {username: "history-owner", sub: "owner-sub"}; const other = {username: "history-other", sub: "other-sub"}; let server; try { server = await startServer(); const initialSettings = {reasoning: "low", showReasoningSummary: false, temperature: null, maxOutputTokens: null}; const createdResult = await api(server.origin, owner, "/api/conversations", { method: "POST", body: JSON.stringify({providerId: "openai", model: "gpt-test", generationSettings: initialSettings}) }); assert.equal(createdResult.response.status, 201); assert.deepEqual(createdResult.payload.conversation.generationSettings, initialSettings); const conversationId = createdResult.payload.conversation.id; const messages = [ {id: "user-1", role: "user", parts: [{type: "text", text: "persistent question"}]}, {id: "assistant-1", role: "assistant", parts: [{type: "reasoning", text: "persistent reasoning"}, {type: "text", text: "persistent answer"}], metadata: {custom: {response: {providerId: "openai", model: "gpt-test", durationMs: 2000, outputTokens: 40, tokensPerSecond: 20}}}} ]; const savedResult = await api(server.origin, owner, `/api/conversations/${conversationId}`, { method: "PUT", body: JSON.stringify({providerId: "openai", model: "gpt-test", messages}) }); assert.equal(savedResult.response.status, 200); assert.equal(savedResult.payload.conversation.title, "persistent question"); assert.equal(savedResult.payload.conversation.messageCount, 2); const updatedSettings = {reasoning: "high", showReasoningSummary: true, temperature: 0.7, maxOutputTokens: 4096}; const settingsResult = await api(server.origin, owner, `/api/conversations/${conversationId}`, { method: "PATCH", body: JSON.stringify({providerId: "openai", model: "gpt-test", generationSettings: updatedSettings}) }); assert.equal(settingsResult.response.status, 200); await stopServer(server.child); server = await startServer(); const restored = await api(server.origin, owner, `/api/conversations/${conversationId}`); assert.equal(restored.response.status, 200); assert.deepEqual(restored.payload.conversation.messages, messages); assert.deepEqual(restored.payload.conversation.generationSettings, updatedSettings); const ownerList = await api(server.origin, owner, "/api/conversations"); assert.equal(ownerList.payload.conversations.length, 1); assert.equal(ownerList.payload.conversations[0].id, conversationId); const otherList = await api(server.origin, other, "/api/conversations"); assert.deepEqual(otherList.payload.conversations, []); assert.equal((await api(server.origin, other, `/api/conversations/${conversationId}`)).response.status, 404); assert.equal((await api(server.origin, other, `/api/conversations/${conversationId}`, {method: "DELETE"})).response.status, 404); assert.equal((await api(server.origin, owner, `/api/conversations/${conversationId}`, {method: "DELETE"})).response.status, 204); assert.equal((await api(server.origin, owner, `/api/conversations/${conversationId}`)).response.status, 404); console.log("Chat history API tests passed"); } finally { if (server) await stopServer(server.child); rmSync(temporaryDirectory, {recursive: true, force: true}); }