mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-09 17:28:58 +00:00
330 lines
8.5 KiB
Go
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)
|
|
}
|
|
}
|