internal/message/ — Content discriminated union, Message, Usage, StopReason, Response. 22 tests. internal/stream/ — Stream pull-based iterator interface, Event types, Accumulator (assembles Response from events). 8 tests. internal/provider/ — Provider interface, Request, ToolDefinition, Registry with factory pattern, ProviderError with HTTP status classification. errors.AsType[E] for Go 1.26. 13 tests. 43 tests total, all passing.
215 lines
5.2 KiB
Go
215 lines
5.2 KiB
Go
package message
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestNewUserText(t *testing.T) {
|
|
m := NewUserText("hello")
|
|
if m.Role != RoleUser {
|
|
t.Errorf("Role = %q, want %q", m.Role, RoleUser)
|
|
}
|
|
if len(m.Content) != 1 {
|
|
t.Fatalf("len(Content) = %d, want 1", len(m.Content))
|
|
}
|
|
if m.Content[0].Type != ContentText {
|
|
t.Errorf("Content[0].Type = %v, want %v", m.Content[0].Type, ContentText)
|
|
}
|
|
if m.Content[0].Text != "hello" {
|
|
t.Errorf("Content[0].Text = %q, want %q", m.Content[0].Text, "hello")
|
|
}
|
|
}
|
|
|
|
func TestNewAssistantText(t *testing.T) {
|
|
m := NewAssistantText("response")
|
|
if m.Role != RoleAssistant {
|
|
t.Errorf("Role = %q, want %q", m.Role, RoleAssistant)
|
|
}
|
|
if m.TextContent() != "response" {
|
|
t.Errorf("TextContent() = %q, want %q", m.TextContent(), "response")
|
|
}
|
|
}
|
|
|
|
func TestNewSystemText(t *testing.T) {
|
|
m := NewSystemText("you are a helper")
|
|
if m.Role != RoleSystem {
|
|
t.Errorf("Role = %q, want %q", m.Role, RoleSystem)
|
|
}
|
|
}
|
|
|
|
func TestNewAssistantContent_Mixed(t *testing.T) {
|
|
m := NewAssistantContent(
|
|
NewTextContent("I'll run that command."),
|
|
NewToolCallContent(ToolCall{
|
|
ID: "tc_1",
|
|
Name: "bash",
|
|
Arguments: json.RawMessage(`{"command":"ls"}`),
|
|
}),
|
|
)
|
|
|
|
if m.Role != RoleAssistant {
|
|
t.Errorf("Role = %q, want %q", m.Role, RoleAssistant)
|
|
}
|
|
if len(m.Content) != 2 {
|
|
t.Fatalf("len(Content) = %d, want 2", len(m.Content))
|
|
}
|
|
if m.Content[0].Type != ContentText {
|
|
t.Errorf("Content[0].Type = %v, want text", m.Content[0].Type)
|
|
}
|
|
if m.Content[1].Type != ContentToolCall {
|
|
t.Errorf("Content[1].Type = %v, want tool_call", m.Content[1].Type)
|
|
}
|
|
}
|
|
|
|
func TestNewToolResults(t *testing.T) {
|
|
m := NewToolResults(
|
|
ToolResult{ToolCallID: "tc_1", Content: "output1"},
|
|
ToolResult{ToolCallID: "tc_2", Content: "output2", IsError: true},
|
|
)
|
|
|
|
if m.Role != RoleUser {
|
|
t.Errorf("Role = %q, want %q", m.Role, RoleUser)
|
|
}
|
|
if len(m.Content) != 2 {
|
|
t.Fatalf("len(Content) = %d, want 2", len(m.Content))
|
|
}
|
|
if m.Content[0].ToolResult.ToolCallID != "tc_1" {
|
|
t.Errorf("Content[0].ToolResult.ToolCallID = %q", m.Content[0].ToolResult.ToolCallID)
|
|
}
|
|
if m.Content[1].ToolResult.IsError != true {
|
|
t.Error("Content[1].ToolResult.IsError should be true")
|
|
}
|
|
}
|
|
|
|
func TestMessage_HasToolCalls(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
msg Message
|
|
want bool
|
|
}{
|
|
{
|
|
name: "text only",
|
|
msg: NewUserText("hello"),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "with tool call",
|
|
msg: NewAssistantContent(
|
|
NewTextContent("running..."),
|
|
NewToolCallContent(ToolCall{ID: "tc_1", Name: "bash"}),
|
|
),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "tool results (not calls)",
|
|
msg: NewToolResults(ToolResult{ToolCallID: "tc_1", Content: "ok"}),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "empty message",
|
|
msg: Message{Role: RoleAssistant},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.msg.HasToolCalls(); got != tt.want {
|
|
t.Errorf("HasToolCalls() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMessage_ToolCalls(t *testing.T) {
|
|
m := NewAssistantContent(
|
|
NewTextContent("here are two commands"),
|
|
NewToolCallContent(ToolCall{ID: "tc_1", Name: "bash", Arguments: json.RawMessage(`{"command":"ls"}`)}),
|
|
NewTextContent("and another"),
|
|
NewToolCallContent(ToolCall{ID: "tc_2", Name: "fs.read", Arguments: json.RawMessage(`{"path":"go.mod"}`)}),
|
|
)
|
|
|
|
calls := m.ToolCalls()
|
|
if len(calls) != 2 {
|
|
t.Fatalf("len(ToolCalls()) = %d, want 2", len(calls))
|
|
}
|
|
if calls[0].ID != "tc_1" {
|
|
t.Errorf("calls[0].ID = %q, want tc_1", calls[0].ID)
|
|
}
|
|
if calls[1].Name != "fs.read" {
|
|
t.Errorf("calls[1].Name = %q, want fs.read", calls[1].Name)
|
|
}
|
|
}
|
|
|
|
func TestMessage_ToolCalls_Empty(t *testing.T) {
|
|
m := NewUserText("no tools here")
|
|
calls := m.ToolCalls()
|
|
if len(calls) != 0 {
|
|
t.Errorf("len(ToolCalls()) = %d, want 0", len(calls))
|
|
}
|
|
}
|
|
|
|
func TestMessage_TextContent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
msg Message
|
|
want string
|
|
}{
|
|
{
|
|
name: "single text",
|
|
msg: NewUserText("hello"),
|
|
want: "hello",
|
|
},
|
|
{
|
|
name: "multiple text blocks",
|
|
msg: NewAssistantContent(
|
|
NewTextContent("first "),
|
|
NewToolCallContent(ToolCall{ID: "tc_1", Name: "bash"}),
|
|
NewTextContent("second"),
|
|
),
|
|
want: "first second",
|
|
},
|
|
{
|
|
name: "no text",
|
|
msg: NewToolResults(ToolResult{ToolCallID: "tc_1", Content: "output"}),
|
|
want: "",
|
|
},
|
|
{
|
|
name: "empty message",
|
|
msg: Message{Role: RoleAssistant},
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.msg.TextContent(); got != tt.want {
|
|
t.Errorf("TextContent() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResponse_Fields(t *testing.T) {
|
|
r := Response{
|
|
Message: NewAssistantText("done"),
|
|
StopReason: StopEndTurn,
|
|
Usage: Usage{InputTokens: 100, OutputTokens: 50},
|
|
Model: "mistral-large-latest",
|
|
}
|
|
|
|
if r.StopReason != StopEndTurn {
|
|
t.Errorf("StopReason = %q, want %q", r.StopReason, StopEndTurn)
|
|
}
|
|
if r.Usage.TotalTokens() != 150 {
|
|
t.Errorf("Usage.TotalTokens() = %d, want 150", r.Usage.TotalTokens())
|
|
}
|
|
if r.Model != "mistral-large-latest" {
|
|
t.Errorf("Model = %q", r.Model)
|
|
}
|
|
if r.Message.TextContent() != "done" {
|
|
t.Errorf("Message.TextContent() = %q", r.Message.TextContent())
|
|
}
|
|
}
|