package backend import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "testing" ) func TestValidReasoningEffort(t *testing.T) { cases := map[string]bool{ "": true, // unset means "use default" "low": true, "medium": true, "high": true, "LOW": false, // case-sensitive; profiles use lowercase "max": false, "hihg": false, // typo must be rejected, not silently ignored } for in, want := range cases { if got := ValidReasoningEffort(in); got != want { t.Errorf("ValidReasoningEffort(%q) = %v, want %v", in, got, want) } } } func TestEffortThinkingBudget(t *testing.T) { cases := map[string]int{ EffortLow: 4096, EffortMedium: 8192, EffortHigh: 16384, "": 0, "bogus": 0, } for in, want := range cases { if got := EffortThinkingBudget(in); got != want { t.Errorf("EffortThinkingBudget(%q) = %d, want %d", in, got, want) } } } func TestResolvedThinkingBudget(t *testing.T) { // Explicit budget wins over effort. p := GenerationParams{ThinkingBudget: 5000, ReasoningEffort: EffortHigh} if got := p.ResolvedThinkingBudget(); got != 5000 { t.Errorf("explicit budget: got %d, want 5000", got) } // Derived from effort when budget unset. p = GenerationParams{ReasoningEffort: EffortHigh} if got := p.ResolvedThinkingBudget(); got != 16384 { t.Errorf("derived budget: got %d, want 16384", got) } // Neither set → 0 (no thinking). p = GenerationParams{} if got := p.ResolvedThinkingBudget(); got != 0 { t.Errorf("unset: got %d, want 0", got) } } // captureAnthropicRequest drives AnthropicBackend.ChatStream against a test // server and returns the decoded request body. func captureAnthropicRequest(t *testing.T, params GenerationParams) anthropicRequest { t.Helper() var raw []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "text/event-stream") // Minimal well-formed stream so the reader terminates cleanly. w.Write([]byte("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")) })) defer srv.Close() b, err := NewAnthropic("key") if err != nil { t.Fatal(err) } u, _ := url.Parse(srv.URL) b.baseURL = u ch, err := b.ChatStream(context.Background(), []Message{{Role: "user", Content: "audit this"}}, nil, params) if err != nil { t.Fatal(err) } for range ch { //nolint:revive } var areq anthropicRequest if err := json.Unmarshal(raw, &areq); err != nil { t.Fatalf("decode request: %v (body=%s)", err, raw) } return areq } func TestAnthropic_EffortEnablesThinking(t *testing.T) { temp := 0.3 areq := captureAnthropicRequest(t, GenerationParams{ ReasoningEffort: EffortHigh, MaxTokens: 8192, Temperature: &temp, }) if areq.Thinking == nil { t.Fatal("thinking not enabled for reasoningEffort=high") } if areq.Thinking.BudgetTokens != 16384 { t.Errorf("budget_tokens = %d, want 16384", areq.Thinking.BudgetTokens) } // max_tokens must be grown above the thinking budget (budget < max_tokens). if areq.MaxTokens <= areq.Thinking.BudgetTokens { t.Errorf("max_tokens (%d) must exceed thinking budget (%d)", areq.MaxTokens, areq.Thinking.BudgetTokens) } // Extended thinking requires temperature=1. if areq.Temperature == nil || *areq.Temperature != 1.0 { t.Errorf("temperature = %v, want 1.0 when thinking enabled", areq.Temperature) } } func TestAnthropic_NoEffortNoThinking(t *testing.T) { temp := 0.3 areq := captureAnthropicRequest(t, GenerationParams{MaxTokens: 8192, Temperature: &temp}) if areq.Thinking != nil { t.Errorf("thinking should be disabled when no effort/budget set") } // Temperature passes through untouched when thinking is off. if areq.Temperature == nil || *areq.Temperature != 0.3 { t.Errorf("temperature = %v, want 0.3", areq.Temperature) } } func TestAnthropic_ExplicitBudgetOverridesEffort(t *testing.T) { areq := captureAnthropicRequest(t, GenerationParams{ ReasoningEffort: EffortLow, ThinkingBudget: 20000, MaxTokens: 8192, }) if areq.Thinking == nil || areq.Thinking.BudgetTokens != 20000 { t.Fatalf("budget_tokens = %v, want 20000 (explicit overrides effort)", areq.Thinking) } if areq.MaxTokens <= 20000 { t.Errorf("max_tokens (%d) must exceed explicit budget (20000)", areq.MaxTokens) } }