File size: 3,260 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import { generate } from '#/generate';
import type { Message, StreamedMessagePart } from '#/message';
import { createAssistantMessage, createUserMessage } from '#/message';
import type { ChatProvider, GenerateOptions, StreamedMessage, ThinkingEffort } from '#/provider';
import type { Tool } from '#/tool';
import type { TokenUsage } from '#/usage';
import { describe, expect, it } from 'vitest';

function createMockStream(
  parts: StreamedMessagePart[],
  opts?: { id?: string; usage?: TokenUsage },
): StreamedMessage {
  return {
    get id(): string | null {
      return opts?.id ?? null;
    },
    get usage(): TokenUsage | null {
      return opts?.usage ?? null;
    },
    finishReason: null,
    rawFinishReason: null,
    async *[Symbol.asyncIterator](): AsyncIterator<StreamedMessagePart> {
      for (const part of parts) {
        yield part;
      }
    },
  };
}

function createMockProvider(stream: StreamedMessage): ChatProvider {
  return {
    name: 'mock',
    modelName: 'mock-model',
    thinkingEffort: null,
    generate: async (
      _systemPrompt: string,
      _tools: Tool[],
      _history: Message[],
      _options?: GenerateOptions,
    ): Promise<StreamedMessage> => stream,
    withThinking(_effort: ThinkingEffort): ChatProvider {
      return this;
    },
  };
}

describe('regression', () => {
  describe('empty text parts', () => {
    it('standalone empty TextPart is kept in content', async () => {
      const stream = createMockStream([
        { type: 'text', text: 'before' },
        { type: 'image_url', imageUrl: { url: 'https://example.com/img.png' } },
        { type: 'text', text: '' },
      ]);
      const provider = createMockProvider(stream);

      const result = await generate(provider, '', [], []);

      expect(result.message.content).toEqual([
        { type: 'text', text: 'before' },
        { type: 'image_url', imageUrl: { url: 'https://example.com/img.png' } },
        { type: 'text', text: '' },
      ]);
    });
  });

  describe('toolCalls defaults', () => {
    it('createUserMessage has toolCalls as empty array (not undefined)', () => {
      const msg = createUserMessage('hello');
      expect(msg.toolCalls).toEqual([]);
    });

    it('createAssistantMessage without toolCalls has empty array', () => {
      const msg = createAssistantMessage([{ type: 'text', text: 'test' }]);
      expect(msg.toolCalls).toEqual([]);
    });

    it('createAssistantMessage with explicit toolCalls preserves them', () => {
      const msg = createAssistantMessage(
        [{ type: 'text', text: 'test' }],
        [
          {
            type: 'function',
            id: 'call-1',
            name: 'search', arguments: '{}',
          },
        ],
      );
      expect(msg.toolCalls).toEqual([
        {
          type: 'function',
          id: 'call-1',
          name: 'search', arguments: '{}',
        },
      ]);
    });

    it('generate() result message has toolCalls as empty array when no tools called', async () => {
      const stream = createMockStream([{ type: 'text', text: 'hi' }]);
      const provider = createMockProvider(stream);

      const result = await generate(provider, '', [], []);
      expect(result.message.toolCalls).toEqual([]);
    });
  });
});