Cover all core library logic (Client, Model, Chat, middleware, streaming, message conversion, request building) using a configurable mock provider that avoids real API calls. ~50 tests across 7 files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
6.3 KiB
Go
213 lines
6.3 KiB
Go
package llm
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/go-llm/v2/provider"
|
|
)
|
|
|
|
func TestUserMessage(t *testing.T) {
|
|
msg := UserMessage("hello")
|
|
if msg.Role != RoleUser {
|
|
t.Errorf("expected role=user, got %v", msg.Role)
|
|
}
|
|
if msg.Content.Text != "hello" {
|
|
t.Errorf("expected text='hello', got %q", msg.Content.Text)
|
|
}
|
|
if len(msg.Content.Images) != 0 {
|
|
t.Errorf("expected no images, got %d", len(msg.Content.Images))
|
|
}
|
|
}
|
|
|
|
func TestUserMessageWithImages(t *testing.T) {
|
|
img1 := Image{URL: "https://example.com/1.png"}
|
|
img2 := Image{Base64: "abc123", ContentType: "image/png"}
|
|
|
|
msg := UserMessageWithImages("describe", img1, img2)
|
|
if msg.Role != RoleUser {
|
|
t.Errorf("expected role=user, got %v", msg.Role)
|
|
}
|
|
if msg.Content.Text != "describe" {
|
|
t.Errorf("expected text='describe', got %q", msg.Content.Text)
|
|
}
|
|
if len(msg.Content.Images) != 2 {
|
|
t.Fatalf("expected 2 images, got %d", len(msg.Content.Images))
|
|
}
|
|
if msg.Content.Images[0].URL != "https://example.com/1.png" {
|
|
t.Errorf("expected image[0] URL, got %q", msg.Content.Images[0].URL)
|
|
}
|
|
if msg.Content.Images[1].Base64 != "abc123" {
|
|
t.Errorf("expected image[1] base64='abc123', got %q", msg.Content.Images[1].Base64)
|
|
}
|
|
if msg.Content.Images[1].ContentType != "image/png" {
|
|
t.Errorf("expected image[1] contentType='image/png', got %q", msg.Content.Images[1].ContentType)
|
|
}
|
|
}
|
|
|
|
func TestSystemMessage(t *testing.T) {
|
|
msg := SystemMessage("Be helpful")
|
|
if msg.Role != RoleSystem {
|
|
t.Errorf("expected role=system, got %v", msg.Role)
|
|
}
|
|
if msg.Content.Text != "Be helpful" {
|
|
t.Errorf("expected text='Be helpful', got %q", msg.Content.Text)
|
|
}
|
|
}
|
|
|
|
func TestAssistantMessage(t *testing.T) {
|
|
msg := AssistantMessage("Sure thing")
|
|
if msg.Role != RoleAssistant {
|
|
t.Errorf("expected role=assistant, got %v", msg.Role)
|
|
}
|
|
if msg.Content.Text != "Sure thing" {
|
|
t.Errorf("expected text='Sure thing', got %q", msg.Content.Text)
|
|
}
|
|
}
|
|
|
|
func TestToolResultMessage(t *testing.T) {
|
|
msg := ToolResultMessage("tc-123", "result data")
|
|
if msg.Role != RoleTool {
|
|
t.Errorf("expected role=tool, got %v", msg.Role)
|
|
}
|
|
if msg.ToolCallID != "tc-123" {
|
|
t.Errorf("expected toolCallID='tc-123', got %q", msg.ToolCallID)
|
|
}
|
|
if msg.Content.Text != "result data" {
|
|
t.Errorf("expected text='result data', got %q", msg.Content.Text)
|
|
}
|
|
}
|
|
|
|
func TestConvertMessages(t *testing.T) {
|
|
msgs := []Message{
|
|
SystemMessage("system prompt"),
|
|
UserMessageWithImages("look at this", Image{URL: "https://example.com/img.png"}),
|
|
{
|
|
Role: RoleAssistant,
|
|
Content: Content{Text: "I'll use a tool"},
|
|
ToolCalls: []ToolCall{
|
|
{ID: "tc1", Name: "search", Arguments: `{"q":"test"}`},
|
|
},
|
|
},
|
|
ToolResultMessage("tc1", "found it"),
|
|
}
|
|
|
|
converted := convertMessages(msgs)
|
|
|
|
if len(converted) != 4 {
|
|
t.Fatalf("expected 4 converted messages, got %d", len(converted))
|
|
}
|
|
|
|
// System message
|
|
if converted[0].Role != "system" {
|
|
t.Errorf("msg[0]: expected role='system', got %q", converted[0].Role)
|
|
}
|
|
if converted[0].Content != "system prompt" {
|
|
t.Errorf("msg[0]: expected content='system prompt', got %q", converted[0].Content)
|
|
}
|
|
|
|
// User message with images
|
|
if converted[1].Role != "user" {
|
|
t.Errorf("msg[1]: expected role='user', got %q", converted[1].Role)
|
|
}
|
|
if len(converted[1].Images) != 1 {
|
|
t.Fatalf("msg[1]: expected 1 image, got %d", len(converted[1].Images))
|
|
}
|
|
if converted[1].Images[0].URL != "https://example.com/img.png" {
|
|
t.Errorf("msg[1]: expected image URL, got %q", converted[1].Images[0].URL)
|
|
}
|
|
|
|
// Assistant message with tool calls
|
|
if converted[2].Role != "assistant" {
|
|
t.Errorf("msg[2]: expected role='assistant', got %q", converted[2].Role)
|
|
}
|
|
if len(converted[2].ToolCalls) != 1 {
|
|
t.Fatalf("msg[2]: expected 1 tool call, got %d", len(converted[2].ToolCalls))
|
|
}
|
|
if converted[2].ToolCalls[0].ID != "tc1" {
|
|
t.Errorf("msg[2]: expected tool call ID='tc1', got %q", converted[2].ToolCalls[0].ID)
|
|
}
|
|
if converted[2].ToolCalls[0].Name != "search" {
|
|
t.Errorf("msg[2]: expected tool call name='search', got %q", converted[2].ToolCalls[0].Name)
|
|
}
|
|
if converted[2].ToolCalls[0].Arguments != `{"q":"test"}` {
|
|
t.Errorf("msg[2]: expected tool call arguments, got %q", converted[2].ToolCalls[0].Arguments)
|
|
}
|
|
|
|
// Tool result message
|
|
if converted[3].Role != "tool" {
|
|
t.Errorf("msg[3]: expected role='tool', got %q", converted[3].Role)
|
|
}
|
|
if converted[3].ToolCallID != "tc1" {
|
|
t.Errorf("msg[3]: expected toolCallID='tc1', got %q", converted[3].ToolCallID)
|
|
}
|
|
if converted[3].Content != "found it" {
|
|
t.Errorf("msg[3]: expected content='found it', got %q", converted[3].Content)
|
|
}
|
|
}
|
|
|
|
func TestConvertProviderResponse(t *testing.T) {
|
|
t.Run("text only", func(t *testing.T) {
|
|
resp := convertProviderResponse(provider.Response{
|
|
Text: "hello",
|
|
Usage: &provider.Usage{
|
|
InputTokens: 10,
|
|
OutputTokens: 5,
|
|
TotalTokens: 15,
|
|
},
|
|
})
|
|
if resp.Text != "hello" {
|
|
t.Errorf("expected text='hello', got %q", resp.Text)
|
|
}
|
|
if resp.HasToolCalls() {
|
|
t.Error("expected no tool calls")
|
|
}
|
|
if resp.Usage == nil {
|
|
t.Fatal("expected usage")
|
|
}
|
|
if resp.Usage.InputTokens != 10 {
|
|
t.Errorf("expected 10 input tokens, got %d", resp.Usage.InputTokens)
|
|
}
|
|
|
|
msg := resp.Message()
|
|
if msg.Role != RoleAssistant {
|
|
t.Errorf("expected role=assistant, got %v", msg.Role)
|
|
}
|
|
if msg.Content.Text != "hello" {
|
|
t.Errorf("expected message text='hello', got %q", msg.Content.Text)
|
|
}
|
|
})
|
|
|
|
t.Run("with tool calls", func(t *testing.T) {
|
|
resp := convertProviderResponse(provider.Response{
|
|
ToolCalls: []provider.ToolCall{
|
|
{ID: "tc1", Name: "search", Arguments: `{"q":"go"}`},
|
|
{ID: "tc2", Name: "calc", Arguments: `{"a":1}`},
|
|
},
|
|
})
|
|
if !resp.HasToolCalls() {
|
|
t.Fatal("expected tool calls")
|
|
}
|
|
if len(resp.ToolCalls) != 2 {
|
|
t.Fatalf("expected 2 tool calls, got %d", len(resp.ToolCalls))
|
|
}
|
|
if resp.ToolCalls[0].ID != "tc1" || resp.ToolCalls[0].Name != "search" {
|
|
t.Errorf("unexpected tool call[0]: %+v", resp.ToolCalls[0])
|
|
}
|
|
if resp.ToolCalls[1].ID != "tc2" || resp.ToolCalls[1].Name != "calc" {
|
|
t.Errorf("unexpected tool call[1]: %+v", resp.ToolCalls[1])
|
|
}
|
|
|
|
msg := resp.Message()
|
|
if len(msg.ToolCalls) != 2 {
|
|
t.Errorf("expected 2 tool calls in message, got %d", len(msg.ToolCalls))
|
|
}
|
|
})
|
|
|
|
t.Run("nil usage", func(t *testing.T) {
|
|
resp := convertProviderResponse(provider.Response{Text: "ok"})
|
|
if resp.Usage != nil {
|
|
t.Errorf("expected nil usage, got %+v", resp.Usage)
|
|
}
|
|
})
|
|
}
|