char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Linting & Formatting scripts (#26819)92d1bb0c9

char/aliceai-t5-moe
5.2 KiB138 linesraw
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 {
13	return {
14		children: [],
15		content: '',
16		convId: 'c1',
17		id: 'm1',
18		parent: null,
19		role: MessageRole.ASSISTANT,
20		timestamp: 0,
21		type: 'text',
22		...overrides
23	} as DatabaseMessage;
24}
25
26describe('parseToolCalls memoization', () => {
27	it('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).
33		const toolCallsJson = JSON.stringify([
34			{ function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
35		]);
36		const msg = makeMessage({ content: 'hello', toolCalls: toolCallsJson });
37		const sections1 = deriveAgenticSections(msg, [], [], false);
38		const sections2 = deriveAgenticSections(msg, [], [], false);
39
40		expect(sections1).toHaveLength(sections2.length);
41		expect(sections1[0].type).toBe(sections2[0].type);
42	});
43
44	it('does not re-parse JSON on cache hit', () => {
45		const toolCallsJson = JSON.stringify([
46			{ function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
47		]);
48		const msg = makeMessage({ content: 'hello', toolCalls: toolCallsJson });
49		const spy = vi.spyOn(JSON, 'parse');
50
51		deriveAgenticSections(msg, [], [], false);
52		const callsAfterFirst = spy.mock.calls.length;
53
54		deriveAgenticSections(msg, [], [], false);
55		expect(spy.mock.calls.length).toBe(callsAfterFirst);
56
57		spy.mockRestore();
58	});
59
60	it('handles empty/undefined toolCalls without error', () => {
61		const msg = makeMessage({ content: 'hello' });
62		const sections = deriveAgenticSections(msg, [], [], false);
63
64		expect(sections).toHaveLength(1);
65		expect(sections[0].type).toBe(AgenticSectionType.TEXT);
66	});
67
68	it('handles invalid JSON gracefully', () => {
69		const msg = makeMessage({ content: 'hello', toolCalls: '{invalid' });
70		const sections = deriveAgenticSections(msg, [], [], false);
71
72		// Should return just the text section, no tool call sections
73		expect(sections).toHaveLength(1);
74		expect(sections[0].type).toBe(AgenticSectionType.TEXT);
75	});
76});
77
78describe('deriveAgenticSections O(1) tool message lookup', () => {
79	it('matches tool messages to tool calls by toolCallId', () => {
80		const 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		]);
84		const toolMessages = [
85			makeMessage({ content: 'result_1', role: MessageRole.TOOL, toolCallId: 'call_1' }),
86			makeMessage({ content: 'result_2', role: MessageRole.TOOL, toolCallId: 'call_2' })
87		];
88		const msg = makeMessage({ content: 'hello', toolCalls: toolCallsJson });
89		const sections = deriveAgenticSections(msg, toolMessages, [], false);
90		// Expect: TEXT + 2 TOOL_CALL sections
91		const toolCallSections = sections.filter((s) => s.type === AgenticSectionType.TOOL_CALL);
92
93		expect(toolCallSections).toHaveLength(2);
94		expect(toolCallSections[0].toolResult).toBe('result_1');
95		expect(toolCallSections[1].toolResult).toBe('result_2');
96	});
97
98	it('handles missing tool messages (pending calls during streaming)', () => {
99		const toolCallsJson = JSON.stringify([
100			{ function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
101		]);
102		const msg = makeMessage({ content: '', toolCalls: toolCallsJson });
103		const sections = deriveAgenticSections(msg, [], [], true);
104		const toolCallSection = sections.find((s) => s.type === AgenticSectionType.TOOL_CALL_PENDING);
105
106		expect(toolCallSection).toBeDefined();
107		expect(toolCallSection?.content).toBe('');
108	});
109
110	it('scales with many tool calls (no O(n^2) blowup)', () => {
111		const N = 100;
112		const toolCalls = Array.from(
113			{ length: N },
114			(_, i): ApiChatCompletionToolCall => ({
115				function: { arguments: '{}', name: `tool_${i}` },
116				id: `call_${i}`,
117				type: 'function'
118			})
119		);
120		const toolCallsJson = JSON.stringify(toolCalls);
121		const toolMessages = Array.from({ length: N }, (_, i) =>
122			makeMessage({
123				content: `result_${i}`,
124				role: MessageRole.TOOL,
125				toolCallId: `call_${i}`
126			})
127		);
128		const msg = makeMessage({ content: 'hello', toolCalls: toolCallsJson });
129		// If the lookup were still O(n^2), this would be noticeably slow
130		const start = Date.now();
131		const sections = deriveAgenticSections(msg, toolMessages, [], false);
132		const elapsed = Date.now() - start;
133		const toolCallSections = sections.filter((s) => s.type === AgenticSectionType.TOOL_CALL);
134
135		expect(toolCallSections).toHaveLength(N);
136		expect(elapsed).toBeLessThan(100); // Should be fast with O(1) lookup
137	});
138});