Team Ai
Modelpublic

Felipe97/llama-cpp-compiled

sourceHugging Faceupdated 21d agoView on Hugging Face
0likes1.2kdownloads
conversation-export.test.ts176 linesDownload Raw Back to unit
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