char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
1863ac033
char/aliceai-t5-moe
1import { beforeEach , describe , expect , it , vi } from 'vitest' ; 2 3vi . mock ( '$lib/services/database.service' , () => ({ 4DatabaseService :{ 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 the 18* sidebar list, because that list is only loaded at init while IndexedDB is stamped 19* on every message insert. 20* 21* Exporting from the cached record resulted in no branch pointer, and importing 22* the file showed every branch at once. 23*/ 24 25const fetchMock = vi . mocked ( DatabaseService . getConversationsWithMessages ); 26 27beforeEach (() => { 28fetchMock . mockReset (); 29}); 30 31const CONV_ID = 'c1' ; 32 33function message ( 34id :string , 35parent :string | null , 36timestamp :number , 37role :MessageRole , 38type :MessageType = MessageType . TEXT 39) :DatabaseMessage { 40return { 41children :[], 42content :id , 43convId :CONV_ID , 44 id, 45 parent, 46 role, 47 timestamp, 48toolCalls :'' , 49 type 50} as DatabaseMessage ; 51} 52 53/** root -> u1 -> a1 -> { u2a -> a2a (older) | u2b -> a2b (newer) } */ 54function branchedMessages () :DatabaseMessage [] { 55const messages = [ 56message ( 'root' , null , 10 , MessageRole . USER , MessageType . ROOT ), 57message ( 'u1' , 'root' , 20 , MessageRole . USER ), 58message ( 'a1' , 'u1' , 30 , MessageRole . ASSISTANT ), 59message ( 'u2a' , 'a1' , 40 , MessageRole . USER ), 60message ( 'a2a' , 'u2a' , 50 , MessageRole . ASSISTANT ), 61message ( 'u2b' , 'a1' , 60 , MessageRole . USER ), 62message ( 'a2b' , 'u2b' , 70 , MessageRole . ASSISTANT ) 63]; 64 65for ( const m of messages ) { 66m . children = messages . filter (( c ) => c . parent === m . id ). map (( c ) => c . id ); 67} 68 69return messages ; 70} 71 72/** A second conversation with a single linear path: root -> u1 -> a1. */ 73function linearMessages ( convId :string ) :DatabaseMessage [] { 74return [ 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 { 82return { currNode, id, lastModified :100 , name :`Chat ${ id } ` }; 83} 84 85/** Mirrors `conversationsStore.loadConversation` */ 86function displayedIds ( imported :{ conv :DatabaseConversation ; messages :DatabaseMessage [] }) { 87if ( imported . conv . currNode ) { 88return filterByLeafNodeId ( imported . messages , imported . conv . currNode , false ). map (( m ) => m . id ); 89} 90 91return imported . messages . map (( m ) => m . id ); 92} 93 94/** Export then re-import */ 95function roundTrip ( conv :DatabaseConversation ) { 96const jsonl = ConversationTransferService . serializeSessionToJsonl ({ 97 conv, 98messages :branchedMessages () 99}); 100const [ imported ] = ConversationTransferService . parseSessionsJsonl ( jsonl ); 101 102return { imported, sessionLine :JSON . parse ( jsonl . split ( '\n' )[ 0 ]) }; 103} 104 105describe ( 'conversation export source' , () => { 106it ( 'reads the database record rather than the stale sidebar list' , async () => { 107conversationsStore . conversations = [ conversation ( '' )]; 108 109fetchMock . mockResolvedValue ( 110new Map ([[ CONV_ID , { conv :conversation ( 'a2a' ), messages :branchedMessages () }]]) 111); 112 113const [ exported ] = await conversationsStore . getConversationsForExport ([ CONV_ID ]); 114 115expect ( exported . conv . currNode ). toBe ( 'a2a' ); 116expect ( conversationsStore . conversations [ 0 ]. currNode ). toBe ( '' ); 117}); 118 119it ( 'reads every selected conversation from the database on bulk export' , async () => { 120conversationsStore . conversations = [ conversation ( '' ), conversation ( '' , 'c2' )]; 121conversationsStore . activeConversation = conversation ( '' ); 122 123fetchMock . mockResolvedValue ( 124new Map ([ 125[ 'c2' , { conv :conversation ( 'a1' , 'c2' ), messages :linearMessages ( 'c2' ) }], 126[ CONV_ID , { conv :conversation ( 'a2a' ), messages :branchedMessages () }] 127]) 128); 129 130const archive = vi 131. spyOn ( ConversationTransferService , 'downloadConversationsArchive' ) 132. mockImplementation (() => {}); 133 134await conversationsStore . bulkExportConversations ([ CONV_ID , 'c2' ]); 135 136expect ( fetchMock ). toHaveBeenCalledWith ([ CONV_ID , 'c2' ]); 137expect ( archive ). toHaveBeenCalledTimes ( 1 ); 138 139const payload = archive . mock . calls [ 0 ][ 0 ]; 140 141expect ( payload . map (( entry ) => entry . conv . id )). toEqual ([ CONV_ID , 'c2' ]); 142// Each entry carries its own database currNode. 143expect ( payload . map (( entry ) => entry . conv . currNode )). toEqual ([ 'a2a' , 'a1' ]); 144expect ( payload [ 1 ]. messages . map (( m :DatabaseMessage ) => m . id )). toEqual ([ 'root' , 'u1' , 'a1' ]); 145 146archive . mockRestore (); 147}); 148}); 149 150describe ( 'exported conversation branch pointer' , () => { 151it ( '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. 154const { imported, sessionLine} = roundTrip ( conversation ( 'a2a' )); 155 156expect ( sessionLine . currNode ). toBe ( 'a2a' ); 157expect ( displayedIds ( imported )). toEqual ([ 'u1' , 'a1' , 'u2a' , 'a2a' ]); 158expect ( 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 169it ( 'shows every branch on import when the cache entry exported an empty currNode' , () => { 170const { imported, sessionLine} = roundTrip ( conversation ( '' )); 171 172expect ( sessionLine . currNode ). toBe ( '' ); 173expect ( displayedIds ( imported )). toEqual ([ 'root' , 'u1' , 'a1' , 'u2a' , 'a2a' , 'u2b' , 'a2b' ]); 174}); 175});