package processing import ( "fmt" "strings" "testing" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/google/uuid" ) func TestScoreV1ProcessCompletedItem_processedStructure(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "123", "status": "processed", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": "Sony Headphones", "name": "Sony Headphones", "description": "Sony Headphones deliver clear audio with noise cancelling for daily commuting.", "meta_title": "Sony | Sony Headphones", "meta_description": "Sony Headphones deliver clear audio with noise cancelling for daily commuting.", "category": "48", "category_name": "Slušalke", "attributes": map[string]any{"brand": "Sony", "color": "Black"}, "main_image": "https://example.com/a.jpg", "more_images": []string{"https://example.com/b.jpg"}, "eprel": nil, } sc := ScoreV1ProcessCompletedItem(item, ScoreV1ProcessItemOptions{MappedCategory: "48"}) if !sc.OK { t.Fatalf("expected OK, flags=%v score=%d", sc.FailFlags, sc.Score) } if sc.Score < 90 { t.Fatalf("score=%d want >= 90", sc.Score) } } func TestScoreV1ProcessCompletedItem_flagsGaps(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "999", "status": "processed", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": "Mount", "description": nil, "meta_title": nil, "meta_description": nil, "category": nil, "category_name": nil, "attributes": map[string]any{ "brand": "X", "eprel_id": "1", ":": "true", }, "eprel": "not-an-object", } sc := ScoreV1ProcessCompletedItem(item, ScoreV1ProcessItemOptions{MappedCategory: "28"}) if sc.OK { t.Fatal("expected failures") } joined := strings.Join(sc.FailFlags, ",") for _, want := range []string{ "description_empty_with_title", "meta_title_missing", "meta_description_missing", "category_missing_though_mapped", "eprel_invalid_shape", } { if !strings.Contains(joined, want) { t.Fatalf("missing flag %q in %v", want, sc.FailFlags) } } } func TestEnforceV1ProcessCompletedItem_fillsDescriptionMetaSanitizes(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "1", "status": "processed", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": "Fiksni stenski TV nosilec 32''-55'' MANHATTAN", "description": "Fiksni stenski TV nosilec 32''-55'' MANHATTAN from Ostalo. Ready for retail listing.", "meta_title": nil, "meta_description": nil, "category": "28", "category_name": "Nosilci za TV", "attributes": map[string]any{ "brand": "Ostalo", "product_model": "460934", "eprel_label": "A", "name": "should-drop", ":": "true", }, "eprel": nil, } out := EnforceV1ProcessCompletedItem(item, "sl", nil) desc, _ := out["description"].(string) if desc == "" { t.Fatal("expected nonempty description") } if descriptionEchoesTitle(desc, fmt.Sprint(out["name"])) { t.Fatalf("EnforceV1 must replace weak/echo desc, got title-echo: %q", desc) } if containsWeakFillerPhrase(desc) { t.Fatalf("filler left in description: %q", desc) } if strings.TrimSpace(fmt.Sprint(out["meta_title"])) == "" || out["meta_title"] == nil { t.Fatalf("meta_title empty: %v", out["meta_title"]) } if strings.TrimSpace(fmt.Sprint(out["meta_description"])) == "" || out["meta_description"] == nil { t.Fatalf("meta_description empty: %v", out["meta_description"]) } if containsWeakFillerPhrase(fmt.Sprint(out["meta_description"])) { t.Fatalf("meta_description still filler: %v", out["meta_description"]) } attrs, ok := out["attributes"].(map[string]any) if !ok { t.Fatalf("attributes type=%T", out["attributes"]) } if _, bad := attrs["eprel_label"]; bad { t.Fatalf("eprel_* should be stripped: %v", attrs) } if _, bad := attrs[":"]; bad { t.Fatalf("invalid key kept: %v", attrs) } if _, bad := attrs["name"]; bad { t.Fatalf("reserved name kept: %v", attrs) } if out["name"] == nil || strings.TrimSpace(fmt.Sprint(out["name"])) == "" { t.Fatalf("name missing: %v", out["name"]) } if _, ok := out["title"]; ok { t.Fatalf("duplicate title should be omitted when name exists: %v", out["title"]) } for _, junk := range []string{"id", "processed_product_id", "raw_product_id"} { if _, ok := out[junk]; ok { t.Fatalf("%s must be omitted: %v", junk, out) } } sc := ScoreV1ProcessCompletedItem(out, ScoreV1ProcessItemOptions{MappedCategory: "28"}) if !sc.OK { t.Fatalf("enforced item should score OK, flags=%v", sc.FailFlags) } } func TestEnforceV1ProcessCompletedItem_skipsTerminalStatuses(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "1", "status": "not_found", "error": "missing", "ai_enhance": true, "finish_reason": "stop", "field_sources": map[string]any{"enhance_input_hash": "abc"}, "prompt_tokens": 12, "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", } out := EnforceV1ProcessCompletedItem(item, "", nil) if _, ok := out["title"]; ok { t.Fatalf("should not invent fields for not_found: %v", out) } for _, junk := range []string{"id", "ai_enhance", "finish_reason", "field_sources", "prompt_tokens"} { if _, ok := out[junk]; ok { t.Fatalf("%s must be stripped from public items: %v", junk, out) } } } func TestEnforceV1ProcessCompletedItem_categoryFromCallerPreserved(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "2", "status": "processed", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": "Cooker", "description": "", "category": "50", "category_name": "Štedilniki", "attributes": map[string]any{"brand": "Vox"}, "eprel": nil, } out := EnforceV1ProcessCompletedItem(item, "", nil) if out["category"] != "Štedilniki" { t.Fatalf("category=%v want display name", out["category"]) } if out["category_id"] != "50" { t.Fatalf("category_id=%v want 50", out["category_id"]) } if out["category_name"] != "Štedilniki" { t.Fatalf("category_name=%v", out["category_name"]) } desc, _ := out["description"].(string) if desc == "" { t.Fatal("expected synthesized description when title set") } if descriptionEchoesTitle(desc, "Cooker") { t.Fatalf("synthesized description must not equal title: %q", desc) } sc := ScoreV1ProcessCompletedItem(out, ScoreV1ProcessItemOptions{MappedCategory: "50"}) if !sc.OK { t.Fatalf("flags=%v", sc.FailFlags) } } func TestEnforceV1ProcessCompletedItem_replacesTitleEchoDescription(t *testing.T) { t.Parallel() title := "VOX EBR700" item := V1ProcessJobItem{ "ean": "8806095210711", "status": "processed", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": title, "description": title, "category": "50", "category_name": "Štedilniki", "attributes": map[string]any{"brand": "VOX"}, "eprel": nil, } out := EnforceV1ProcessCompletedItem(item, "sl", nil) desc, _ := out["description"].(string) if desc == "" { t.Fatal("expected synthesized description") } if descriptionEchoesTitle(desc, title) { t.Fatalf("must not leave title-echo description: %q", desc) } if !strings.Contains(desc, "Štedilniki") && !strings.Contains(desc, "VOX") { t.Fatalf("expected factual synth with category/brand: %q", desc) } } func TestEnforceV1ProcessCompletedItem_refreshesPoisonedMetaTitle(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "8806095210711", "status": "processed", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": "VOX EBR700", "description": "VOX EBR700 is a Štedilniki product from VOX.", "meta_title": "VOX EBR700 | 50", "meta_description": "Ready for retail listing.", "category": "50", "category_name": "Štedilniki", "attributes": map[string]any{ "brand": "VOX", "zavora": "junk", }, "eprel": nil, } out := EnforceV1ProcessCompletedItem(item, "sl", nil) mt := fmt.Sprint(out["meta_title"]) if isPoisonedMetaTitle(mt) || strings.Contains(mt, "| 50") { t.Fatalf("poisoned meta_title preserved: %q", mt) } if !strings.Contains(mt, "Štedilniki") { t.Fatalf("expected CategoryName in meta_title: %q", mt) } md := fmt.Sprint(out["meta_description"]) if md == "" || md == "" || out["meta_description"] == nil { t.Fatalf("meta_description empty after poisoned title refresh: %v", out["meta_description"]) } if containsWeakFillerPhrase(md) || isWeakPriorEnhanceDescription(md, "VOX EBR700") { t.Fatalf("stub meta_description preserved with poisoned title: %q", md) } attrs, ok := out["attributes"].(map[string]any) if !ok { t.Fatalf("attributes type=%T", out["attributes"]) } if _, bad := attrs["zavora"]; bad { t.Fatalf("feed junk zavora leaked: %v", attrs) } if _, ok := attrs["brand"]; !ok { t.Fatalf("brand should remain: %v", attrs) } } func TestIsPoisonedMetaTitle(t *testing.T) { t.Parallel() if !isPoisonedMetaTitle("VOX EBR700 | 50") { t.Fatal("expected poisoned") } if !isPoisonedMetaTitle("Name | 7") { t.Fatal("expected poisoned with extra spaces") } if isPoisonedMetaTitle("VOX | Štedilniki") { t.Fatal("display name suffix must not count as poisoned") } if isPoisonedMetaTitle("") { t.Fatal("empty not poisoned") } if !isPoisonedMetaTitle("Sony | short retail title; follow any Title formula constr…") { t.Fatal("expected prompt-leakage meta_title poisoned") } if !isPoisonedMetaTitle("ANKER | short retail title; follow any Title formula constraints that follow") { t.Fatal("expected Title formula leakage poisoned") } } func TestEnforceV1ProcessCompletedItem_refreshesPromptLeakageMeta(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "4548736132597", "status": "processed", "id": "dddddddd-dddd-dddd-dddd-dddddddddddd", "processed_product_id": "dddddddd-dddd-dddd-dddd-dddddddddddd", "raw_product_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "title": "SONY BT slušalke WH1000XM5S sive", "description": "SONY BT slušalke WH1000XM5S sive je izdelek v kategoriji Slušalke znamke Sony.", "meta_title": "Sony | short retail title; follow any Title formula constr…", "meta_description": "prefer 1-3 factual paragraphs as ONE string; limited HTML () is allowed if needed", "category": "48", "category_name": "Slušalke", "attributes": map[string]any{ "brand": "Sony", }, "eprel": nil, } out := EnforceV1ProcessCompletedItem(item, "sl", nil) mt := fmt.Sprint(out["meta_title"]) if isPoisonedMetaTitle(mt) || strings.Contains(strings.ToLower(mt), "short retail title") { t.Fatalf("prompt-leakage meta_title preserved: %q", mt) } if !strings.Contains(mt, "Slušalke") && !strings.Contains(mt, "SONY") { t.Fatalf("expected synth meta_title with product/category: %q", mt) } md := fmt.Sprint(out["meta_description"]) if md == "" || md == "" || out["meta_description"] == nil { t.Fatalf("meta_description empty after leakage refresh: %v", out["meta_description"]) } if isPromptLeakageTitle(md) || strings.Contains(strings.ToLower(md), "prefer 1-3") { t.Fatalf("prompt-leakage meta_description preserved: %q", md) } } func TestCompanyOmitsSEOMetaID(t *testing.T) { t.Parallel() a1 := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790") demo := uuid.MustParse("2b3159b0-fc08-415b-b248-35ed02a6baab") legacy := uuid.MustParse(billing.A1LegacyCompanyID) other := uuid.MustParse("11111111-1111-1111-1111-111111111111") if !CompanyOmitsSEOMetaID(a1) || !CompanyOmitsSEOMetaID(demo) || !CompanyOmitsSEOMetaID(legacy) { t.Fatal("A1, Demo, and legacy A1 ids must omit SEO meta") } if CompanyOmitsSEOMetaID(other) || CompanyOmitsSEOMetaID(uuid.Nil) { t.Fatal("unrelated company ids must keep SEO meta") } } func TestApplyV1SEOMetaPolicy_omitsOnlyA1(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "1", "status": "processed", "meta_title": "T", "meta_description": "D", } a1 := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790") out := ApplyV1SEOMetaPolicy(a1, []V1ProcessJobItem{item}) if _, ok := out[0]["meta_title"]; ok { t.Fatal("A1 must omit meta_title") } if _, ok := out[0]["meta_description"]; ok { t.Fatal("A1 must omit meta_description") } keep := V1ProcessJobItem{ "ean": "1", "status": "processed", "meta_title": "T", "meta_description": "D", } other := uuid.MustParse("11111111-1111-1111-1111-111111111111") kept := ApplyV1SEOMetaPolicy(other, []V1ProcessJobItem{keep}) if kept[0]["meta_title"] != "T" || kept[0]["meta_description"] != "D" { t.Fatalf("non-A1 must keep SEO meta: %v", kept[0]) } } func TestEnforceV1ProcessCompletedItemOpts_omitSEOMeta(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "1", "status": "processed", "title": "Sony Headphones", "description": "Sony Headphones deliver clear audio with noise cancelling for daily commuting.", "meta_title": "Sony | Headphones", "meta_description": "Sony Headphones deliver clear audio.", "category": "48", "category_name": "Slušalke", "attributes": map[string]any{"brand": "Sony"}, "eprel": nil, } out := EnforceV1ProcessCompletedItemOpts(item, EnforceV1Opts{Language: "sl", OmitSEOMeta: true}) if _, ok := out["meta_title"]; ok { t.Fatalf("meta_title should be omitted: %v", out["meta_title"]) } if _, ok := out["meta_description"]; ok { t.Fatalf("meta_description should be omitted: %v", out["meta_description"]) } sc := ScoreV1ProcessCompletedItem(out, ScoreV1ProcessItemOptions{MappedCategory: "48", OmitSEOMeta: true}) if !sc.OK { t.Fatalf("omit meta should still score OK, flags=%v", sc.FailFlags) } } func TestEnforceV1ProcessCompletedItemOpts_A1PollScrubsPromptDump(t *testing.T) { t.Parallel() item := V1ProcessJobItem{ "ean": "8606019604493", "status": "processed", "name": "--- Title ---\nReply with ONLY JSON\nSchema: {\"name\":\"string\"}", "description": "--- Description ---\nReply with ONLY JSON (no markdown)\n" + "Title formula: Brand + Model. Follow any constraints that follow.\n" + "Schema: {\"description\":\"string\"}", "meta_title": "Sony | short retail title; follow any Title formula constr…", "meta_description": "prefer 1-3 factual paragraphs as ONE string", "category": "50", "category_name": "Cookers", "attributes": map[string]any{"brand": "Vox", "product_model": "EHT6020WG"}, } out := EnforceV1ProcessCompletedItemOpts(item, EnforceV1Opts{Language: "en", OmitSEOMeta: true}) name := fmt.Sprint(out["name"]) desc := fmt.Sprint(out["description"]) for _, phrase := range []string{ "--- Title ---", "--- Description ---", "Reply with ONLY JSON", "Title formula", "Schema:", } { if strings.Contains(name, phrase) || strings.Contains(desc, phrase) { t.Fatalf("A1 poll must not return prompt dump %q: name=%q desc=%q", phrase, name, desc) } } if isPromptLeakageTitle(name) || isPromptLeakageTitle(desc) { t.Fatalf("A1 poll still looks like prompt leakage: name=%q desc=%q", name, desc) } if desc == "" || desc == "" || out["description"] == nil { t.Fatalf("A1 poll should replace leaked description, got %v", out["description"]) } if _, ok := out["meta_title"]; ok { t.Fatal("A1 poll must omit meta_title") } if _, ok := out["meta_description"]; ok { t.Fatal("A1 poll must omit meta_description") } } func TestEnforceV1ProcessCompletedItem_preservesFormulaHTML(t *testing.T) { t.Parallel() htmlDesc := `

Anker Soundcore Space One Pro

Zložljive ANC slušalke z bogatim zvokom.

  • Bluetooth 5.3
` item := V1ProcessJobItem{ "ean": "1", "status": "processed", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": "Anker Soundcore Space One Pro", "description": htmlDesc, "category": "48", "category_name": "Slušalke", "attributes": map[string]any{"brand": "Anker"}, "eprel": nil, } out := EnforceV1ProcessCompletedItem(item, "sl", nil) desc, _ := out["description"].(string) for _, tag := range []string{"

", "

", "

    "} { if !strings.Contains(desc, tag) { t.Fatalf("EnforceV1 must keep formula HTML %q, got %q", tag, desc) } } // meta_description must stay plain (no tags). md := fmt.Sprint(out["meta_description"]) if strings.Contains(md, "

    ") || strings.Contains(md, "

    ") { t.Fatalf("meta_description should be plain, got %q", md) } }