mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-25 17:14:54 +00:00
183 lines
6.4 KiB
TypeScript
183 lines
6.4 KiB
TypeScript
import { ScriptedEchoChatProvider } from './fixtures/echo-provider';
|
|
import { ChatProviderError } from '#/errors';
|
|
import type {
|
|
AudioURLPart,
|
|
StreamedMessagePart,
|
|
TextPart,
|
|
ToolCall,
|
|
ToolCallPart,
|
|
VideoURLPart,
|
|
} from '#/message';
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
describe('ScriptedEchoChatProvider', () => {
|
|
it('streams parts from first and second scripts', async () => {
|
|
const dsl = [
|
|
'id: scripted-1',
|
|
'usage: {"input_other": 4, "output": 1, "input_cache_read": 2}',
|
|
'text: Hello,',
|
|
'text: world!',
|
|
'think: thinking...',
|
|
'image_url: {"url": "https://example.com/image.png", "id": "img-1"}',
|
|
'audio_url: https://example.com/audio.mp3',
|
|
'video_url: https://example.com/video.mp4',
|
|
'tool_call: {"id": "call-1", "name": "search", "arguments": "{\\"q\\":\\"python\\"", "extras": {"source": "test"}}',
|
|
'tool_call_part: {"arguments_part": "}"}',
|
|
].join('\n');
|
|
|
|
const secondDsl = ['id: scripted-2', 'text: second turn'].join('\n');
|
|
|
|
const provider = new ScriptedEchoChatProvider([dsl, secondDsl]);
|
|
|
|
// First call
|
|
const parts: StreamedMessagePart[] = [];
|
|
const stream = await provider.generate('', [], []);
|
|
for await (const part of stream) {
|
|
parts.push(part);
|
|
}
|
|
|
|
expect(stream.id).toBe('scripted-1');
|
|
expect(stream.usage).toEqual({
|
|
inputOther: 4,
|
|
output: 1,
|
|
inputCacheRead: 2,
|
|
inputCacheCreation: 0,
|
|
});
|
|
expect(parts).toEqual([
|
|
{ type: 'text', text: 'Hello,' } satisfies TextPart,
|
|
{ type: 'text', text: ' world!' } satisfies TextPart,
|
|
{ type: 'think', think: 'thinking...' },
|
|
{
|
|
type: 'image_url',
|
|
imageUrl: { url: 'https://example.com/image.png', id: 'img-1' },
|
|
},
|
|
{
|
|
type: 'audio_url',
|
|
audioUrl: { url: 'https://example.com/audio.mp3' },
|
|
} satisfies AudioURLPart,
|
|
{
|
|
type: 'video_url',
|
|
videoUrl: { url: 'https://example.com/video.mp4' },
|
|
} satisfies VideoURLPart,
|
|
{
|
|
type: 'function',
|
|
id: 'call-1',
|
|
name: 'search', arguments: '{"q":"python"',
|
|
} satisfies ToolCall,
|
|
{ type: 'tool_call_part', argumentsPart: '}' } satisfies ToolCallPart,
|
|
]);
|
|
|
|
// Second call
|
|
const secondStream = await provider.generate('', [], []);
|
|
const secondParts: StreamedMessagePart[] = [];
|
|
for await (const part of secondStream) {
|
|
secondParts.push(part);
|
|
}
|
|
|
|
expect(secondStream.id).toBe('scripted-2');
|
|
expect(secondStream.usage).toBeNull();
|
|
expect(secondParts).toEqual([{ type: 'text', text: 'second turn' }]);
|
|
});
|
|
|
|
it('throws when scripts are exhausted', async () => {
|
|
const provider = new ScriptedEchoChatProvider(['text: only once']);
|
|
|
|
await provider.generate('', [], []);
|
|
|
|
await expect(provider.generate('', [], [])).rejects.toThrow(ChatProviderError);
|
|
});
|
|
|
|
it('rejects non-string arguments in tool_call', async () => {
|
|
const dsl = 'tool_call: {"id": "call-1", "name": "search", "arguments": {"q": "python"}}';
|
|
const provider = new ScriptedEchoChatProvider([dsl]);
|
|
|
|
await expect(provider.generate('', [], [])).rejects.toThrow(ChatProviderError);
|
|
});
|
|
|
|
it('rejects empty DSL content', async () => {
|
|
const provider = new ScriptedEchoChatProvider(['# comment only\n```']);
|
|
|
|
await expect(provider.generate('', [], [])).rejects.toThrow(ChatProviderError);
|
|
});
|
|
|
|
it('has correct default properties', () => {
|
|
const provider = new ScriptedEchoChatProvider([]);
|
|
expect(provider.name).toBe('scripted_echo');
|
|
expect(provider.modelName).toBe('scripted_echo');
|
|
expect(provider.thinkingEffort).toBeNull();
|
|
});
|
|
|
|
it('withThinking returns a new provider with remaining scripts', async () => {
|
|
const provider = new ScriptedEchoChatProvider(['text: first', 'text: second']);
|
|
|
|
// consume first script
|
|
await provider.generate('', [], []);
|
|
|
|
const newProvider = provider.withThinking('high');
|
|
expect(newProvider).toBeInstanceOf(ScriptedEchoChatProvider);
|
|
expect(newProvider).not.toBe(provider);
|
|
|
|
// new provider should have only the remaining script
|
|
const stream = await newProvider.generate('', [], []);
|
|
const parts: StreamedMessagePart[] = [];
|
|
for await (const part of stream) {
|
|
parts.push(part);
|
|
}
|
|
expect(parts).toEqual([{ type: 'text', text: 'second' }]);
|
|
});
|
|
|
|
it('exposes per-script finish_reason values across sequential generate() calls', async () => {
|
|
// Each script carries a different DSL finish_reason and the returned
|
|
// stream must expose the right normalized + raw pair.
|
|
const scripts = [
|
|
['id: scripted-fr-1', 'text: turn-1', 'finish_reason: stop'].join('\n'),
|
|
['id: scripted-fr-2', 'text: turn-2', 'finish_reason: length'].join('\n'),
|
|
['id: scripted-fr-3', 'text: turn-3', 'finish_reason: tool_calls'].join('\n'),
|
|
['id: scripted-fr-4', 'text: turn-4', 'finish_reason: null'].join('\n'),
|
|
['id: scripted-fr-5', 'text: turn-5'].join('\n'), // default
|
|
];
|
|
const provider = new ScriptedEchoChatProvider(scripts);
|
|
|
|
const expected: Array<{
|
|
finishReason: 'completed' | 'tool_calls' | 'truncated' | null;
|
|
rawFinishReason: string | null;
|
|
}> = [
|
|
{ finishReason: 'completed', rawFinishReason: 'stop' },
|
|
{ finishReason: 'truncated', rawFinishReason: 'length' },
|
|
{ finishReason: 'tool_calls', rawFinishReason: 'tool_calls' },
|
|
{ finishReason: null, rawFinishReason: null },
|
|
{ finishReason: 'completed', rawFinishReason: 'stop' },
|
|
];
|
|
|
|
for (const exp of expected) {
|
|
const stream = await provider.generate('', [], []);
|
|
for await (const _ of stream) {
|
|
void _;
|
|
}
|
|
expect({
|
|
finishReason: stream.finishReason,
|
|
rawFinishReason: stream.rawFinishReason,
|
|
}).toEqual(exp);
|
|
}
|
|
});
|
|
|
|
it('generate merges tool_call arguments via mergeInPlace', async () => {
|
|
const dsl = [
|
|
'id: scripted-merge-1',
|
|
'tool_call: {"id": "call-1", "name": "search", "arguments": "{\\"q\\":\\"py"}',
|
|
'tool_call_part: {"arguments_part": "thon\\"}"}',
|
|
].join('\n');
|
|
|
|
const provider = new ScriptedEchoChatProvider([dsl]);
|
|
const { generate } = await import('#/generate');
|
|
|
|
const result = await generate(provider, '', [], []);
|
|
|
|
expect(result.message.toolCalls).toHaveLength(1);
|
|
|
|
const tc = result.message.toolCalls[0]!;
|
|
expect(tc.id).toBe('call-1');
|
|
expect(tc.name).toBe('search');
|
|
expect(tc.arguments).toBe('{"q":"python"}');
|
|
});
|
|
});
|