import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import sensible from "@fastify/sensible"; const serverRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const databaseDir = mkdtempSync(join(tmpdir(), "sybil-chat-forks-")); process.env.DATABASE_URL = `file:${join(databaseDir, "test.db")}`; process.env.OPENAI_API_KEY = ""; delete process.env.ADMIN_TOKEN; execFileSync(process.execPath, [join(serverRoot, "node_modules/prisma/build/index.js"), "migrate", "deploy"], { cwd: serverRoot, env: { ...process.env, PRISMA_HIDE_UPDATE_MESSAGE: "1" }, stdio: "pipe", }); const [{ clearActiveChatStream, registerActiveChatStream, registerRoutes }, { prisma }] = await Promise.all([ import("../src/routes.js"), import("../src/db.js"), ]); const app = Fastify({ logger: false }); await app.register(sensible); await registerRoutes(app); await app.ready(); test.after(async () => { await app.close(); await prisma.$disconnect(); rmSync(databaseDir, { recursive: true, force: true }); }); test("forks copy bounded history and keep every child grouped under the root", async () => { const timestamp = new Date("2026-08-16T12:00:00.000Z"); const source = await prisma.chat.create({ data: { title: "Planning session", initiatedProvider: "openai", initiatedModel: "gpt-4.1-mini", lastUsedProvider: "anthropic", lastUsedModel: "claude-sonnet-4-20250514", additionalSystemPrompt: "Keep answers concise.", enabledTools: ["web_search"], messages: { create: [ { id: "source-message-0001", createdAt: timestamp, role: "user", content: "Plan a trip", metadata: { clientRequestId: "source-request", attachments: [{ kind: "text", filename: "notes.txt" }], }, }, { id: "source-message-0002", createdAt: timestamp, role: "assistant", content: "First assistant response", metadata: { clientRequestId: "source-request", citations: ["https://example.com"] }, }, { id: "source-message-0003", createdAt: timestamp, role: "user", content: "Only visible in a whole-chat fork", }, ], }, calls: { create: { provider: "openai", model: "gpt-4.1-mini", request: { input: "Plan a trip" }, }, }, }, include: { messages: { orderBy: [{ createdAt: "asc" }, { id: "asc" }] } }, }); const forkResponse = await app.inject({ method: "POST", url: `/v1/chats/${source.id}/fork`, payload: { messageId: source.messages[1].id }, }); assert.equal(forkResponse.statusCode, 200, forkResponse.body); const forkSummary = forkResponse.json().chat; assert.equal(forkSummary.parentChatId, source.id); assert.equal(forkSummary.title, "Fork of 'First assistant response'"); assert.equal(forkSummary.titleGenerationPending, true); assert.equal(forkSummary.starred, false); const detailResponse = await app.inject({ method: "GET", url: `/v1/chats/${forkSummary.id}` }); assert.equal(detailResponse.statusCode, 200, detailResponse.body); const fork = detailResponse.json().chat; assert.deepEqual( fork.messages.map((message: any) => [message.role, message.content]), [ ["user", "Plan a trip"], ["assistant", "First assistant response"], ] ); assert.notEqual(fork.messages[0].id, source.messages[0].id); assert.equal(fork.messages[0].createdAt, timestamp.toISOString()); assert.deepEqual(fork.messages[0].metadata, { attachments: [{ kind: "text", filename: "notes.txt" }], }); assert.deepEqual(fork.messages[1].metadata, { citations: ["https://example.com"] }); assert.equal(fork.initiatedProvider, "openai"); assert.equal(fork.initiatedModel, "gpt-4.1-mini"); assert.equal(fork.lastUsedProvider, "anthropic"); assert.equal(fork.lastUsedModel, "claude-sonnet-4-20250514"); assert.equal(fork.additionalSystemPrompt, "Keep answers concise."); assert.deepEqual(fork.enabledTools, ["web_search"]); assert.deepEqual(fork.calls, []); const childForkResponse = await app.inject({ method: "POST", url: `/v1/chats/${forkSummary.id}/fork`, payload: {}, }); assert.equal(childForkResponse.statusCode, 200, childForkResponse.body); const childFork = childForkResponse.json().chat; assert.equal(childFork.parentChatId, source.id); assert.equal(childFork.title, "Fork of Fork of 'First assistant response'"); assert.equal(childFork.titleGenerationPending, true); const wholeForkResponse = await app.inject({ method: "POST", url: `/v1/chats/${source.id}/fork`, payload: {}, }); assert.equal(wholeForkResponse.statusCode, 200, wholeForkResponse.body); const wholeFork = wholeForkResponse.json().chat; assert.equal(wholeFork.parentChatId, source.id); assert.equal(wholeFork.title, "Fork of Planning session"); const wholeForkDetail = await app.inject({ method: "GET", url: `/v1/chats/${wholeFork.id}` }); assert.equal(wholeForkDetail.statusCode, 200, wholeForkDetail.body); assert.deepEqual( wholeForkDetail.json().chat.messages.map((message: any) => message.content), ["Plan a trip", "First assistant response", "Only visible in a whole-chat fork"] ); assert.equal(wholeForkDetail.json().chat.messages[2].metadata, null); const suggestedTitleResponse = await app.inject({ method: "POST", url: "/v1/chats/title/suggest", payload: { chatId: wholeFork.id, content: "Compare rail and air options" }, }); assert.equal(suggestedTitleResponse.statusCode, 200, suggestedTitleResponse.body); assert.equal(suggestedTitleResponse.json().chat.title, "Compare rail and air"); assert.equal(suggestedTitleResponse.json().chat.titleGenerationPending, false); const repeatedTitleResponse = await app.inject({ method: "POST", url: "/v1/chats/title/suggest", payload: { chatId: wholeFork.id, content: "This must not overwrite the generated title" }, }); assert.equal(repeatedTitleResponse.statusCode, 200, repeatedTitleResponse.body); assert.equal(repeatedTitleResponse.json().chat.title, "Compare rail and air"); const sourceAfterForks = await prisma.chat.findUniqueOrThrow({ where: { id: source.id }, include: { messages: true, calls: true }, }); assert.equal(sourceAfterForks.messages.length, 3); assert.equal(sourceAfterForks.calls.length, 1); const manualTitleResponse = await app.inject({ method: "PATCH", url: `/v1/chats/${forkSummary.id}`, payload: { title: "Manual branch title" }, }); assert.equal(manualTitleResponse.statusCode, 200, manualTitleResponse.body); assert.equal(manualTitleResponse.json().chat.titleGenerationPending, false); const activeForkStream = registerActiveChatStream(wholeFork.id); try { const deleteActiveRootResponse = await app.inject({ method: "DELETE", url: `/v1/chats/${source.id}` }); assert.equal(deleteActiveRootResponse.statusCode, 409, deleteActiveRootResponse.body); assert.equal(deleteActiveRootResponse.json().message, "chat or fork has an active stream"); const deleteActiveForkResponse = await app.inject({ method: "DELETE", url: `/v1/chats/${wholeFork.id}` }); assert.equal(deleteActiveForkResponse.statusCode, 409, deleteActiveForkResponse.body); } finally { clearActiveChatStream(wholeFork.id, activeForkStream); } const deleteChildResponse = await app.inject({ method: "DELETE", url: `/v1/chats/${childFork.id}` }); assert.equal(deleteChildResponse.statusCode, 200, deleteChildResponse.body); assert.equal(await prisma.chat.count({ where: { id: { in: [source.id, forkSummary.id, wholeFork.id] } } }), 3); const unrelated = await prisma.chat.create({ data: { messages: { create: { role: "assistant", content: "Unrelated response" } }, }, include: { messages: true }, }); const countBeforeInvalidForks = await prisma.chat.count(); const wrongChatResponse = await app.inject({ method: "POST", url: `/v1/chats/${source.id}/fork`, payload: { messageId: unrelated.messages[0].id }, }); assert.equal(wrongChatResponse.statusCode, 404, wrongChatResponse.body); assert.equal(wrongChatResponse.json().message, "message not found in chat"); const nonAssistantResponse = await app.inject({ method: "POST", url: `/v1/chats/${source.id}/fork`, payload: { messageId: source.messages[0].id }, }); assert.equal(nonAssistantResponse.statusCode, 400, nonAssistantResponse.body); assert.equal(nonAssistantResponse.json().message, "fork message must be an assistant response"); assert.equal(await prisma.chat.count(), countBeforeInvalidForks); const concurrentTarget = await prisma.chat.create({ data: { title: "Concurrent delete" } }); const concurrentDeleteResponses = await Promise.all([ app.inject({ method: "DELETE", url: `/v1/chats/${concurrentTarget.id}` }), app.inject({ method: "DELETE", url: `/v1/chats/${concurrentTarget.id}` }), ]); assert.equal(concurrentDeleteResponses.filter((response) => response.statusCode === 200).length, 1); assert.equal(concurrentDeleteResponses.some((response) => response.statusCode >= 500), false); const concurrentRoot = await prisma.chat.create({ data: { title: "Concurrent family delete" } }); const concurrentChildResponse = await app.inject({ method: "POST", url: `/v1/chats/${concurrentRoot.id}/fork`, payload: {}, }); assert.equal(concurrentChildResponse.statusCode, 200, concurrentChildResponse.body); const concurrentChild = concurrentChildResponse.json().chat; const concurrentFamilyDeleteResponses = await Promise.all([ app.inject({ method: "DELETE", url: `/v1/chats/${concurrentRoot.id}` }), app.inject({ method: "DELETE", url: `/v1/chats/${concurrentChild.id}` }), ]); assert.equal(concurrentFamilyDeleteResponses.some((response) => response.statusCode >= 500), false); assert.equal(concurrentFamilyDeleteResponses.some((response) => response.statusCode === 200), true); await prisma.chat.deleteMany({ where: { id: concurrentRoot.id } }); const deleteRootResponse = await app.inject({ method: "DELETE", url: `/v1/chats/${source.id}` }); assert.equal(deleteRootResponse.statusCode, 200, deleteRootResponse.body); assert.equal(await prisma.chat.count({ where: { id: { in: [source.id, forkSummary.id, childFork.id, wholeFork.id] } } }), 0); assert.equal(await prisma.chat.count({ where: { id: unrelated.id } }), 1); });