import test from "node:test"; import assert from "node:assert/strict"; import {createApp} from "../src/app.js"; class CookieJar { constructor() { this.cookie = ""; } update(response) { const values = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [response.headers.get("set-cookie")].filter(Boolean); if (!values.length) return; this.cookie = values.map((value) => value.split(";")[0]).join("; "); } headers(extra = {}) { return this.cookie ? {...extra, Cookie: this.cookie} : extra; } } async function startTestServer(overrides = {}) { let replyCount = 0; const modelService = overrides.modelService || { async generateReply() { replyCount += 1; return `stub-reply-${replyCount}`; }, async streamReply(model, messagesPayload, onChunk) { replyCount += 1; const reply = `stub-reply-${replyCount}`; if (onChunk) { onChunk(reply.slice(0, 5), reply.slice(0, 5)); onChunk(reply.slice(5), reply); } return reply; }, async listModels() { return {models: [{name: "mistral:latest"}]}; }, async checkHealth() { return {ok: true, checkedAt: new Date().toISOString(), error: null}; }, mapError(error) { return {status: 500, body: {error: error.message, code: "TEST_MODEL"}}; } }; const app = createApp({ config: { port: 0, dbPath: ":memory:", testMode: true, sessionSecret: "test-secret", ...(overrides.config || {}) }, modelService }); const server = await new Promise((resolve) => { const instance = app.listen(0, () => resolve(instance)); }); const {port} = server.address(); const baseUrl = `http://127.0.0.1:${port}`; return { app, baseUrl, async close() { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); await app.locals.close(); } }; } async function request(baseUrl, path, {method = "GET", body, jar} = {}) { const response = await fetch(`${baseUrl}${path}`, { method, headers: { "Content-Type": "application/json", ...(jar ? jar.headers() : {}) }, body: body ? JSON.stringify(body) : undefined }); if (jar) jar.update(response); const json = await response.json(); return {status: response.status, json}; } test("auth flow validates inputs and persists session", async () => { const server = await startTestServer(); const jar = new CookieJar(); await request(server.baseUrl, "/register", { method: "POST", body: {username: "ab", password: "123456"} }).then(({status, json}) => { assert.equal(status, 400); assert.match(json.error, /at least 3 characters/i); }); await request(server.baseUrl, "/register", { method: "POST", body: {username: "alice", password: "password123"} }).then(({status}) => assert.equal(status, 200)); await request(server.baseUrl, "/login", { method: "POST", body: {username: "alice", password: "password123"}, jar }).then(({status}) => assert.equal(status, 200)); await request(server.baseUrl, "/session", {jar}).then(({json}) => { assert.equal(json.user, "alice"); }); await server.close(); }); test("chat creation, send, and retry endpoints work with stubbed model service", async () => { const server = await startTestServer(); const jar = new CookieJar(); await request(server.baseUrl, "/register", {method: "POST", body: {username: "bob", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "bob", password: "password123"}, jar}); const createChat = await request(server.baseUrl, "/chats", { method: "POST", body: {title: "Test chat"}, jar }); assert.equal(createChat.status, 200); const chatId = createChat.json.chat.id; const sendMessage = await request(server.baseUrl, "/chat", { method: "POST", body: {chatId, message: "hello", model: "mistral:latest"}, jar }); assert.equal(sendMessage.status, 200); assert.equal(sendMessage.json.reply, "stub-reply-1"); const messages = await request(server.baseUrl, `/chats/${chatId}/messages`, {jar}); assert.equal(messages.status, 200); assert.equal(messages.json.messages.length, 2); const botMessage = messages.json.messages.find((message) => message.role === "bot"); const retry = await request(server.baseUrl, `/chats/${chatId}/messages/${botMessage.id}/retry`, { method: "POST", body: {model: "mistral:latest"}, jar }); assert.equal(retry.status, 200); assert.equal(retry.json.message.content, "stub-reply-2"); assert.equal(retry.json.message.retryVariants.length, 2); await server.close(); }); test("roleplay start keeps the session and roleplay messages work", async () => { const server = await startTestServer(); const jar = new CookieJar(); await request(server.baseUrl, "/register", {method: "POST", body: {username: "roleplayer", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "roleplayer", password: "password123"}, jar}); const persona = await request(server.baseUrl, "/personas", { method: "POST", body: {personaType: "assistant", name: "Guide", details: "Patient and direct"}, jar }); assert.equal(persona.status, 200); const started = await request(server.baseUrl, "/roleplays/start", { method: "POST", body: {assistantPersonaId: persona.json.persona.id, scenarioPrompt: "A quiet station.", model: "mistral:latest"}, jar }); assert.equal(started.status, 200); assert.equal(started.json.generatedInitialMessage, true); assert.ok(started.json.chat.id); const session = await request(server.baseUrl, "/session", {jar}); assert.equal(session.status, 200); assert.equal(session.json.user, "roleplayer"); const sendMessage = await request(server.baseUrl, "/chat", { method: "POST", body: {chatId: started.json.chat.id, message: "I step closer.", model: "mistral:latest"}, jar }); assert.equal(sendMessage.status, 200); assert.equal(sendMessage.json.reply, "stub-reply-2"); await server.close(); }); test("roleplay start degrades to an open chat when opener generation fails", async () => { const modelService = { async generateReply() { throw new Error("Model backend timed out."); }, async streamReply() { return "unused"; }, async listModels() { return {models: [{name: "mistral:latest"}]}; }, async checkHealth() { return {ok: false, checkedAt: new Date().toISOString(), error: "Model backend timed out."}; }, mapError(error) { return {status: 504, body: {error: error.message, code: "MODEL_TIMEOUT"}}; } }; const server = await startTestServer({modelService}); const jar = new CookieJar(); await request(server.baseUrl, "/register", {method: "POST", body: {username: "degraded", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "degraded", password: "password123"}, jar}); const persona = await request(server.baseUrl, "/personas", { method: "POST", body: {personaType: "assistant", name: "Guide", details: "Patient and direct"}, jar }); const started = await request(server.baseUrl, "/roleplays/start", { method: "POST", body: {assistantPersonaId: persona.json.persona.id, model: "mistral:latest"}, jar }); assert.equal(started.status, 200); assert.equal(started.json.degraded, true); assert.equal(started.json.generatedInitialMessage, false); assert.ok(started.json.chat.id); const session = await request(server.baseUrl, "/session", {jar}); assert.equal(session.json.user, "degraded"); await server.close(); }); test("persona publish and collect flow works across accounts", async () => { const server = await startTestServer(); const creatorJar = new CookieJar(); const collectorJar = new CookieJar(); await request(server.baseUrl, "/register", {method: "POST", body: {username: "creator", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "creator", password: "password123"}, jar: creatorJar}); const persona = await request(server.baseUrl, "/personas", { method: "POST", body: {personaType: "assistant", name: "Agent", details: "Helpful"}, jar: creatorJar }); assert.equal(persona.status, 200); const publish = await request(server.baseUrl, `/personas/${persona.json.persona.id}/publish`, { method: "POST", body: {}, jar: creatorJar }); assert.equal(publish.status, 200); await request(server.baseUrl, "/register", {method: "POST", body: {username: "collector", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "collector", password: "password123"}, jar: collectorJar}); const market = await request(server.baseUrl, "/personas/market", {jar: collectorJar}); assert.equal(market.status, 200); assert.equal(market.json.personas.length, 1); const collect = await request(server.baseUrl, `/personas/market/${market.json.personas[0].id}/collect`, { method: "POST", body: {}, jar: collectorJar }); assert.equal(collect.status, 200); assert.equal(collect.json.persona.name, "Agent"); await server.close(); }); test("chat search, organization, and workspace export work", async () => { const server = await startTestServer(); const jar = new CookieJar(); await request(server.baseUrl, "/register", {method: "POST", body: {username: "searcher", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "searcher", password: "password123"}, jar}); const createChat = await request(server.baseUrl, "/chats", { method: "POST", body: {title: "Notes"}, jar }); const chatId = createChat.json.chat.id; await request(server.baseUrl, "/chat", { method: "POST", body: {chatId, message: "findable keyword", model: "mistral:latest"}, jar }); const search = await request(server.baseUrl, "/chats/search?q=keyword", {jar}); assert.equal(search.status, 200); assert.equal(search.json.chats.length, 1); const organize = await request(server.baseUrl, `/chats/${chatId}/organization`, { method: "PUT", body: {folderName: "Research", isPinned: true, archived: false}, jar }); assert.equal(organize.status, 200); assert.equal(organize.json.chat.folder_name, "Research"); assert.equal(organize.json.chat.is_pinned, 1); const exported = await request(server.baseUrl, "/exports/workspace", {jar}); assert.equal(exported.status, 200); assert.equal(exported.json.workspace.chats.length, 1); await server.close(); }); test("persona clone, version restore, and market feedback endpoints work", async () => { const server = await startTestServer(); const jar = new CookieJar(); await request(server.baseUrl, "/register", {method: "POST", body: {username: "maker", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "maker", password: "password123"}, jar}); const persona = await request(server.baseUrl, "/personas", { method: "POST", body: {personaType: "assistant", name: "Historian", details: "Original"}, jar }); const personaId = persona.json.persona.id; const clone = await request(server.baseUrl, `/personas/${personaId}/clone`, {method: "POST", body: {}, jar}); assert.equal(clone.status, 200); assert.match(clone.json.persona.name, /copy/i); await request(server.baseUrl, `/personas/${personaId}`, { method: "PUT", body: {personaType: "assistant", name: "Historian", details: "Updated"}, jar }); const versions = await request(server.baseUrl, `/personas/${personaId}/versions`, {jar}); assert.equal(versions.status, 200); assert.ok(versions.json.versions.length >= 1); const restoreTarget = versions.json.versions[0]; const restored = await request(server.baseUrl, `/personas/${personaId}/versions/${restoreTarget.id}/restore`, { method: "POST", body: {}, jar }); assert.equal(restored.status, 200); const publish = await request(server.baseUrl, `/personas/${personaId}/publish`, { method: "POST", body: {tags: ["mentor", "history"]}, jar }); assert.equal(publish.status, 200); const marketId = publish.json.persona.id; const favorite = await request(server.baseUrl, `/personas/market/${marketId}/favorite`, { method: "POST", body: {}, jar }); assert.equal(favorite.status, 200); assert.equal(favorite.json.favorite, true); const rating = await request(server.baseUrl, `/personas/market/${marketId}/rate`, { method: "POST", body: {rating: 5}, jar }); assert.equal(rating.status, 200); assert.equal(rating.json.persona.rating_count, 1); await server.close(); }); test("market listing supports persona type filters, sorting, and favorite state", async () => { const server = await startTestServer(); const creatorJar = new CookieJar(); const fanJar = new CookieJar(); const runnerJar = new CookieJar(); await request(server.baseUrl, "/register", {method: "POST", body: {username: "creator2", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "creator2", password: "password123"}, jar: creatorJar}); await request(server.baseUrl, "/register", {method: "POST", body: {username: "fan", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "fan", password: "password123"}, jar: fanJar}); await request(server.baseUrl, "/register", {method: "POST", body: {username: "runner", password: "password123"}}); await request(server.baseUrl, "/login", {method: "POST", body: {username: "runner", password: "password123"}, jar: runnerJar}); const assistantOne = await request(server.baseUrl, "/personas", { method: "POST", body: {personaType: "assistant", name: "Archivist", details: "Keeps records"}, jar: creatorJar }); const assistantTwo = await request(server.baseUrl, "/personas", { method: "POST", body: {personaType: "assistant", name: "Pilot", details: "Fast paced"}, jar: creatorJar }); const userPersona = await request(server.baseUrl, "/personas", { method: "POST", body: {personaType: "user", name: "Scout", details: "Observant"}, jar: creatorJar }); const publishAssistantOne = await request(server.baseUrl, `/personas/${assistantOne.json.persona.id}/publish`, { method: "POST", body: {tags: ["lore"]}, jar: creatorJar }); const publishAssistantTwo = await request(server.baseUrl, `/personas/${assistantTwo.json.persona.id}/publish`, { method: "POST", body: {tags: ["action"]}, jar: creatorJar }); const publishUser = await request(server.baseUrl, `/personas/${userPersona.json.persona.id}/publish`, { method: "POST", body: {tags: ["identity"]}, jar: creatorJar }); const archivistMarketId = publishAssistantOne.json.persona.id; const pilotMarketId = publishAssistantTwo.json.persona.id; const scoutMarketId = publishUser.json.persona.id; await request(server.baseUrl, `/personas/market/${archivistMarketId}/favorite`, { method: "POST", body: {}, jar: fanJar }); await request(server.baseUrl, `/personas/market/${archivistMarketId}/favorite`, { method: "POST", body: {}, jar: runnerJar }); await request(server.baseUrl, `/personas/market/${archivistMarketId}/rate`, { method: "POST", body: {rating: 5}, jar: fanJar }); await request(server.baseUrl, `/personas/market/${pilotMarketId}/collect`, { method: "POST", body: {}, jar: fanJar }); await request(server.baseUrl, `/personas/market/${pilotMarketId}/chat`, { method: "POST", body: {}, jar: runnerJar }); const assistantsNewest = await request(server.baseUrl, "/personas/market?personaType=assistant&sort=newest", {jar: fanJar}); assert.equal(assistantsNewest.status, 200); assert.deepEqual(assistantsNewest.json.personas.map((persona) => persona.name), ["Pilot", "Archivist"]); const assistantsFavorited = await request(server.baseUrl, "/personas/market?personaType=assistant&sort=most_favorited", {jar: fanJar}); assert.equal(assistantsFavorited.status, 200); assert.equal(assistantsFavorited.json.personas[0].name, "Archivist"); assert.equal(assistantsFavorited.json.personas[0].is_favorite, true); assert.equal(assistantsFavorited.json.personas[1].is_favorite, false); const assistantsPopular = await request(server.baseUrl, "/personas/market?personaType=assistant&sort=most_popular", {jar: fanJar}); assert.equal(assistantsPopular.status, 200); assert.equal(assistantsPopular.json.personas[0].name, "Pilot"); const userOnly = await request(server.baseUrl, "/personas/market?personaType=user&sort=best", {jar: fanJar}); assert.equal(userOnly.status, 200); assert.deepEqual(userOnly.json.personas.map((persona) => persona.name), ["Scout"]); assert.equal(userOnly.json.personas[0].id, scoutMarketId); await server.close(); });