- 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
245 lines
6.6 KiB
Go
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.
|
|
|