import { describe, expect, it, afterEach } from "vitest"; import { ObjectId } from "mongodb"; import { v4 } from "uuid"; import superjson from "superjson"; import { collections } from "$lib/server/database"; import type { Message } from "$lib/types/Message"; import { createTestLocals, createTestUser, createTestConversation, cleanupTestData, } from "./testHelpers"; import { DELETE } from "../../../../routes/api/v2/conversations/[id]/message/[messageId]/+server"; async function parseResponse(res: Response): Promise { return superjson.parse(await res.text()) as T; } /** * Build a simple message tree: * * root (system) * -> msg1 (user) * -> msg2 (assistant) * -> msg3 (user) * -> unrelated (user) -- sibling branch from root */ function buildMessageTree(): { messages: Message[]; rootId: string; msg1Id: string; msg2Id: string; msg3Id: string; unrelatedId: string; } { const rootId = v4(); const msg1Id = v4(); const msg2Id = v4(); const msg3Id = v4(); const unrelatedId = v4(); const root: Message = { id: rootId, from: "system", content: "System prompt", ancestors: [], children: [msg1Id, unrelatedId], }; const msg1: Message = { id: msg1Id, from: "user", content: "Hello", ancestors: [rootId], children: [msg2Id], }; const msg2: Message = { id: msg2Id, from: "assistant", content: "Hi there!", ancestors: [rootId, msg1Id], children: [msg3Id], }; const msg3: Message = { id: msg3Id, from: "user", content: "How are you?", ancestors: [rootId, msg1Id, msg2Id], children: [], }; const unrelated: Message = { id: unrelatedId, from: "user", content: "Unrelated branch", ancestors: [rootId], children: [], }; return { messages: [root, msg1, msg2, msg3, unrelated], rootId, msg1Id, msg2Id, msg3Id, unrelatedId, }; } describe.sequential("DELETE /api/v2/conversations/[id]/message/[messageId]", () => { afterEach(async () => { await cleanupTestData(); }); it("removes target message and its descendants", { timeout: 30000 }, async () => { const { locals } = await createTestUser(); const tree = buildMessageTree(); const conv = await createTestConversation(locals, { messages: tree.messages, rootMessageId: tree.rootId, }); // Delete msg1 -> should also remove msg2 and msg3 (descendants) const res = await DELETE({ locals, params: { id: conv._id.toString(), messageId: tree.msg1Id }, } as never); expect(res.status).toBe(200); const data = await parseResponse<{ success: boolean }>(res); expect(data.success).toBe(true); const updated = await collections.conversations.findOne({ _id: conv._id }); expect(updated).not.toBeNull(); const remainingIds = (updated?.messages ?? []).map((m) => m.id); // msg1, msg2, msg3 should all be removed expect(remainingIds).not.toContain(tree.msg1Id); expect(remainingIds).not.toContain(tree.msg2Id); expect(remainingIds).not.toContain(tree.msg3Id); // root and unrelated should remain expect(remainingIds).toContain(tree.rootId); expect(remainingIds).toContain(tree.unrelatedId); }); it("cleans up children arrays referencing deleted message", async () => { const { locals } = await createTestUser(); const tree = buildMessageTree(); const conv = await createTestConversation(locals, { messages: tree.messages, rootMessageId: tree.rootId, }); // Delete msg1 -> root's children should no longer include msg1Id await DELETE({ locals, params: { id: conv._id.toString(), messageId: tree.msg1Id }, } as never); const updated = await collections.conversations.findOne({ _id: conv._id }); const rootMsg = updated?.messages.find((m) => m.id === tree.rootId); expect(rootMsg).toBeDefined(); expect(rootMsg?.children).not.toContain(tree.msg1Id); // The unrelated sibling should still be in root's children expect(rootMsg?.children).toContain(tree.unrelatedId); }); it("throws 404 for non-existent message", async () => { const { locals } = await createTestUser(); const tree = buildMessageTree(); const conv = await createTestConversation(locals, { messages: tree.messages, rootMessageId: tree.rootId, }); const fakeMessageId = v4(); try { await DELETE({ locals, params: { id: conv._id.toString(), messageId: fakeMessageId }, } as never); expect.fail("Should have thrown"); } catch (e: unknown) { expect((e as { status: number }).status).toBe(404); } }); it("throws 401 for unauthenticated request", async () => { const locals = createTestLocals({ sessionId: undefined, user: undefined }); try { await DELETE({ locals, params: { id: new ObjectId().toString(), messageId: v4() }, } as never); expect.fail("Should have thrown"); } catch (e: unknown) { expect((e as { status: number }).status).toBe(401); } }); it("preserves unrelated messages in the tree", async () => { const { locals } = await createTestUser(); const tree = buildMessageTree(); const conv = await createTestConversation(locals, { messages: tree.messages, rootMessageId: tree.rootId, }); // Delete msg3 (a leaf) -> should only remove msg3, everything else stays const res = await DELETE({ locals, params: { id: conv._id.toString(), messageId: tree.msg3Id }, } as never); expect(res.status).toBe(200); const updated = await collections.conversations.findOne({ _id: conv._id }); const remainingIds = (updated?.messages ?? []).map((m) => m.id); expect(remainingIds).toHaveLength(4); expect(remainingIds).toContain(tree.rootId); expect(remainingIds).toContain(tree.msg1Id); expect(remainingIds).toContain(tree.msg2Id); expect(remainingIds).toContain(tree.unrelatedId); expect(remainingIds).not.toContain(tree.msg3Id); // msg2's children should no longer include msg3Id const msg2 = updated?.messages.find((m) => m.id === tree.msg2Id); expect(msg2?.children).not.toContain(tree.msg3Id); }); });