490 lines
18 KiB
Go
490 lines
18 KiB
Go
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 == "<nil>" || 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 == "<nil>" || 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 == "<nil>" || 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 := `<h1>Anker Soundcore Space One Pro</h1><p>Zložljive ANC slušalke z bogatim zvokom.</p><ul><li>Bluetooth 5.3</li></ul>`
|
|
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{"<h1>", "<p>", "<ul>"} {
|
|
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, "<h1>") || strings.Contains(md, "<p>") {
|
|
t.Fatalf("meta_description should be plain, got %q", md)
|
|
}
|
|
}
|