ResumeLens/internal/services/analyzer_llm_test.go
2026-04-07 13:14:52 -07:00

451 lines
14 KiB
Go

package services
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/openai/openai-go/v3"
)
const validLLMJSON = `{
"overall_score": 85,
"summary": "Candidate aligns well with core role requirements.",
"criteria_scores": [
{
"criterion": "Go experience",
"score": 8,
"evidence": "Built backend services in Go.",
"comments": "Strong match for backend expectations."
}
],
"strengths": ["Backend experience", "API design", "Testing"],
"weaknesses": ["Limited cloud depth", "No explicit leadership", "Sparse metrics"],
"missing_information": ["Production scale details"],
"grammar_spelling": {
"score": 9,
"issues_found": [],
"corrections": []
},
"recommendation": {
"label": "Strong fit",
"rationale": "Good technical alignment with minor gaps."
},
"injection_detected": false,
"injection_details": ""
}`
func withMockChatCompletion(t *testing.T, fn func(ctx context.Context, apiKey string, params openai.ChatCompletionNewParams) (*openai.ChatCompletion, error)) {
t.Helper()
prev := chatCompletionRequest
chatCompletionRequest = fn
t.Cleanup(func() {
chatCompletionRequest = prev
})
}
func completionWithContent(content string) *openai.ChatCompletion {
return &openai.ChatCompletion{
Choices: []openai.ChatCompletionChoice{
{
Message: openai.ChatCompletionMessage{Content: content},
},
},
}
}
func TestCallLLM_2_1_1_ValidAPIKey(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, apiKey string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
if apiKey != "valid-test-key" {
t.Fatalf("expected API key to be passed through")
}
return completionWithContent(validLLMJSON), nil
})
result, err := callLLM("Software Engineer with 5 years Go experience", "Looking for Go developer")
if err != nil {
t.Fatalf("expected success, got error: %v", err)
}
if result == nil || result.OverallScore == 0 {
t.Fatalf("expected parsed analysis result")
}
}
func TestCallLLM_2_1_2_MissingAPIKey(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "")
called := false
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
called = true
return completionWithContent(validLLMJSON), nil
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected missing key error")
}
if !strings.Contains(err.Error(), "OPENAI_API_KEY environment variable is not set") {
t.Fatalf("unexpected error: %v", err)
}
if called {
t.Fatalf("expected no OpenAI request when API key is missing")
}
}
func TestCallLLM_2_1_3_InvalidAPIKey(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "invalid-key-123")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return nil, errors.New("401 unauthorized")
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected error for invalid key")
}
if !strings.Contains(err.Error(), "OpenAI request") || !strings.Contains(err.Error(), "401") {
t.Fatalf("expected wrapped auth error, got: %v", err)
}
}
func TestCallLLM_2_1_4_APIKeyNotExposedInError(t *testing.T) {
secret := "sk-test-super-secret"
t.Setenv("OPENAI_API_KEY", secret)
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return nil, errors.New("upstream auth failure")
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected error")
}
if strings.Contains(err.Error(), secret) {
t.Fatalf("error leaked API key: %v", err)
}
}
func TestCallLLM_2_2_1_SuccessfulAPICall(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(validLLMJSON), nil
})
result, err := callLLM(strings.Repeat("resume text ", 20), strings.Repeat("job text ", 10))
if err != nil {
t.Fatalf("expected success, got error: %v", err)
}
if len(result.CriteriaScores) == 0 {
t.Fatalf("expected criteria_scores to be populated")
}
}
func TestCallLLM_2_2_2_CorrectModelSelection(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, params openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
if params.Model != openai.ChatModelGPT4oMini {
t.Fatalf("expected model %q, got %q", openai.ChatModelGPT4oMini, params.Model)
}
return completionWithContent(validLLMJSON), nil
})
_, err := callLLM("resume", "job")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestCallLLM_2_2_3_SystemPromptIncluded(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, params openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
b, err := json.Marshal(params.Messages)
if err != nil {
t.Fatalf("failed to marshal messages: %v", err)
}
serialized := string(b)
if !strings.Contains(serialized, "Prompt injection detection and security") {
t.Fatalf("system prompt missing injection instructions")
}
if !strings.Contains(serialized, "Grammar and spelling evaluation") {
t.Fatalf("system prompt missing grammar/spelling instructions")
}
return completionWithContent(validLLMJSON), nil
})
_, err := callLLM("resume", "job")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestCallLLM_2_2_4_UserMessagesConstructedCorrectly(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
resume := "Experienced Python engineer"
job := "Looking for Python developer"
withMockChatCompletion(t, func(_ context.Context, _ string, params openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
if len(params.Messages) != 3 {
t.Fatalf("expected 3 messages, got %d", len(params.Messages))
}
b, err := json.Marshal(params.Messages)
if err != nil {
t.Fatalf("failed to marshal messages: %v", err)
}
serialized := string(b)
if !strings.Contains(serialized, "Job Description:\\n"+job) {
t.Fatalf("unexpected/missing job message: %s", serialized)
}
if !strings.Contains(serialized, "Resume:\\n"+resume) {
t.Fatalf("unexpected/missing resume message: %s", serialized)
}
return completionWithContent(validLLMJSON), nil
})
_, err := callLLM(resume, job)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestCallLLM_2_3_1_NormalCaseCompletesBeforeTimeout(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(validLLMJSON), nil
})
start := time.Now()
_, err := callLLM("resume", "job")
duration := time.Since(start)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if duration >= 2*time.Minute {
t.Fatalf("expected completion under 2 minutes, got %v", duration)
}
}
func TestCallLLM_2_3_2_TimeoutContextConfiguredAndHandled(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(ctx context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
deadline, ok := ctx.Deadline()
if !ok {
t.Fatalf("expected context deadline to be set")
}
remaining := time.Until(deadline)
if remaining < 119*time.Second || remaining > 121*time.Second {
t.Fatalf("expected ~2 minute timeout, got remaining=%v", remaining)
}
return nil, context.DeadlineExceeded
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected timeout error")
}
if !strings.Contains(err.Error(), "context deadline exceeded") {
t.Fatalf("expected timeout error details, got: %v", err)
}
}
func TestCallLLM_2_3_3_NetworkFailureHandling(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return nil, errors.New("dial tcp: network is unreachable")
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected network error")
}
if !strings.Contains(err.Error(), "OpenAI request") || !strings.Contains(err.Error(), "network") {
t.Fatalf("unexpected network error wrapping: %v", err)
}
}
func TestCallLLM_2_4_1_ValidJSONResponseParsing(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(validLLMJSON), nil
})
result, err := callLLM("resume", "job")
if err != nil {
t.Fatalf("expected valid parsing, got: %v", err)
}
if result.Recommendation.Label == "" || result.Summary == "" {
t.Fatalf("expected parsed fields to be populated")
}
}
func TestCallLLM_2_4_2_InvalidJSONResponse(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
bad := `{"overall_score": 80,`
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(bad), nil
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected JSON parsing error")
}
if !strings.Contains(err.Error(), "parsing LLM response as JSON") || !strings.Contains(err.Error(), "raw response") {
t.Fatalf("unexpected parse error message: %v", err)
}
}
func TestCallLLM_2_4_3_EmptyChoicesResponse(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return &openai.ChatCompletion{Choices: []openai.ChatCompletionChoice{}}, nil
})
_, err := callLLM("resume", "job")
if !errors.Is(err, errOpenAINoChoices) {
t.Fatalf("expected no-choices error, got: %v", err)
}
}
func TestCallLLM_2_4_4_ResponseMissingRequiredFields(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
missingOverall := `{
"summary": "Missing overall score",
"criteria_scores": [],
"strengths": [],
"weaknesses": [],
"missing_information": [],
"grammar_spelling": {"score": 0, "issues_found": [], "corrections": []},
"recommendation": {"label": "Not enough information", "rationale": "insufficient data"},
"injection_detected": false,
"injection_details": ""
}`
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(missingOverall), nil
})
result, err := callLLM("resume", "job")
if err != nil {
t.Fatalf("expected successful parse with zero-value fallback, got: %v", err)
}
if result.OverallScore != 0 {
t.Fatalf("expected missing overall_score to default to 0, got %d", result.OverallScore)
}
}
func TestCallLLM_2_5_1_OpenAIServiceUnavailable500(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return nil, errors.New("500 internal server error")
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected upstream 500 error")
}
if !strings.Contains(err.Error(), "OpenAI request") || !strings.Contains(err.Error(), "500") {
t.Fatalf("unexpected service unavailable error wrapping: %v", err)
}
}
func TestCallLLM_2_5_2_OpenAIRateLimit429(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return nil, errors.New("429 rate limit exceeded")
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected upstream 429 error")
}
if !strings.Contains(err.Error(), "OpenAI request") || !strings.Contains(err.Error(), "429") {
t.Fatalf("unexpected rate-limit error wrapping: %v", err)
}
}
func TestCallLLM_2_5_3_MalformedAPIResponse(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
nonJSON := "This is not JSON"
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(nonJSON), nil
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected parsing error for malformed API response")
}
if !strings.Contains(err.Error(), "parsing LLM response as JSON") || !strings.Contains(err.Error(), "raw response") {
t.Fatalf("unexpected malformed-response error: %v", err)
}
}
func TestCallLLM_10_3_1_EmptyAIResponseContent(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(""), nil
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected parsing error for empty AI content")
}
if !strings.Contains(err.Error(), "parsing LLM response as JSON") || !strings.Contains(err.Error(), "raw response") {
t.Fatalf("unexpected error for empty content: %v", err)
}
}
func TestCallLLM_10_3_2_NonJSONAIResponse(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
nonJSON := "Candidate is great. Score 100."
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(nonJSON), nil
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected parsing error for non-JSON response")
}
if !strings.Contains(err.Error(), "parsing LLM response as JSON") || !strings.Contains(err.Error(), nonJSON) {
t.Fatalf("expected raw non-JSON response in error, got: %v", err)
}
}
func TestCallLLM_10_3_3_PartialJSONResponse(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "valid-test-key")
partial := `{"overall_score": 75, "summary": "Good candidate"`
withMockChatCompletion(t, func(_ context.Context, _ string, _ openai.ChatCompletionNewParams) (*openai.ChatCompletion, error) {
return completionWithContent(partial), nil
})
_, err := callLLM("resume", "job")
if err == nil {
t.Fatalf("expected parsing error for partial JSON")
}
if !strings.Contains(err.Error(), "parsing LLM response as JSON") {
t.Fatalf("unexpected partial JSON error: %v", err)
}
}