package processing import ( "context" "strings" "testing" "unicode/utf8" ) func TestFillMetaFromResult_buildsTitleAndDesc(t *testing.T) { t.Parallel() title, desc := fillMetaFromResult(StepResult{ ProcessedName: "Drill 18V", Category: "Tools", ProcessedDescription: "Cordless drill with battery pack for DIY projects.", ProcessedAttributes: map[string]any{"brand": "Bosch"}, }) if title == "" || desc == "" { t.Fatal("expected non-empty meta") } if !strings.Contains(title, "Drill 18V") { t.Fatalf("title missing name: %q", title) } if !strings.Contains(title, "Bosch") { t.Fatalf("title missing brand: %q", title) } if utf8.RuneCountInString(title) > metaTitleMaxChars { t.Fatalf("title too long: %d", utf8.RuneCountInString(title)) } if utf8.RuneCountInString(desc) > metaDescriptionMaxChars { t.Fatalf("desc too long: %d", utf8.RuneCountInString(desc)) } } func TestFillMetaFromResult_usesCategoryNameNotUniqueID(t *testing.T) { t.Parallel() title, _ := fillMetaFromResult(StepResult{ ProcessedName: "Gorenje GEC5A21WG", Category: "50", CategoryName: "Štedilniki", ProcessedAttributes: map[string]any{ "brand": "Gorenje", }, }) if strings.Contains(title, "| 50") || strings.HasSuffix(title, "50") { t.Fatalf("meta title must not use unique_id: %q", title) } if !strings.Contains(title, "Štedilniki") { t.Fatalf("meta title missing display name: %q", title) } // Digits-only Category without CategoryName must omit category from title. title2, _ := fillMetaFromResult(StepResult{ ProcessedName: "Cooker", Category: "50", }) if strings.Contains(title2, "50") { t.Fatalf("digits-only unique_id must not appear in meta: %q", title2) } } func TestTruncateMetaDescription_wordBoundary(t *testing.T) { t.Parallel() // 160+ runes with a clear word break near 155. words := strings.Repeat("word ", 40) // 200 chars with spaces out := truncateMetaDescription(words, v1MetaDescriptionMaxChars) n := utf8.RuneCountInString(out) if n > v1MetaDescriptionMaxChars { t.Fatalf("len=%d want <= %d (%q)", n, v1MetaDescriptionMaxChars, out) } if strings.HasSuffix(out, "wor") || strings.HasSuffix(out, "wo") || strings.HasSuffix(out, "w") { t.Fatalf("mid-word cut: %q", out) } if !strings.HasSuffix(out, "word") { t.Fatalf("expected last full word, got %q", out) } } func TestTruncateMetaDescription_collapsesSpace(t *testing.T) { t.Parallel() out := truncateMetaDescription(" hello \n\t world ", 155) if out != "hello world" { t.Fatalf("got %q", out) } } func TestMetaFieldsFromEnhanceObj_snakeAndCamel(t *testing.T) { t.Parallel() mt, md := metaFieldsFromEnhanceObj(map[string]any{ "meta_title": "Acme Widget Pro | Daily Use", "meta_description": "Shop Acme Widget Pro for reliable everyday performance.", }) if mt != "Acme Widget Pro | Daily Use" { t.Fatalf("meta_title=%q", mt) } if !strings.Contains(md, "Acme Widget Pro") { t.Fatalf("meta_description=%q", md) } mt, md = metaFieldsFromEnhanceObj(map[string]any{ "metaTitle": "Camel Title", "metaDescription": "

Camel desc with enough characters for a real SEO snippet about the product.

", }) if strings.Contains(mt, "<") || mt == "" { t.Fatalf("camel meta_title should be plain: %q", mt) } if strings.Contains(md, "<") || md == "" { t.Fatalf("camel meta_description should be plain: %q", md) } mt, md = metaFieldsFromEnhanceObj(map[string]any{ "meta_title": "short retail title; follow any Title formula", }) if mt != "" || md != "" { t.Fatalf("leakage meta must be dropped: %q %q", mt, md) } } func TestRunSteps_enhancePersistsMetaFields(t *testing.T) { e := &Engine{Completer: stubCompleter{fn: func(system, user string) (Completion, error) { return Completion{Text: `{ "name":"Acme Widget Pro", "description":"

Acme Widget Pro

Durable widget for everyday use.

", "meta_title":"Acme Widget Pro | Durable Daily Use", "meta_description":"Shop Acme Widget Pro for reliable everyday performance. Clear specs and fast delivery." }`, TotalTokens: 12}, nil }}} out, err := e.RunSteps(context.Background(), "c1", ProductInput{ Mapped: map[string]any{"name": "Raw", "description": "old", "category": "tools"}, Language: "en", }, "full", nil, StepPolicy{AllowAI: true}) if err != nil { t.Fatal(err) } if out.MetaTitle != "Acme Widget Pro | Durable Daily Use" { t.Fatalf("MetaTitle=%q", out.MetaTitle) } if !strings.Contains(out.MetaDescription, "Shop Acme Widget Pro") { t.Fatalf("MetaDescription=%q", out.MetaDescription) } if !strings.Contains(out.ProcessedDescription, "

") { t.Fatalf("description should stay HTML: %q", out.ProcessedDescription) } if strings.Contains(out.MetaDescription, "<") { t.Fatalf("meta_description must be plain: %q", out.MetaDescription) } } func TestV1PollMetaFallback_usesTemplateWhenEmpty(t *testing.T) { t.Parallel() title := "Cordless Drill" cat := "Tools" plain := strings.Repeat("quality cordless drill for home and workshop use. ", 10) synthTitle, synthDesc := fillMetaFromResult(StepResult{ Name: title, ProcessedName: title, Category: cat, Description: plain, ProcessedDescription: plain, Attributes: map[string]any{"brand": "Makita"}, }) if synthTitle == "" || synthDesc == "" { t.Fatal("expected synth meta") } if utf8.RuneCountInString(synthDesc) > v1MetaDescriptionMaxChars { t.Fatalf("poll meta_description too long: %d", utf8.RuneCountInString(synthDesc)) } // Word-safe: last rune should not be mid-word fragment from a hard cut. r := []rune(synthDesc) if len(r) == v1MetaDescriptionMaxChars { // hard-cut path only when no good space; otherwise last char is letter from a word end last := r[len(r)-1] if last == ' ' { t.Fatalf("trailing space in meta_description: %q", synthDesc) } } }