open-code-review/internal/llm/message_test.go
kite e2d75b732a
Some checks are pending
CI / test (push) Waiting to run
test: fix golangci-lint errcheck/staticcheck issues in test code (#323)
2026-07-08 22:46:18 +08:00

330 lines
8.5 KiB
Go

package llm
import (
"os"
"path/filepath"
"testing"
)
func TestNewTextMessage(t *testing.T) {
m := NewTextMessage("user", "hello")
if m.Role != "user" {
t.Errorf("Role = %q, want user", m.Role)
}
if m.Content != "hello" {
t.Errorf("Content = %v, want hello", m.Content)
}
if m.ToolCallID != "" {
t.Error("ToolCallID should be empty")
}
if len(m.ToolCalls) != 0 {
t.Errorf("ToolCalls should be nil, got %v", m.ToolCalls)
}
}
func TestNewToolCallMessage(t *testing.T) {
calls := []ToolCall{
{ID: "c1", Type: "function", Function: FunctionCall{Name: "tool_a", Arguments: `{}`}},
{ID: "c2", Type: "function", Function: FunctionCall{Name: "tool_b", Arguments: `{"x":1}`}},
}
m := NewToolCallMessage("thinking", calls)
if m.Role != "assistant" {
t.Errorf("Role = %q, want assistant", m.Role)
}
if m.Content != "thinking" {
t.Errorf("Content = %v, want thinking", m.Content)
}
if len(m.ToolCalls) != 2 {
t.Fatalf("ToolCalls len = %d, want 2", len(m.ToolCalls))
}
if m.ToolCalls[0].ID != "c1" || m.ToolCalls[1].Function.Name != "tool_b" {
t.Errorf("ToolCalls not copied correctly")
}
// Mutation of original must not affect the message.
calls[0].ID = "mutated"
if m.ToolCalls[0].ID == "mutated" {
t.Error("NewToolCallMessage must copy ToolCalls")
}
}
func TestNewToolCallMessage_NilCalls(t *testing.T) {
m := NewToolCallMessage("text", nil)
if m.ToolCalls != nil {
t.Errorf("expected nil ToolCalls for nil input, got %v", m.ToolCalls)
}
}
func TestNewToolResultMessage(t *testing.T) {
m := NewToolResultMessage("call-123", "result text")
if m.Role != "tool" {
t.Errorf("Role = %q, want tool", m.Role)
}
if m.Content != "result text" {
t.Errorf("Content = %v, want result text", m.Content)
}
if m.ToolCallID != "call-123" {
t.Errorf("ToolCallID = %q, want call-123", m.ToolCallID)
}
}
func TestExtractText_String(t *testing.T) {
m := Message{Role: "user", Content: "plain text"}
if got := m.ExtractText(); got != "plain text" {
t.Errorf("ExtractText() = %q, want plain text", got)
}
}
func TestExtractText_ContentBlocks(t *testing.T) {
m := Message{Role: "assistant", Content: []ContentBlock{
{Type: "text", Text: "part1"},
{Type: "text", Text: " part2"},
}}
if got := m.ExtractText(); got != "part1 part2" {
t.Errorf("ExtractText() = %q, want 'part1 part2'", got)
}
}
func TestExtractText_NestedContentBlocks(t *testing.T) {
m := Message{Role: "tool", Content: []ContentBlock{
{
Type: "tool_result",
Content: []ContentBlock{
{Type: "text", Text: "inner1"},
{Type: "text", Text: "inner2"},
},
},
{Type: "text", Text: "outer"},
}}
got := m.ExtractText()
if got != "inner1inner2outer" {
t.Errorf("ExtractText() = %q, want inner1inner2outer", got)
}
}
func TestExtractText_Default(t *testing.T) {
m := Message{Role: "user", Content: 42}
if got := m.ExtractText(); got != "" {
t.Errorf("ExtractText() for non-string/non-block = %q, want empty", got)
}
}
func TestExtractText_NilContent(t *testing.T) {
m := Message{Role: "user", Content: nil}
if got := m.ExtractText(); got != "" {
t.Errorf("ExtractText() for nil = %q, want empty", got)
}
}
func TestChatResponse_Content(t *testing.T) {
text := "hello world"
resp := &ChatResponse{
Choices: []Choice{{
Message: ResponseMessage{Content: &text},
}},
}
if got := resp.Content(); got != "hello world" {
t.Errorf("Content() = %q, want hello world", got)
}
}
func TestChatResponse_Content_Empty(t *testing.T) {
resp := &ChatResponse{}
if got := resp.Content(); got != "" {
t.Errorf("Content() with no choices = %q, want empty", got)
}
}
func TestChatResponse_Content_FallbackToReasoning(t *testing.T) {
empty := ""
resp := &ChatResponse{
Choices: []Choice{{
Message: ResponseMessage{Content: &empty, ReasoningContent: "reasoning here"},
}},
}
if got := resp.Content(); got != "reasoning here" {
t.Errorf("Content() = %q, want reasoning here", got)
}
}
func TestChatResponse_Content_NilContent(t *testing.T) {
resp := &ChatResponse{
Choices: []Choice{{
Message: ResponseMessage{Content: nil, ReasoningContent: "fallback"},
}},
}
if got := resp.Content(); got != "fallback" {
t.Errorf("Content() = %q, want fallback", got)
}
}
func TestChatResponse_Content_StripsThinkTags(t *testing.T) {
text := "<think>internal</think>answer"
resp := &ChatResponse{
Choices: []Choice{{
Message: ResponseMessage{Content: &text},
}},
}
if got := resp.Content(); got != "internalanswer" {
t.Errorf("Content() = %q, want internalanswer", got)
}
}
func TestChatResponse_ToolCalls(t *testing.T) {
resp := &ChatResponse{
Choices: []Choice{{
Message: ResponseMessage{
ToolCalls: []ToolCall{
{ID: "c1", Type: "function", Function: FunctionCall{Name: "tool_a"}},
{ID: "c2", Type: "function", Function: FunctionCall{Name: "tool_b"}},
},
},
}},
}
calls := resp.ToolCalls()
if len(calls) != 2 {
t.Fatalf("ToolCalls() len = %d, want 2", len(calls))
}
if calls[0].Function.Name != "tool_a" || calls[1].Function.Name != "tool_b" {
t.Error("ToolCalls returned unexpected values")
}
}
func TestChatResponse_ToolCalls_Empty(t *testing.T) {
resp := &ChatResponse{}
if got := resp.ToolCalls(); got != nil {
t.Errorf("ToolCalls() with no choices = %v, want nil", got)
}
}
func TestParseShellRC(t *testing.T) {
tmp := t.TempDir()
rcPath := filepath.Join(tmp, ".zshrc")
content := `# some comment
export PATH="/usr/bin:$PATH"
export ANTHROPIC_BASE_URL="https://api.example.com"
export ANTHROPIC_AUTH_TOKEN='sk-test-token'
export ANTHROPIC_MODEL=claude-sonnet-4-20250514
`
if err := os.WriteFile(rcPath, []byte(content), 0644); err != nil {
t.Fatal(err)
}
ep, ok, err := parseShellRC(rcPath, "")
if err != nil {
t.Fatalf("parseShellRC: %v", err)
}
if !ok {
t.Fatal("expected ok=true")
}
if ep.Token != "sk-test-token" {
t.Errorf("Token = %q, want sk-test-token", ep.Token)
}
if ep.Model != "claude-sonnet-4-20250514" {
t.Errorf("Model = %q", ep.Model)
}
if ep.Protocol != "anthropic" {
t.Errorf("Protocol = %q, want anthropic", ep.Protocol)
}
if ep.AuthHeader != "authorization" {
t.Errorf("AuthHeader = %q, want authorization", ep.AuthHeader)
}
if ep.Source != "Shell rc file" {
t.Errorf("Source = %q", ep.Source)
}
}
func TestParseShellRC_ModelOverride(t *testing.T) {
tmp := t.TempDir()
rcPath := filepath.Join(tmp, ".bashrc")
content := `export ANTHROPIC_BASE_URL="https://api.example.com"
export ANTHROPIC_AUTH_TOKEN="token"
export ANTHROPIC_MODEL=claude-3-opus
`
if err := os.WriteFile(rcPath, []byte(content), 0644); err != nil {
t.Fatalf("write rc: %v", err)
}
ep, ok, err := parseShellRC(rcPath, "override-model")
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("expected ok=true")
}
if ep.Model != "override-model" {
t.Errorf("Model = %q, want override-model", ep.Model)
}
}
func TestParseShellRC_Incomplete(t *testing.T) {
tmp := t.TempDir()
rcPath := filepath.Join(tmp, ".zshrc")
content := `export ANTHROPIC_BASE_URL="https://api.example.com"
export ANTHROPIC_AUTH_TOKEN="token"
# missing ANTHROPIC_MODEL
`
if err := os.WriteFile(rcPath, []byte(content), 0644); err != nil {
t.Fatalf("write rc: %v", err)
}
_, ok, err := parseShellRC(rcPath, "")
if err != nil {
t.Fatal(err)
}
if ok {
t.Error("expected ok=false when model is missing")
}
}
func TestParseShellRC_NonexistentFile(t *testing.T) {
_, ok, err := parseShellRC("/nonexistent/path/.zshrc", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok {
t.Error("expected ok=false for missing file")
}
}
func TestModelListContains(t *testing.T) {
models := []string{"gpt-4", " claude-3-opus ", "gemini-pro"}
if !ModelListContains(models, "claude-3-opus") {
t.Error("expected true for claude-3-opus")
}
if !ModelListContains(models, "gpt-4") {
t.Error("expected true for gpt-4")
}
if ModelListContains(models, "gpt-3.5") {
t.Error("expected false for gpt-3.5")
}
if ModelListContains(nil, "anything") {
t.Error("expected false for nil list")
}
}
func TestDefaultAuthHeader(t *testing.T) {
if got := defaultAuthHeader("anthropic"); got != "authorization" {
t.Errorf("anthropic: got %q, want authorization", got)
}
if got := defaultAuthHeader("openai"); got != "" {
t.Errorf("openai: got %q, want empty", got)
}
if got := defaultAuthHeader(""); got != "" {
t.Errorf("empty: got %q, want empty", got)
}
}
func TestUserAgent(t *testing.T) {
got := userAgent("anthropic")
if got != "open-code-review/dev | anthropic" {
t.Errorf("userAgent(anthropic) = %q", got)
}
got2 := userAgent("")
if got2 != "open-code-review/dev" {
t.Errorf("userAgent('') = %q", got2)
}
}