Pulse/internal/ai/providers/openai_test.go
rcourtman cb960a04e3 Add comprehensive AI test coverage
- Add integration tests for Ollama provider (17 tests against real API)
- Add unit tests for baseline, correlation, patterns, memory, knowledge, cost packages
- Add context formatter and builder tests
- Add factory tests for provider initialization
- Add Makefile targets: test-integration, test-all
- Clean up test theatre (removed struct field tests)

Integration tests require Ollama at OLLAMA_URL (default: 192.168.0.124:11434)
Run with: make test-integration
2025-12-16 12:33:06 +00:00

245 lines
6.6 KiB
Go

package providers
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// Mock OpenAI API response format
type mockOpenAIResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
// Mock OpenAI models response
type mockOpenAIModelsResponse struct {
Object string `json:"object"`
Data []struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
OwnedBy string `json:"owned_by"`
} `json:"data"`
}
func TestOpenAIClient_Chat_Success(t *testing.T) {
// Create a mock server that returns a successful response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST, got %s", r.Method)
}
if r.Header.Get("Authorization") != "Bearer test-api-key" {
t.Errorf("Expected Bearer token, got %s", r.Header.Get("Authorization"))
}
if r.Header.Get("Content-Type") != "application/json" {
t.Errorf("Expected JSON content type, got %s", r.Header.Get("Content-Type"))
}
// Return mock response
resp := mockOpenAIResponse{
ID: "chatcmpl-123",
Object: "chat.completion",
Created: time.Now().Unix(),
Model: "gpt-4o",
Choices: []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
}{
{
Index: 0,
Message: struct {
Role string `json:"role"`
Content string `json:"content"`
}{
Role: "assistant",
Content: "Hello! I'm here to help.",
},
FinishReason: "stop",
},
},
Usage: struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}{
PromptTokens: 10,
CompletionTokens: 20,
TotalTokens: 30,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
// Create client with mock server URL
client := NewOpenAIClient("test-api-key", "gpt-4o", server.URL)
// Execute chat request
ctx := context.Background()
resp, err := client.Chat(ctx, ChatRequest{
Messages: []Message{
{Role: "user", Content: "Hello"},
},
Model: "gpt-4o",
})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if resp.Content != "Hello! I'm here to help." {
t.Errorf("Expected content 'Hello! I'm here to help.', got '%s'", resp.Content)
}
if resp.Model != "gpt-4o" {
t.Errorf("Expected model 'gpt-4o', got '%s'", resp.Model)
}
if resp.InputTokens != 10 {
t.Errorf("Expected 10 input tokens, got %d", resp.InputTokens)
}
if resp.OutputTokens != 20 {
t.Errorf("Expected 20 output tokens, got %d", resp.OutputTokens)
}
}
func TestOpenAIClient_Chat_APIError(t *testing.T) {
// Create a mock server that returns an error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error": {"message": "Invalid API key", "type": "invalid_request_error"}}`))
}))
defer server.Close()
client := NewOpenAIClient("invalid-key", "gpt-4o", server.URL)
ctx := context.Background()
_, err := client.Chat(ctx, ChatRequest{
Messages: []Message{{Role: "user", Content: "Hello"}},
})
if err == nil {
t.Error("Expected error for invalid API key")
}
}
func TestOpenAIClient_Chat_NetworkError(t *testing.T) {
// Create client pointing to non-existent server
client := NewOpenAIClient("test-key", "gpt-4o", "http://localhost:99999")
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err := client.Chat(ctx, ChatRequest{
Messages: []Message{{Role: "user", Content: "Hello"}},
})
if err == nil {
t.Error("Expected error for network failure")
}
}
func TestOpenAIClient_Chat_ContextCanceled(t *testing.T) {
// Create a slow mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
}))
defer server.Close()
client := NewOpenAIClient("test-key", "gpt-4o", server.URL)
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
_, err := client.Chat(ctx, ChatRequest{
Messages: []Message{{Role: "user", Content: "Hello"}},
})
if err == nil {
t.Error("Expected error for canceled context")
}
}
func TestOpenAIClient_Chat_WithSystemPrompt(t *testing.T) {
var receivedRequest map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewDecoder(r.Body).Decode(&receivedRequest)
resp := mockOpenAIResponse{
ID: "chatcmpl-123",
Model: "gpt-4o",
Choices: []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
}{
{Message: struct {
Role string `json:"role"`
Content string `json:"content"`
}{Role: "assistant", Content: "Response"}, FinishReason: "stop"},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewOpenAIClient("test-key", "gpt-4o", server.URL)
ctx := context.Background()
_, err := client.Chat(ctx, ChatRequest{
Messages: []Message{{Role: "user", Content: "Hello"}},
System: "You are a helpful assistant",
})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Verify system message was included
messages, ok := receivedRequest["messages"].([]interface{})
if !ok || len(messages) < 2 {
t.Error("Expected at least 2 messages (system + user)")
}
}
// Note: ListModels and TestConnection tests are not included here because
// the OpenAI client hardcodes the models endpoint URLs (api.openai.com/v1/models).
// To properly test these methods, the OpenAI client would need to be refactored
// to derive the models URL from the baseURL, similar to how Chat works.
//
// For now, these tests would require actual API keys to run E2E tests,
// which should be done in a separate integration test suite.