package mapping import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "qctextbuilder/internal/domain" "qctextbuilder/internal/llmruntime" ) type stubSettingsReader struct { settings *domain.AppSettings err error } func (s *stubSettingsReader) GetSettings(context.Context) (*domain.AppSettings, error) { if s.err != nil { return nil, s.err } return s.settings, nil } func TestProviderAwareSuggestionGenerator_UsesActiveProviderModelAndKey(t *testing.T) { t.Parallel() var ( gotPath string gotAuth string gotModel string gotTemperature float64 gotMaxTokens float64 ) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotAuth = r.Header.Get("Authorization") var payload map[string]any _ = json.NewDecoder(r.Body).Decode(&payload) gotModel, _ = payload["model"].(string) gotTemperature, _ = payload["temperature"].(float64) gotMaxTokens, _ = payload["max_tokens"].(float64) _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"{\"suggestions\":[{\"fieldPath\":\"text.textTitle_m1710_1\",\"value\":\"Provider Hero\",\"reason\":\"focused hero\"}]}"}}]}`)) })) defer server.Close() generator := NewProviderAwareSuggestionGenerator(&stubSettingsReader{settings: &domain.AppSettings{ LLMActiveProvider: domain.LLMProviderOpenAI, LLMActiveModel: "gpt-5.4", LLMBaseURL: server.URL, LLMTemperature: 0.65, LLMMaxTokens: 333, OpenAIAPIKeyEncrypted: "openai-key", }}, llmruntime.NewFactory(5*time.Second)) result, err := generator.Generate(context.Background(), SuggestionRequest{ Fields: []domain.TemplateField{ {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}, }, GlobalData: map[string]any{"companyName": "Muster AG"}, Existing: map[string]string{}, }) if err != nil { t.Fatalf("generate failed: %v", err) } if got := result.ByFieldPath["text.textTitle_m1710_1"].Value; got != "Provider Hero" { t.Fatalf("unexpected value: %q", got) } if got := result.ByFieldPath["text.textTitle_m1710_1"].Source; got != domain.LLMProviderOpenAI { t.Fatalf("unexpected source: %q", got) } if gotPath != "/v1/chat/completions" { t.Fatalf("unexpected path: %s", gotPath) } if gotAuth != "Bearer openai-key" { t.Fatalf("unexpected auth header: %q", gotAuth) } if gotModel != "gpt-5.4" { t.Fatalf("unexpected model: %q", gotModel) } if gotTemperature != 0.65 { t.Fatalf("unexpected temperature: %v", gotTemperature) } if gotMaxTokens != 333 { t.Fatalf("unexpected max_tokens: %v", gotMaxTokens) } } func TestProviderAwareSuggestionGenerator_RequiresAPIKeyForNonOllama(t *testing.T) { t.Parallel() generator := NewProviderAwareSuggestionGenerator(&stubSettingsReader{settings: &domain.AppSettings{ LLMActiveProvider: domain.LLMProviderAnthropic, LLMActiveModel: "claude-sonnet-4-5", }}, llmruntime.NewFactory(5*time.Second)) _, err := generator.Generate(context.Background(), SuggestionRequest{ Fields: []domain.TemplateField{{Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}}, }) if err == nil || !strings.Contains(err.Error(), "api key") { t.Fatalf("expected api key error, got: %v", err) } } func TestParseProviderSuggestions_AcceptsFencedJSON(t *testing.T) { t.Parallel() items, err := parseProviderSuggestions("```json\n{\"suggestions\":[{\"fieldPath\":\"a\",\"value\":\"b\"}]}\n```") if err != nil { t.Fatalf("parse failed: %v", err) } if len(items) != 1 || items[0].FieldPath != "a" || items[0].Value != "b" { t.Fatalf("unexpected parsed result: %+v", items) } } func TestParseProviderSuggestions_RejectsEmptyValue(t *testing.T) { t.Parallel() _, err := parseProviderSuggestions(`{"suggestions":[{"fieldPath":"a","value":""}]}`) if err == nil || !strings.Contains(err.Error(), "empty value") { t.Fatalf("expected empty value error, got: %v", err) } }