Felipe97/llama-cpp-compiled
01.2k
1import { MessageRole } from '$lib/enums';2import type { DatabaseMessage } from '$lib/types/database';3import { deriveAgenticSections } from '$lib/utils/agentic';4import { describe, expect, it } from 'vitest';5 6function makeAssistant(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {7 return {8 children: [],9 content: overrides.content ?? '',10 convId: 'conv-1',11 id: overrides.id ?? 'ast-1',12 parent: null,13 role: MessageRole.ASSISTANT,14 timestamp: Date.now(),15 type: 'text',16 ...overrides17 } as DatabaseMessage;18}19 20// Mirrors the filter inside ChatService.convertDbMessageToApiChatMessageData:21// a partial tool call captured mid-stream must not survive into the next request22// payload. The fix in chatStore.savePartialResponseIfNeeded clears toolCalls to ''23// on Stop/Send immediately, mirroring what the agentic flow already does in24// onAssistantTurnComplete(...undefined).25function buildApiToolCalls(message: DatabaseMessage): unknown[] | undefined {26 if (!message.toolCalls) return undefined;27 28 try {29 const parsed = JSON.parse(message.toolCalls);30 31 return Array.isArray(parsed) && parsed.length > 0 ? parsed : undefined;32 } catch {33 return undefined;34 }35}36 37describe('partial tool call cleanup', () => {38 // Reproduces the broken payload from the user's screenshot: model was39 // streaming a tool call whose arguments JSON was cut mid-string. The outer40 // envelope still parses, but the arguments themselves are invalid JSON and41 // the server rejects the request.42 it('marks a partial tool call payload as unsafe to re-send', () => {43 const message = makeAssistant({44 content: 'partial reasoning',45 toolCalls: JSON.stringify([46 {47 function: {48 arguments: '{"command":`grep -n \\"read_to\\" ` /Users',49 name: 'exec_shell_command'50 },51 id: 'call_1',52 type: 'function'53 }54 ])55 });56 const apiToolCalls = buildApiToolCalls(message);57 58 // The bug: even though arguments are invalid, the outer array parses and59 // the request gets sent. Function arguments must be parseable JSON on their60 // own for the server to execute the tool.61 expect(apiToolCalls).toBeDefined();62 const args = (apiToolCalls![0] as { function: { arguments: string } }).function.arguments;63 64 expect(() => JSON.parse(args)).toThrow();65 });66 67 // After Stop, savePartialResponseIfNeeded clears toolCalls and the agentic68 // flow does the same in its silent-return detection. The next request reads69 // toolCalls = '' and the conversion drops the field entirely so the server70 // never sees the half-streamed call.71 it('drops tool_calls from the API request after toolCalls is cleared', () => {72 const clearedMessage = makeAssistant({73 content: 'partial reasoning',74 toolCalls: ''75 });76 const apiToolCalls = buildApiToolCalls(clearedMessage);77 78 expect(apiToolCalls).toBeUndefined();79 });80 81 // The cleanup path keeps the partial reasoning content visible in the UI;82 // only the tool_calls field is reset. deriveAgenticSections should still83 // surface the reasoning as interrupted (no content / no tool calls behind84 // it) without resurrecting the dead tool call block.85 it('keeps reasoning content visible after cleanup, without a tool call block', () => {86 const cleared = makeAssistant({87 content: '',88 reasoningContent: 'thinking about read_to',89 toolCalls: ''90 });91 const sections = deriveAgenticSections(cleared);92 93 expect(sections).toHaveLength(1);94 expect(sections[0].type).toBe('reasoning');95 expect(sections.some((s) => s.type.includes('tool_call'))).toBe(false);96 });97});98 