|
- package mapping
-
- import (
- "context"
- "encoding/json"
- "testing"
- "time"
-
- "qctextbuilder/internal/domain"
- "qctextbuilder/internal/qcclient"
- )
-
- func TestSuggestFieldValues_FillsEmptyMappedFields(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- {Path: "services.servicesTitle_r4830_8", KeyName: "servicesTitle_r4830_8", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionServices},
- {Path: "services.servicesDescription_r4830_9", KeyName: "servicesDescription_r4830_9", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionServices},
- {Path: "text.buttonText_c1165_1", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA},
- }
-
- result := SuggestFieldValues(SuggestionRequest{
- Fields: fields,
- GlobalData: map[string]any{
- "companyName": "Muster AG",
- "businessType": "Solar",
- },
- DraftContext: &domain.DraftContext{
- LLM: domain.DraftLLMContext{
- WebsiteSummary: "Wir planen und installieren Solaranlagen fuer KMU und Privatkunden.",
- StyleProfile: domain.DraftStyleProfile{
- ContentTone: "professionell",
- },
- },
- },
- Existing: map[string]string{},
- })
-
- if _, ok := result.ByFieldPath["text.textTitle_m1710_1"]; !ok {
- t.Fatalf("expected hero title suggestion")
- }
- if got := result.ByFieldPath["text.buttonText_c1165_1"].Value; got == "" {
- t.Fatalf("expected cta suggestion")
- }
- if got := result.ByFieldPath["services.servicesDescription_r4830_9"].Slot; got != "service_items[0].description" {
- t.Fatalf("unexpected slot: %q", got)
- }
- }
-
- func TestSuggestFieldValues_RespectsExistingValues(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- }
-
- result := SuggestFieldValues(SuggestionRequest{
- Fields: fields,
- GlobalData: map[string]any{
- "companyName": "Muster AG",
- },
- Existing: map[string]string{
- "text.textTitle_m1710_1": "Schon gesetzt",
- },
- })
-
- if len(result.Suggestions) != 0 {
- t.Fatalf("expected no suggestions, got %d", len(result.Suggestions))
- }
- }
-
- func TestGenerateAllSuggestions_IncludesFilledFields(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- }
- state := GenerateAllSuggestions(context.Background(), nil, SuggestionRequest{
- Fields: fields,
- GlobalData: map[string]any{
- "companyName": "Muster AG",
- },
- Existing: map[string]string{
- "text.textTitle_m1710_1": "Bereits gesetzt",
- },
- }, domain.DraftSuggestionState{}, time.Now().UTC())
-
- if _, ok := state.ByFieldPath["text.textTitle_m1710_1"]; !ok {
- t.Fatalf("expected suggestion for filled field")
- }
- }
-
- func TestApplySuggestionsToEmptyFields_DoesNotOverwriteExisting(t *testing.T) {
- t.Parallel()
-
- now := time.Now().UTC()
- values, state := ApplySuggestionsToEmptyFields(map[string]string{
- "field.hero": "Custom",
- }, domain.DraftSuggestionState{
- ByFieldPath: map[string]domain.DraftSuggestion{
- "field.hero": {
- FieldPath: "field.hero",
- Value: "Suggestion",
- Status: domain.DraftSuggestionStatusSuggested,
- },
- "field.cta": {
- FieldPath: "field.cta",
- Value: "Jetzt anfragen",
- Status: domain.DraftSuggestionStatusSuggested,
- },
- },
- }, now)
-
- if got := values["field.hero"]; got != "Custom" {
- t.Fatalf("expected existing value unchanged, got %q", got)
- }
- if got := values["field.cta"]; got != "Jetzt anfragen" {
- t.Fatalf("expected empty value filled, got %q", got)
- }
- if got := state.ByFieldPath["field.hero"].Status; got != domain.DraftSuggestionStatusSuggested {
- t.Fatalf("expected hero status unchanged, got %q", got)
- }
- if got := state.ByFieldPath["field.cta"].Status; got != domain.DraftSuggestionStatusApplied {
- t.Fatalf("expected cta status applied, got %q", got)
- }
- }
-
- func TestApplyAllSuggestions_OverwritesExisting(t *testing.T) {
- t.Parallel()
-
- now := time.Now().UTC()
- values, state := ApplyAllSuggestions(map[string]string{
- "field.hero": "Custom",
- }, domain.DraftSuggestionState{
- ByFieldPath: map[string]domain.DraftSuggestion{
- "field.hero": {
- FieldPath: "field.hero",
- Value: "Suggestion",
- Status: domain.DraftSuggestionStatusSuggested,
- },
- "field.cta": {
- FieldPath: "field.cta",
- Value: "Jetzt anfragen",
- Status: domain.DraftSuggestionStatusSuggested,
- },
- },
- }, now)
-
- if got := values["field.hero"]; got != "Suggestion" {
- t.Fatalf("expected existing value overwritten, got %q", got)
- }
- if got := values["field.cta"]; got != "Jetzt anfragen" {
- t.Fatalf("expected cta applied, got %q", got)
- }
- if got := state.ByFieldPath["field.hero"].Status; got != domain.DraftSuggestionStatusApplied {
- t.Fatalf("expected hero status applied, got %q", got)
- }
- if got := state.ByFieldPath["field.cta"].Status; got != domain.DraftSuggestionStatusApplied {
- t.Fatalf("expected cta status applied, got %q", got)
- }
- }
-
- func TestRegenerateFieldSuggestion_OnlyChangesTargetField(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- {Path: "text.buttonText_c1165_1", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA},
- }
- current := domain.DraftSuggestionState{
- ByFieldPath: map[string]domain.DraftSuggestion{
- "text.textTitle_m1710_1": {FieldPath: "text.textTitle_m1710_1", Value: "Old Hero"},
- "text.buttonText_c1165_1": {FieldPath: "text.buttonText_c1165_1", Value: "Old CTA"},
- },
- }
-
- updated := RegenerateFieldSuggestion(context.Background(), nil, SuggestionRequest{
- Fields: fields,
- GlobalData: map[string]any{
- "companyName": "Muster AG",
- },
- }, current, "text.buttonText_c1165_1", time.Now().UTC())
-
- if got := updated.ByFieldPath["text.textTitle_m1710_1"].Value; got != "Old Hero" {
- t.Fatalf("expected untargeted field unchanged, got %q", got)
- }
- if got := updated.ByFieldPath["text.buttonText_c1165_1"].Value; got == "" || got == "Old CTA" {
- t.Fatalf("expected target field regenerated, got %q", got)
- }
- }
-
- func TestGenerateAllSuggestions_UsesGeneratorFallbackWhenPrimaryPartial(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- {Path: "text.buttonText_c1165_1", Section: "text", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA},
- }
- generator := NewCompositeSuggestionGenerator(
- NewLLMSuggestionGenerator(&stubQCClient{
- generateContent: qcclient.GenerateContentData{
- "text": {
- "textTitle_m1710_1": "LLM Hero",
- },
- },
- }),
- NewRuleBasedSuggestionGenerator(),
- )
-
- state := GenerateAllSuggestions(context.Background(), generator, SuggestionRequest{
- TemplateID: 101,
- Fields: fields,
- GlobalData: map[string]any{"companyName": "Muster AG"},
- Existing: map[string]string{},
- }, domain.DraftSuggestionState{}, time.Now().UTC())
-
- hero := state.ByFieldPath["text.textTitle_m1710_1"]
- if hero.Source != domain.DraftSuggestionSourceLLM {
- t.Fatalf("expected hero source llm, got %q", hero.Source)
- }
- cta := state.ByFieldPath["text.buttonText_c1165_1"]
- if cta.Source != domain.DraftSuggestionSourceFallbackRuleBased {
- t.Fatalf("expected cta source fallback rule-based, got %q", cta.Source)
- }
- }
-
- func TestGenerateAllSuggestions_FallsBackWhenLLMReturnsInvalidValueType(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- }
- generator := NewCompositeSuggestionGenerator(
- NewLLMSuggestionGenerator(&stubQCClient{
- generateContent: qcclient.GenerateContentData{
- "text": {
- "textTitle_m1710_1": true,
- },
- },
- }),
- NewRuleBasedSuggestionGenerator(),
- )
-
- state := GenerateAllSuggestions(context.Background(), generator, SuggestionRequest{
- TemplateID: 202,
- Fields: fields,
- GlobalData: map[string]any{"companyName": "Muster AG"},
- Existing: map[string]string{},
- }, domain.DraftSuggestionState{}, time.Now().UTC())
-
- hero := state.ByFieldPath["text.textTitle_m1710_1"]
- if hero.Value == "" {
- t.Fatalf("expected fallback suggestion value")
- }
- if hero.Source != domain.DraftSuggestionSourceFallbackRuleBased {
- t.Fatalf("expected fallback source, got %q", hero.Source)
- }
- }
-
- func TestGenerateAllSuggestions_PreservesSourceFromByFieldPathOnStateApply(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- {Path: "text.buttonText_c1165_1", Section: "text", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA},
- }
- generator := &stubSuggestionGenerator{
- result: SuggestionResult{
- Suggestions: []Suggestion{
- {FieldPath: "text.textTitle_m1710_1", Value: "Provider Hero", Source: ""},
- {FieldPath: "text.buttonText_c1165_1", Value: "Fallback CTA", Source: ""},
- },
- ByFieldPath: map[string]Suggestion{
- "text.textTitle_m1710_1": {FieldPath: "text.textTitle_m1710_1", Value: "Provider Hero", Source: domain.LLMProviderOpenAI},
- "text.buttonText_c1165_1": {FieldPath: "text.buttonText_c1165_1", Value: "Fallback CTA", Source: ""},
- },
- },
- }
-
- state := GenerateAllSuggestions(context.Background(), generator, SuggestionRequest{
- Fields: fields,
- Existing: map[string]string{},
- IncludeFilled: true,
- }, domain.DraftSuggestionState{}, time.Now().UTC())
-
- if got := state.ByFieldPath["text.textTitle_m1710_1"].Source; got != domain.LLMProviderOpenAI {
- t.Fatalf("expected provider source preserved from generated result, got %q", got)
- }
- if got := state.ByFieldPath["text.buttonText_c1165_1"].Source; got != "unknown" {
- t.Fatalf("expected unknown source only when suggestion source is empty, got %q", got)
- }
- }
-
- func TestGenerateAllSuggestions_ReplacesStaleRuleBasedSourceWithProviderSource(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- }
- current := domain.DraftSuggestionState{
- ByFieldPath: map[string]domain.DraftSuggestion{
- "text.textTitle_m1710_1": {
- FieldPath: "text.textTitle_m1710_1",
- Value: "Old fallback hero",
- Source: domain.DraftSuggestionSourceFallbackRuleBased,
- Status: domain.DraftSuggestionStatusSuggested,
- },
- },
- }
- generator := &stubSuggestionGenerator{
- result: SuggestionResult{
- Suggestions: []Suggestion{
- {FieldPath: "text.textTitle_m1710_1", Value: "Provider Hero", Source: domain.LLMProviderOpenAI},
- },
- ByFieldPath: map[string]Suggestion{
- "text.textTitle_m1710_1": {FieldPath: "text.textTitle_m1710_1", Value: "Provider Hero", Source: domain.LLMProviderOpenAI},
- },
- },
- }
-
- state := GenerateAllSuggestions(context.Background(), generator, SuggestionRequest{
- Fields: fields,
- Existing: map[string]string{},
- IncludeFilled: true,
- }, current, time.Now().UTC())
-
- hero := state.ByFieldPath["text.textTitle_m1710_1"]
- if hero.Source != domain.LLMProviderOpenAI {
- t.Fatalf("expected stale fallback source to be replaced by provider source, got %q", hero.Source)
- }
- if hero.Value != "Provider Hero" {
- t.Fatalf("expected provider value to replace stale fallback value, got %q", hero.Value)
- }
- }
-
- func TestCompositeSuggestionGenerator_NoFallbackWhenPrimaryCoversAllTargets(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- {Path: "text.buttonText_c1165_1", Section: "text", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA},
- }
- primary := &stubSuggestionGenerator{
- result: SuggestionResult{
- Suggestions: []Suggestion{
- {FieldPath: "text.textTitle_m1710_1", Value: "Primary Hero", Source: domain.DraftSuggestionSourceLLM},
- {FieldPath: "text.buttonText_c1165_1", Value: "Primary CTA", Source: domain.DraftSuggestionSourceLLM},
- },
- },
- }
- fallback := &stubSuggestionGenerator{
- result: SuggestionResult{
- Suggestions: []Suggestion{
- {FieldPath: "text.textTitle_m1710_1", Value: "Fallback Hero", Source: domain.DraftSuggestionSourceFallbackRuleBased},
- },
- },
- }
- generator := NewCompositeSuggestionGenerator(primary, fallback)
-
- result, err := generator.Generate(context.Background(), SuggestionRequest{
- Fields: fields,
- Existing: map[string]string{},
- IncludeFilled: true,
- })
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if fallback.callCount != 0 {
- t.Fatalf("expected no fallback call, got %d", fallback.callCount)
- }
- if len(result.Suggestions) != 2 {
- t.Fatalf("expected 2 suggestions, got %d", len(result.Suggestions))
- }
- }
-
- func TestCompositeSuggestionGenerator_FallbackReceivesOnlyMissingTargets(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- {Path: "text.buttonText_c1165_1", Section: "text", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA},
- }
- primary := &stubSuggestionGenerator{
- result: SuggestionResult{
- Suggestions: []Suggestion{
- {FieldPath: "text.textTitle_m1710_1", Value: "Primary Hero", Source: domain.DraftSuggestionSourceLLM},
- },
- },
- }
- fallback := &stubSuggestionGenerator{
- result: SuggestionResult{
- Suggestions: []Suggestion{
- {FieldPath: "text.buttonText_c1165_1", Value: "Fallback CTA", Source: domain.DraftSuggestionSourceFallbackRuleBased},
- },
- },
- }
- generator := NewCompositeSuggestionGenerator(primary, fallback)
-
- result, err := generator.Generate(context.Background(), SuggestionRequest{
- Fields: fields,
- Existing: map[string]string{},
- IncludeFilled: true,
- })
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if fallback.callCount != 1 {
- t.Fatalf("expected one fallback call, got %d", fallback.callCount)
- }
- if len(fallback.lastReq.Fields) != 1 || fallback.lastReq.Fields[0].Path != "text.buttonText_c1165_1" {
- t.Fatalf("expected fallback request only for missing CTA field, got %+v", fallback.lastReq.Fields)
- }
- if got := result.ByFieldPath["text.textTitle_m1710_1"].Source; got != domain.DraftSuggestionSourceLLM {
- t.Fatalf("expected primary source on hero, got %q", got)
- }
- if got := result.ByFieldPath["text.buttonText_c1165_1"].Source; got != domain.DraftSuggestionSourceFallbackRuleBased {
- t.Fatalf("expected fallback source on cta, got %q", got)
- }
- }
-
- func TestCompositeSuggestionGenerator_PrimaryWinsOverFallbackForSameField(t *testing.T) {
- t.Parallel()
-
- fields := []domain.TemplateField{
- {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero},
- {Path: "text.buttonText_c1165_1", Section: "text", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA},
- }
- primary := &stubSuggestionGenerator{
- result: SuggestionResult{
- Suggestions: []Suggestion{
- {FieldPath: "text.textTitle_m1710_1", Value: "Primary Hero", Source: domain.DraftSuggestionSourceLLM},
- },
- },
- }
- fallback := &stubSuggestionGenerator{
- result: SuggestionResult{
- Suggestions: []Suggestion{
- {FieldPath: "text.textTitle_m1710_1", Value: "Fallback Hero", Source: domain.DraftSuggestionSourceFallbackRuleBased},
- {FieldPath: "text.buttonText_c1165_1", Value: "Fallback CTA", Source: domain.DraftSuggestionSourceFallbackRuleBased},
- },
- },
- }
- generator := NewCompositeSuggestionGenerator(primary, fallback)
-
- result, err := generator.Generate(context.Background(), SuggestionRequest{
- Fields: fields,
- Existing: map[string]string{},
- IncludeFilled: true,
- })
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got := result.ByFieldPath["text.textTitle_m1710_1"]; got.Value != "Primary Hero" || got.Source != domain.DraftSuggestionSourceLLM {
- t.Fatalf("expected primary hero suggestion to win, got %+v", got)
- }
- }
-
- type stubQCClient struct {
- generateContent qcclient.GenerateContentData
- generateErr error
- }
-
- func (s *stubQCClient) Health(context.Context) error { return nil }
-
- func (s *stubQCClient) ListAITemplates(context.Context) ([]qcclient.Template, error) {
- return nil, nil
- }
-
- func (s *stubQCClient) GetTemplate(context.Context, int64) (*qcclient.Template, error) {
- return nil, nil
- }
-
- func (s *stubQCClient) GetTemplateSchema(context.Context, int64) (json.RawMessage, error) {
- return nil, nil
- }
-
- func (s *stubQCClient) GenerateContent(context.Context, qcclient.GenerateContentRequest) (qcclient.GenerateContentData, json.RawMessage, error) {
- if s.generateErr != nil {
- return nil, nil, s.generateErr
- }
- return s.generateContent, nil, nil
- }
-
- func (s *stubQCClient) CreateSite(context.Context, qcclient.CreateSiteRequest) (*qcclient.CreateSiteResponseData, json.RawMessage, error) {
- return nil, nil, nil
- }
-
- func (s *stubQCClient) GetJob(context.Context, int64) (*qcclient.JobStatusData, json.RawMessage, error) {
- return nil, nil, nil
- }
-
- func (s *stubQCClient) GetEditorURL(context.Context, int64) (*qcclient.SiteEditorLoginData, json.RawMessage, error) {
- return nil, nil, nil
- }
-
- type stubSuggestionGenerator struct {
- result SuggestionResult
- err error
- callCount int
- lastReq SuggestionRequest
- }
-
- func (s *stubSuggestionGenerator) Generate(_ context.Context, req SuggestionRequest) (SuggestionResult, error) {
- s.callCount++
- s.lastReq = req
- if s.err != nil {
- return SuggestionResult{}, s.err
- }
- return s.result, nil
- }
|