Felipe97/llama-cpp-compiled
01.2k
1import { beforeEach, describe, expect, it, vi } from 'vitest';2 3vi.mock('$lib/services/database.service', () => ({4 DatabaseService: { getConversationsWithMessages: vi.fn() }5}));6 7import { MessageRole, MessageType } from '$lib/enums';8import { ConversationTransferService } from '$lib/services/conversation-transfer.service';9import { DatabaseService } from '$lib/services/database.service';10import { conversationsStore } from '$lib/stores/conversations/index.svelte';11import type { DatabaseConversation, DatabaseMessage } from '$lib/types/database';12import { filterByLeafNodeId } from '$lib/utils/branching';13 14/**15 * Reproduces the exported-conversation bug:16 *17 * A conversation created in the current page session keeps `currNode: ''` in the18 * sidebar list, because that list is only loaded at init while IndexedDB is stamped19 * on every message insert.20 *21 * Exporting from the cached record resulted in no branch pointer, and importing22 * the file showed every branch at once.23 */24 25const fetchMock = vi.mocked(DatabaseService.getConversationsWithMessages);26 27beforeEach(() => {28 fetchMock.mockReset();29});30 31const CONV_ID = 'c1';32 33function message(34 id: string,35 parent: string | null,36 timestamp: number,37 role: MessageRole,38 type: MessageType = MessageType.TEXT39): DatabaseMessage {40 return {41 children: [],42 content: id,43 convId: CONV_ID,44 id,45 parent,46 role,47 timestamp,48 toolCalls: '',49 type50 } as DatabaseMessage;51}52 53/** root -> u1 -> a1 -> { u2a -> a2a (older) | u2b -> a2b (newer) } */54function branchedMessages(): DatabaseMessage[] {55 const messages = [56 message('root', null, 10, MessageRole.USER, MessageType.ROOT),57 message('u1', 'root', 20, MessageRole.USER),58 message('a1', 'u1', 30, MessageRole.ASSISTANT),59 message('u2a', 'a1', 40, MessageRole.USER),60 message('a2a', 'u2a', 50, MessageRole.ASSISTANT),61 message('u2b', 'a1', 60, MessageRole.USER),62 message('a2b', 'u2b', 70, MessageRole.ASSISTANT)63 ];64 65 for (const m of messages) {66 m.children = messages.filter((c) => c.parent === m.id).map((c) => c.id);67 }68 69 return messages;70}71 72/** A second conversation with a single linear path: root -> u1 -> a1. */73function linearMessages(convId: string): DatabaseMessage[] {74 return [75 { ...message('root', null, 10, MessageRole.USER, MessageType.ROOT), children: ['u1'], convId },76 { ...message('u1', 'root', 20, MessageRole.USER), children: ['a1'], convId },77 { ...message('a1', 'u1', 30, MessageRole.ASSISTANT), convId }78 ];79}80 81function conversation(currNode: string, id: string = CONV_ID): DatabaseConversation {82 return { currNode, id, lastModified: 100, name: `Chat ${id}` };83}84 85/** Mirrors `conversationsStore.loadConversation` */86function displayedIds(imported: { conv: DatabaseConversation; messages: DatabaseMessage[] }) {87 if (imported.conv.currNode) {88 return filterByLeafNodeId(imported.messages, imported.conv.currNode, false).map((m) => m.id);89 }90 91 return imported.messages.map((m) => m.id);92}93 94/** Export then re-import */95function roundTrip(conv: DatabaseConversation) {96 const jsonl = ConversationTransferService.serializeSessionToJsonl({97 conv,98 messages: branchedMessages()99 });100 const [imported] = ConversationTransferService.parseSessionsJsonl(jsonl);101 102 return { imported, sessionLine: JSON.parse(jsonl.split('\n')[0]) };103}104 105describe('conversation export source', () => {106 it('reads the database record rather than the stale sidebar list', async () => {107 conversationsStore.conversations = [conversation('')];108 109 fetchMock.mockResolvedValue(110 new Map([[CONV_ID, { conv: conversation('a2a'), messages: branchedMessages() }]])111 );112 113 const [exported] = await conversationsStore.getConversationsForExport([CONV_ID]);114 115 expect(exported.conv.currNode).toBe('a2a');116 expect(conversationsStore.conversations[0].currNode).toBe('');117 });118 119 it('reads every selected conversation from the database on bulk export', async () => {120 conversationsStore.conversations = [conversation(''), conversation('', 'c2')];121 conversationsStore.activeConversation = conversation('');122 123 fetchMock.mockResolvedValue(124 new Map([125 ['c2', { conv: conversation('a1', 'c2'), messages: linearMessages('c2') }],126 [CONV_ID, { conv: conversation('a2a'), messages: branchedMessages() }]127 ])128 );129 130 const archive = vi131 .spyOn(ConversationTransferService, 'downloadConversationsArchive')132 .mockImplementation(() => {});133 134 await conversationsStore.bulkExportConversations([CONV_ID, 'c2']);135 136 expect(fetchMock).toHaveBeenCalledWith([CONV_ID, 'c2']);137 expect(archive).toHaveBeenCalledTimes(1);138 139 const payload = archive.mock.calls[0][0];140 141 expect(payload.map((entry) => entry.conv.id)).toEqual([CONV_ID, 'c2']);142 // Each entry carries its own database currNode.143 expect(payload.map((entry) => entry.conv.currNode)).toEqual(['a2a', 'a1']);144 expect(payload[1].messages.map((m: DatabaseMessage) => m.id)).toEqual(['root', 'u1', 'a1']);145 146 archive.mockRestore();147 });148});149 150describe('exported conversation branch pointer', () => {151 it('carries the database currNode, so the import restores the current branch', () => {152 // The user regenerated to create a2b, then switched back to the a2a branch,153 // so the stored leaf is NOT the newest message.154 const { imported, sessionLine } = roundTrip(conversation('a2a'));155 156 expect(sessionLine.currNode).toBe('a2a');157 expect(displayedIds(imported)).toEqual(['u1', 'a1', 'u2a', 'a2a']);158 expect(imported.messages.map((m: DatabaseMessage) => m.id).sort()).toEqual([159 'a1',160 'a2a',161 'a2b',162 'root',163 'u1',164 'u2a',165 'u2b'166 ]);167 });168 169 it('shows every branch on import when the cache entry exported an empty currNode', () => {170 const { imported, sessionLine } = roundTrip(conversation(''));171 172 expect(sessionLine.currNode).toBe('');173 expect(displayedIds(imported)).toEqual(['root', 'u1', 'a1', 'u2a', 'a2a', 'u2b', 'a2b']);174 });175});176 