char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
92d1bb0c9
char/aliceai-t5-moe
1// Tests for the memoized parseToolCalls and O(1) tool message lookup in 2// deriveAgenticSections. These were added to prevent regressions where 3// streaming text tokens trigger redundant JSON.parse calls on unchanged 4// tool call data. 5 6import { AgenticSectionType , MessageRole } from '$lib/enums' ; 7import type { ApiChatCompletionToolCall } from '$lib/types/api' ; 8import type { DatabaseMessage } from '$lib/types/database' ; 9import { deriveAgenticSections } from '$lib/utils/agentic' ; 10import { describe , expect , it , vi } from 'vitest' ; 11 12function makeMessage ( overrides :Partial < DatabaseMessage >) :DatabaseMessage { 13return { 14children :[], 15content :'' , 16convId :'c1' , 17id :'m1' , 18parent :null , 19role :MessageRole . ASSISTANT , 20timestamp :0 , 21type :'text' , 22 ...overrides 23} as DatabaseMessage ; 24} 25 26describe ( 'parseToolCalls memoization' , () => { 27it ( 'returns the same array reference for the same JSON string' , () => { 28// parseToolCalls is not exported, but deriveAgenticSections uses it 29// internally. We verify memoization through behavior: calling 30// deriveAgenticSections twice with the same toolCalls should not 31// re-parse (which we verify by checking the returned sections 32// are equivalent). 33const toolCallsJson = JSON . stringify ([ 34{ function :{ arguments :'{}' , name :'test' }, id :'call_1' , type :'function' } 35]); 36const msg = makeMessage ({ content :'hello' , toolCalls :toolCallsJson }); 37const sections1 = deriveAgenticSections ( msg , [], [], false ); 38const sections2 = deriveAgenticSections ( msg , [], [], false ); 39 40expect ( sections1 ). toHaveLength ( sections2 . length ); 41expect ( sections1 [ 0 ]. type ). toBe ( sections2 [ 0 ]. type ); 42}); 43 44it ( 'does not re-parse JSON on cache hit' , () => { 45const toolCallsJson = JSON . stringify ([ 46{ function :{ arguments :'{}' , name :'test' }, id :'call_1' , type :'function' } 47]); 48const msg = makeMessage ({ content :'hello' , toolCalls :toolCallsJson }); 49const spy = vi . spyOn ( JSON , 'parse' ); 50 51deriveAgenticSections ( msg , [], [], false ); 52const callsAfterFirst = spy . mock . calls . length ; 53 54deriveAgenticSections ( msg , [], [], false ); 55expect ( spy . mock . calls . length ). toBe ( callsAfterFirst ); 56 57spy . mockRestore (); 58}); 59 60it ( 'handles empty/undefined toolCalls without error' , () => { 61const msg = makeMessage ({ content :'hello' }); 62const sections = deriveAgenticSections ( msg , [], [], false ); 63 64expect ( sections ). toHaveLength ( 1 ); 65expect ( sections [ 0 ]. type ). toBe ( AgenticSectionType . TEXT ); 66}); 67 68it ( 'handles invalid JSON gracefully' , () => { 69const msg = makeMessage ({ content :'hello' , toolCalls :'{invalid' }); 70const sections = deriveAgenticSections ( msg , [], [], false ); 71 72// Should return just the text section, no tool call sections 73expect ( sections ). toHaveLength ( 1 ); 74expect ( sections [ 0 ]. type ). toBe ( AgenticSectionType . TEXT ); 75}); 76}); 77 78describe ( 'deriveAgenticSections O(1) tool message lookup' , () => { 79it ( 'matches tool messages to tool calls by toolCallId' , () => { 80const toolCallsJson = JSON . stringify ([ 81{ function :{ arguments :'{}' , name :'test_1' }, id :'call_1' , type :'function' }, 82{ function :{ arguments :'{}' , name :'test_2' }, id :'call_2' , type :'function' } 83]); 84const toolMessages = [ 85makeMessage ({ content :'result_1' , role :MessageRole . TOOL , toolCallId :'call_1' }), 86makeMessage ({ content :'result_2' , role :MessageRole . TOOL , toolCallId :'call_2' }) 87]; 88const msg = makeMessage ({ content :'hello' , toolCalls :toolCallsJson }); 89const sections = deriveAgenticSections ( msg , toolMessages , [], false ); 90// Expect: TEXT + 2 TOOL_CALL sections 91const toolCallSections = sections . filter (( s ) => s . type === AgenticSectionType . TOOL_CALL ); 92 93expect ( toolCallSections ). toHaveLength ( 2 ); 94expect ( toolCallSections [ 0 ]. toolResult ). toBe ( 'result_1' ); 95expect ( toolCallSections [ 1 ]. toolResult ). toBe ( 'result_2' ); 96}); 97 98it ( 'handles missing tool messages (pending calls during streaming)' , () => { 99const toolCallsJson = JSON . stringify ([ 100{ function :{ arguments :'{}' , name :'test' }, id :'call_1' , type :'function' } 101]); 102const msg = makeMessage ({ content :'' , toolCalls :toolCallsJson }); 103const sections = deriveAgenticSections ( msg , [], [], true ); 104const toolCallSection = sections . find (( s ) => s . type === AgenticSectionType . TOOL_CALL_PENDING ); 105 106expect ( toolCallSection ). toBeDefined (); 107expect ( toolCallSection ?. content ). toBe ( '' ); 108}); 109 110it ( 'scales with many tool calls (no O(n^2) blowup)' , () => { 111const N = 100 ; 112const toolCalls = Array . from ( 113{ length :N }, 114( _ , i ) :ApiChatCompletionToolCall => ({ 115function :{ arguments :'{}' , name :`tool_ ${ i } ` }, 116id :`call_ ${ i } ` , 117type :'function' 118}) 119); 120const toolCallsJson = JSON . stringify ( toolCalls ); 121const toolMessages = Array . from ({ length :N }, ( _ , i ) => 122makeMessage ({ 123content :`result_ ${ i } ` , 124role :MessageRole . TOOL , 125toolCallId :`call_ ${ i } ` 126}) 127); 128const msg = makeMessage ({ content :'hello' , toolCalls :toolCallsJson }); 129// If the lookup were still O(n^2), this would be noticeably slow 130const start = Date . now (); 131const sections = deriveAgenticSections ( msg , toolMessages , [], false ); 132const elapsed = Date . now () - start ; 133const toolCallSections = sections . filter (( s ) => s . type === AgenticSectionType . TOOL_CALL ); 134 135expect ( toolCallSections ). toHaveLength ( N ); 136expect ( elapsed ). toBeLessThan ( 100 ); // Should be fast with O(1) lookup 137}); 138});