You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

133 lines
4.2KB

  1. package mapping
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "time"
  10. "qctextbuilder/internal/domain"
  11. "qctextbuilder/internal/llmruntime"
  12. )
  13. type stubSettingsReader struct {
  14. settings *domain.AppSettings
  15. err error
  16. }
  17. func (s *stubSettingsReader) GetSettings(context.Context) (*domain.AppSettings, error) {
  18. if s.err != nil {
  19. return nil, s.err
  20. }
  21. return s.settings, nil
  22. }
  23. func TestProviderAwareSuggestionGenerator_UsesActiveProviderModelAndKey(t *testing.T) {
  24. t.Parallel()
  25. var (
  26. gotPath string
  27. gotAuth string
  28. gotModel string
  29. gotTemperature float64
  30. gotMaxTokens float64
  31. gotMaxComp float64
  32. )
  33. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  34. gotPath = r.URL.Path
  35. gotAuth = r.Header.Get("Authorization")
  36. var payload map[string]any
  37. _ = json.NewDecoder(r.Body).Decode(&payload)
  38. gotModel, _ = payload["model"].(string)
  39. gotTemperature, _ = payload["temperature"].(float64)
  40. gotMaxTokens, _ = payload["max_tokens"].(float64)
  41. gotMaxComp, _ = payload["max_completion_tokens"].(float64)
  42. _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"{\"suggestions\":[{\"fieldPath\":\"text.textTitle_m1710_1\",\"value\":\"Provider Hero\",\"reason\":\"focused hero\"}]}"}}]}`))
  43. }))
  44. defer server.Close()
  45. generator := NewProviderAwareSuggestionGenerator(&stubSettingsReader{settings: &domain.AppSettings{
  46. LLMActiveProvider: domain.LLMProviderOpenAI,
  47. LLMActiveModel: "gpt-5.4",
  48. LLMBaseURL: server.URL,
  49. LLMTemperature: 0.65,
  50. LLMMaxTokens: 333,
  51. OpenAIAPIKeyEncrypted: "openai-key",
  52. }}, llmruntime.NewFactory(5*time.Second))
  53. result, err := generator.Generate(context.Background(), SuggestionRequest{
  54. Fields: []domain.TemplateField{
  55. {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
  56. },
  57. GlobalData: map[string]any{"companyName": "Muster AG"},
  58. Existing: map[string]string{},
  59. })
  60. if err != nil {
  61. t.Fatalf("generate failed: %v", err)
  62. }
  63. if got := result.ByFieldPath["text.textTitle_m1710_1"].Value; got != "Provider Hero" {
  64. t.Fatalf("unexpected value: %q", got)
  65. }
  66. if got := result.ByFieldPath["text.textTitle_m1710_1"].Source; got != domain.LLMProviderOpenAI {
  67. t.Fatalf("unexpected source: %q", got)
  68. }
  69. if gotPath != "/v1/chat/completions" {
  70. t.Fatalf("unexpected path: %s", gotPath)
  71. }
  72. if gotAuth != "Bearer openai-key" {
  73. t.Fatalf("unexpected auth header: %q", gotAuth)
  74. }
  75. if gotModel != "gpt-5.4" {
  76. t.Fatalf("unexpected model: %q", gotModel)
  77. }
  78. if gotTemperature != 0.65 {
  79. t.Fatalf("unexpected temperature: %v", gotTemperature)
  80. }
  81. if gotMaxTokens != 0 {
  82. t.Fatalf("unexpected max_tokens for openai gpt-5 model: %v", gotMaxTokens)
  83. }
  84. if gotMaxComp != 333 {
  85. t.Fatalf("unexpected max_completion_tokens: %v", gotMaxComp)
  86. }
  87. }
  88. func TestProviderAwareSuggestionGenerator_RequiresAPIKeyForNonOllama(t *testing.T) {
  89. t.Parallel()
  90. generator := NewProviderAwareSuggestionGenerator(&stubSettingsReader{settings: &domain.AppSettings{
  91. LLMActiveProvider: domain.LLMProviderAnthropic,
  92. LLMActiveModel: "claude-sonnet-4-5",
  93. }}, llmruntime.NewFactory(5*time.Second))
  94. _, err := generator.Generate(context.Background(), SuggestionRequest{
  95. Fields: []domain.TemplateField{{Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}},
  96. })
  97. if err == nil || !strings.Contains(err.Error(), "api key") {
  98. t.Fatalf("expected api key error, got: %v", err)
  99. }
  100. }
  101. func TestParseProviderSuggestions_AcceptsFencedJSON(t *testing.T) {
  102. t.Parallel()
  103. items, err := parseProviderSuggestions("```json\n{\"suggestions\":[{\"fieldPath\":\"a\",\"value\":\"b\"}]}\n```")
  104. if err != nil {
  105. t.Fatalf("parse failed: %v", err)
  106. }
  107. if len(items) != 1 || items[0].FieldPath != "a" || items[0].Value != "b" {
  108. t.Fatalf("unexpected parsed result: %+v", items)
  109. }
  110. }
  111. func TestParseProviderSuggestions_RejectsEmptyValue(t *testing.T) {
  112. t.Parallel()
  113. _, err := parseProviderSuggestions(`{"suggestions":[{"fieldPath":"a","value":""}]}`)
  114. if err == nil || !strings.Contains(err.Error(), "empty value") {
  115. t.Fatalf("expected empty value error, got: %v", err)
  116. }
  117. }