Add comprehensive test suite for v2 module with mock provider
All checks were successful
CI / Lint (push) Successful in 9m36s
CI / V2 Module (push) Successful in 11m33s
CI / Root Module (push) Successful in 11m35s

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>
This commit is contained in:
2026-02-07 22:00:49 -05:00
parent cbe340ced0
commit 6a7eeef619
7 changed files with 1678 additions and 0 deletions

212
v2/message_test.go Normal file
View File

@@ -0,0 +1,212 @@
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)
}
})
}