go-ai/agentic/client_test.go
Claude e84d6ad3c9
feat: extract AI/ML packages from core/go
LEM scoring pipeline, native MLX Metal bindings, Claude SDK wrapper,
RAG with Qdrant/Ollama, unified AI facade, and MCP protocol server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:25:55 +00:00

356 lines
10 KiB
Go

package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test fixtures
var testTask = Task{
ID: "task-123",
Title: "Implement feature X",
Description: "Add the new feature X to the system",
Priority: PriorityHigh,
Status: StatusPending,
Labels: []string{"feature", "backend"},
Files: []string{"pkg/feature/feature.go"},
CreatedAt: time.Now().Add(-24 * time.Hour),
Project: "core",
}
var testTasks = []Task{
testTask,
{
ID: "task-456",
Title: "Fix bug Y",
Description: "Fix the bug in component Y",
Priority: PriorityCritical,
Status: StatusPending,
Labels: []string{"bug", "urgent"},
CreatedAt: time.Now().Add(-2 * time.Hour),
Project: "core",
},
}
func TestNewClient_Good(t *testing.T) {
client := NewClient("https://api.example.com", "test-token")
assert.Equal(t, "https://api.example.com", client.BaseURL)
assert.Equal(t, "test-token", client.Token)
assert.NotNil(t, client.HTTPClient)
}
func TestNewClient_Good_TrailingSlash(t *testing.T) {
client := NewClient("https://api.example.com/", "test-token")
assert.Equal(t, "https://api.example.com", client.BaseURL)
}
func TestNewClientFromConfig_Good(t *testing.T) {
cfg := &Config{
BaseURL: "https://api.example.com",
Token: "config-token",
AgentID: "agent-001",
}
client := NewClientFromConfig(cfg)
assert.Equal(t, "https://api.example.com", client.BaseURL)
assert.Equal(t, "config-token", client.Token)
assert.Equal(t, "agent-001", client.AgentID)
}
func TestClient_ListTasks_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, "/api/tasks", r.URL.Path)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(testTasks)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
tasks, err := client.ListTasks(context.Background(), ListOptions{})
require.NoError(t, err)
assert.Len(t, tasks, 2)
assert.Equal(t, "task-123", tasks[0].ID)
assert.Equal(t, "task-456", tasks[1].ID)
}
func TestClient_ListTasks_Good_WithFilters(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
assert.Equal(t, "pending", query.Get("status"))
assert.Equal(t, "high", query.Get("priority"))
assert.Equal(t, "core", query.Get("project"))
assert.Equal(t, "10", query.Get("limit"))
assert.Equal(t, "bug,urgent", query.Get("labels"))
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode([]Task{testTask})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
opts := ListOptions{
Status: StatusPending,
Priority: PriorityHigh,
Project: "core",
Limit: 10,
Labels: []string{"bug", "urgent"},
}
tasks, err := client.ListTasks(context.Background(), opts)
require.NoError(t, err)
assert.Len(t, tasks, 1)
}
func TestClient_ListTasks_Bad_ServerError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(APIError{Message: "internal error"})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
tasks, err := client.ListTasks(context.Background(), ListOptions{})
assert.Error(t, err)
assert.Nil(t, tasks)
assert.Contains(t, err.Error(), "internal error")
}
func TestClient_GetTask_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, "/api/tasks/task-123", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(testTask)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
task, err := client.GetTask(context.Background(), "task-123")
require.NoError(t, err)
assert.Equal(t, "task-123", task.ID)
assert.Equal(t, "Implement feature X", task.Title)
assert.Equal(t, PriorityHigh, task.Priority)
}
func TestClient_GetTask_Bad_EmptyID(t *testing.T) {
client := NewClient("https://api.example.com", "test-token")
task, err := client.GetTask(context.Background(), "")
assert.Error(t, err)
assert.Nil(t, task)
assert.Contains(t, err.Error(), "task ID is required")
}
func TestClient_GetTask_Bad_NotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(APIError{Message: "task not found"})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
task, err := client.GetTask(context.Background(), "nonexistent")
assert.Error(t, err)
assert.Nil(t, task)
assert.Contains(t, err.Error(), "task not found")
}
func TestClient_ClaimTask_Good(t *testing.T) {
claimedTask := testTask
claimedTask.Status = StatusInProgress
claimedTask.ClaimedBy = "agent-001"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/api/tasks/task-123/claim", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(ClaimResponse{Task: &claimedTask})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
client.AgentID = "agent-001"
task, err := client.ClaimTask(context.Background(), "task-123")
require.NoError(t, err)
assert.Equal(t, StatusInProgress, task.Status)
assert.Equal(t, "agent-001", task.ClaimedBy)
}
func TestClient_ClaimTask_Good_SimpleResponse(t *testing.T) {
// Some APIs might return just the task without wrapping
claimedTask := testTask
claimedTask.Status = StatusInProgress
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(claimedTask)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
task, err := client.ClaimTask(context.Background(), "task-123")
require.NoError(t, err)
assert.Equal(t, "task-123", task.ID)
}
func TestClient_ClaimTask_Bad_EmptyID(t *testing.T) {
client := NewClient("https://api.example.com", "test-token")
task, err := client.ClaimTask(context.Background(), "")
assert.Error(t, err)
assert.Nil(t, task)
assert.Contains(t, err.Error(), "task ID is required")
}
func TestClient_ClaimTask_Bad_AlreadyClaimed(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusConflict)
_ = json.NewEncoder(w).Encode(APIError{Message: "task already claimed"})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
task, err := client.ClaimTask(context.Background(), "task-123")
assert.Error(t, err)
assert.Nil(t, task)
assert.Contains(t, err.Error(), "task already claimed")
}
func TestClient_UpdateTask_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPatch, r.Method)
assert.Equal(t, "/api/tasks/task-123", r.URL.Path)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
var update TaskUpdate
err := json.NewDecoder(r.Body).Decode(&update)
require.NoError(t, err)
assert.Equal(t, StatusInProgress, update.Status)
assert.Equal(t, 50, update.Progress)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
err := client.UpdateTask(context.Background(), "task-123", TaskUpdate{
Status: StatusInProgress,
Progress: 50,
Notes: "Making progress",
})
assert.NoError(t, err)
}
func TestClient_UpdateTask_Bad_EmptyID(t *testing.T) {
client := NewClient("https://api.example.com", "test-token")
err := client.UpdateTask(context.Background(), "", TaskUpdate{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "task ID is required")
}
func TestClient_CompleteTask_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/api/tasks/task-123/complete", r.URL.Path)
var result TaskResult
err := json.NewDecoder(r.Body).Decode(&result)
require.NoError(t, err)
assert.True(t, result.Success)
assert.Equal(t, "Feature implemented", result.Output)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
err := client.CompleteTask(context.Background(), "task-123", TaskResult{
Success: true,
Output: "Feature implemented",
Artifacts: []string{"pkg/feature/feature.go"},
})
assert.NoError(t, err)
}
func TestClient_CompleteTask_Bad_EmptyID(t *testing.T) {
client := NewClient("https://api.example.com", "test-token")
err := client.CompleteTask(context.Background(), "", TaskResult{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "task ID is required")
}
func TestClient_Ping_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/health", r.URL.Path)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
err := client.Ping(context.Background())
assert.NoError(t, err)
}
func TestClient_Ping_Bad_ServerDown(t *testing.T) {
client := NewClient("http://localhost:99999", "test-token")
client.HTTPClient.Timeout = 100 * time.Millisecond
err := client.Ping(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "request failed")
}
func TestAPIError_Error_Good(t *testing.T) {
err := &APIError{
Code: 404,
Message: "task not found",
}
assert.Equal(t, "task not found", err.Error())
err.Details = "task-123 does not exist"
assert.Equal(t, "task not found: task-123 does not exist", err.Error())
}
func TestTaskStatus_Good(t *testing.T) {
assert.Equal(t, TaskStatus("pending"), StatusPending)
assert.Equal(t, TaskStatus("in_progress"), StatusInProgress)
assert.Equal(t, TaskStatus("completed"), StatusCompleted)
assert.Equal(t, TaskStatus("blocked"), StatusBlocked)
}
func TestTaskPriority_Good(t *testing.T) {
assert.Equal(t, TaskPriority("critical"), PriorityCritical)
assert.Equal(t, TaskPriority("high"), PriorityHigh)
assert.Equal(t, TaskPriority("medium"), PriorityMedium)
assert.Equal(t, TaskPriority("low"), PriorityLow)
}