package processing import ( "context" "encoding/json" "strings" "testing" ) func TestNormalizeMapped_aliasesAndZeroDims(t *testing.T) { got := NormalizeMapped(map[string]any{ "EAN": "999", "netWidth": 0, "name": "X", }, nil) if got["gtin"] != "999" { t.Fatalf("gtin=%v", got["gtin"]) } if _, ok := got["width"]; ok { t.Fatalf("zero width should be dropped: %v", got) } } func TestParseSpecifications_htmlAndCSV(t *testing.T) { attrs := ParseSpecifications(``) if attrs["color"] != "Red" { t.Fatalf("html attrs=%v", attrs) } attrs2 := ParseSpecifications("Size: L\nWeight: 2 kg") if attrs2["size"] != "L" { t.Fatalf("csv attrs=%v", attrs2) } } func TestParseSpecifications_skipsGarbageAndReservedKeys(t *testing.T) { attrs := ParseSpecifications(map[string]any{ ":": "true", "zavora": "disc", "name": "Should not appear", "description": "Should not appear", "brand": "Vox", }) if _, ok := attrs[":"]; ok { t.Fatalf("colon key must be dropped: %v", attrs) } if _, ok := attrs["name"]; ok { t.Fatalf("name must be reserved: %v", attrs) } if attrs["zavora"] != "disc" { t.Fatalf("zavora=%v", attrs["zavora"]) } if attrs["brand"] != "Vox" { t.Fatalf("brand=%v", attrs["brand"]) } } func TestSanitizeProductAttributes(t *testing.T) { got := SanitizeProductAttributes(map[string]any{ "name": "TV Mount", "description": "
html", "gtin": "123", "id": "102544", "purchaseprice": "10", "main_image": "https://x", ":": "true", "brand": "Ostalo", "productmodel": "W53070", "netwidth": "0.6 m", "width": "0.6 m", "warranty": "60 mesecev", }) if _, ok := got["name"]; ok { t.Fatalf("name must be stripped: %v", got) } if _, ok := got["description"]; ok { t.Fatalf("description must be stripped: %v", got) } if _, ok := got[":"]; ok { t.Fatalf("invalid key must be stripped: %v", got) } if got["brand"] != "Ostalo" { t.Fatalf("brand=%v", got["brand"]) } if got["product_model"] != "W53070" { t.Fatalf("product_model=%v", got["product_model"]) } if got["width"] != "0.6 m" { t.Fatalf("width=%v", got["width"]) } if _, ok := got["netwidth"]; ok { t.Fatalf("alias netwidth should collapse to width: %v", got) } dropped := SanitizeProductAttributes(map[string]any{ "brand": "XIAOMI", "product_model": "6941948703193", }) if _, ok := dropped["product_model"]; ok { t.Fatalf("barcode-like product_model must be dropped: %v", dropped) } if dropped["brand"] != "XIAOMI" { t.Fatalf("brand=%v", dropped["brand"]) } } func TestFilterAttributesByAllowed(t *testing.T) { in := map[string]any{ "brand": "Ostalo", "zavora": "Mehanska", "vzmetenje": "Spredaj", "nosilnost": "40 kg", "width": "0.6 m", } allowed := map[string]struct{}{ "nosilnost": {}, "barva": {}, } got := FilterAttributesByAllowed(in, allowed) if got["brand"] != "Ostalo" || got["width"] != "0.6 m" { t.Fatalf("core keys missing: %v", got) } if got["nosilnost"] != "40 kg" { t.Fatalf("allowed key missing: %v", got) } if _, ok := got["zavora"]; ok { t.Fatalf("junk spec should be dropped: %v", got) } if _, ok := got["vzmetenje"]; ok { t.Fatalf("junk spec should be dropped: %v", got) } unchanged := FilterAttributesByAllowed(in, nil) if unchanged["zavora"] != "Mehanska" { t.Fatalf("nil allowlist should keep all: %v", unchanged) } } func TestAttrsForEnhance_cat28DropsZavora(t *testing.T) { attrs := map[string]any{ "brand": "Ostalo", "product_model": "W53070", "zavora": "Mehanska in elektricna", "vzmetenje": "Spredaj in zadaj", "nosilnost": "40 kg", } // Cat 28 TV mounts: barva/nosilnost — not motorcycle specs. allowed := map[string]struct{}{ "barva": {}, "nosilnost": {}, } got := AttrsForEnhance(attrs, allowed) if got["brand"] != "Ostalo" || got["product_model"] != "W53070" { t.Fatalf("core keys missing: %v", got) } if got["nosilnost"] != "40 kg" { t.Fatalf("category key missing: %v", got) } if _, ok := got["zavora"]; ok { t.Fatalf("zavora must not reach enhance: %v", got) } if _, ok := got["vzmetenje"]; ok { t.Fatalf("vzmetenje must not reach enhance: %v", got) } user := ProductEnhanceUser("Nosilci za TV", "NOSILEC W53070", "", got) if strings.Contains(strings.ToLower(user), "zavora") { t.Fatalf("enhance user must not mention zavora:\n%s", user) } if !strings.Contains(user, "Ostalo") || !strings.Contains(user, "W53070") { t.Fatalf("enhance user should keep brand/model:\n%s", user) } } func TestAttrsForEnhance_emptyAllowlistCoreOnly(t *testing.T) { got := AttrsForEnhance(map[string]any{ "brand": "Ostalo", "zavora": "Mehanska", }, map[string]struct{}{}) if got["brand"] != "Ostalo" { t.Fatalf("brand=%v", got["brand"]) } if _, ok := got["zavora"]; ok { t.Fatalf("empty allowlist should drop junk: %v", got) } } func TestAttrsForEnhance_copiesEPRELEnergyClass(t *testing.T) { t.Parallel() attrs := map[string]any{ "brand": "Bosch", "eprel_energy_class": "C", } ensureEnergyClassFromEPREL(attrs) got := AttrsForEnhance(attrs, map[string]struct{}{}) if got["energy_class"] != "C" { t.Fatalf("energy_class=%v want C (from eprel_energy_class); attrs=%v", got["energy_class"], got) } if _, ok := got["eprel_energy_class"]; ok { t.Fatalf("eprel_energy_class must be stripped by AttrsForEnhance: %v", got) } // Do not overwrite an existing energy_class. attrs2 := map[string]any{ "energy_class": "A", "eprel_energy_class": "C", } ensureEnergyClassFromEPREL(attrs2) if attrs2["energy_class"] != "A" { t.Fatalf("existing energy_class overwritten: %v", attrs2["energy_class"]) } } func TestAttrsForPersist_tvMountDropsJunkKeepsEPREL(t *testing.T) { attrs := map[string]any{ "name": "TV Mount", "description": "html", "brand": "Ostalo", "product_model": "W53070", "zavora": "Mehanska in elektricna", "vzmetenje": "Spredaj in zadaj", "nosilnost": "40 kg", "eprel_id": "12345", "eprel_label": "https://eprel.example/label", } allowed := map[string]struct{}{ "barva": {}, "nosilnost": {}, } got := AttrsForPersist(attrs, allowed) if _, ok := got["name"]; ok { t.Fatalf("reserved name must be stripped: %v", got) } if got["brand"] != "Ostalo" || got["product_model"] != "W53070" { t.Fatalf("core keys missing: %v", got) } if got["nosilnost"] != "40 kg" { t.Fatalf("category key missing: %v", got) } if _, ok := got["zavora"]; ok { t.Fatalf("zavora must not be stored: %v", got) } if _, ok := got["vzmetenje"]; ok { t.Fatalf("vzmetenje must not be stored: %v", got) } if got["eprel_id"] != "12345" || got["eprel_label"] != "https://eprel.example/label" { t.Fatalf("eprel keys must persist for poll projection: %v", got) } } func TestRunSteps_persistsCategoryAllowlist(t *testing.T) { e := &Engine{} in := ProductInput{ GTIN: "8712285326882", Name: "NOSILEC W53070", Mapped: map[string]any{ "name": "NOSILEC W53070", "category": "28", "brand": "Ostalo", "specifications": map[string]any{ "productmodel": "W53070", "zavora": "Mehanska", "vzmetenje": "Spredaj", "nosilnost": "40 kg", }, }, CategoryAttrKeys: map[string]map[string]struct{}{ "28": {"nosilnost": {}, "barva": {}}, }, } out, err := e.RunSteps(context.Background(), "co", in, "attributes", nil, StepPolicy{}) if err != nil { t.Fatal(err) } if out.ProcessedAttributes["brand"] != "Ostalo" { t.Fatalf("brand=%v", out.ProcessedAttributes["brand"]) } if out.ProcessedAttributes["nosilnost"] != "40 kg" { t.Fatalf("nosilnost=%v", out.ProcessedAttributes) } if _, ok := out.ProcessedAttributes["zavora"]; ok { t.Fatalf("zavora must not be stored: %v", out.ProcessedAttributes) } if _, ok := out.ProcessedAttributes["vzmetenje"]; ok { t.Fatalf("vzmetenje must not be stored: %v", out.ProcessedAttributes) } } func TestV1PlainDescription(t *testing.T) { got := v1PlainDescription("Hello
World
& more

bold") if strings.Contains(got, "<") { t.Fatalf("html should be stripped: %q", got) } if !strings.Contains(got, "Hello") || !strings.Contains(got, "World") || !strings.Contains(got, "bold") { t.Fatalf("got=%q", got) } if !strings.Contains(got, "& more") { t.Fatalf("expected unescaped amp: %q", got) } } func TestFillMissingFields_brandAndDims(t *testing.T) { m := FillMissingFields(map[string]any{ "name": "Nike Air 30x20x10 cm", }, map[string]any{}) if m["brand"] != "Nike" { t.Fatalf("brand=%v", m["brand"]) } if m["width"] == nil || m["height"] == nil { t.Fatalf("dims missing: %v", m) } } func TestRunSteps_skipsAIWithoutCompleter(t *testing.T) { e := &Engine{} out, err := e.RunSteps(context.TODO(), "co", ProductInput{ Mapped: map[string]any{"name": "Widget"}, }, "full", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } found := false for _, n := range out.Notes { if len(n) > 0 { found = true } } if !found { t.Fatalf("expected skip notes, got %v", out.Notes) } } func TestRunSteps_skipsAIWithoutEntitlement(t *testing.T) { calls := 0 e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { calls++ return Completion{Text: `{"name":"N","description":"D"}`, TotalTokens: 1}, nil }}, } out, err := e.RunSteps(context.TODO(), "co", ProductInput{ Mapped: map[string]any{"name": "Widget"}, }, "full", nil, StepPolicy{AllowAI: false, AllowEPREL: false}) if err != nil { t.Fatal(err) } if calls != 0 { t.Fatalf("AI should not run without entitlement, calls=%d", calls) } joined := strings.Join(out.Notes, ";") if !strings.Contains(joined, "can_use_ai") && !strings.Contains(joined, "Free plan") { t.Fatalf("expected free-plan skip note, got %v", out.Notes) } } func TestRunSteps_injectsBrandPromptIntoAI(t *testing.T) { var gotSystem string e := &Engine{Completer: captureCompleter{fn: func(system, _ string) (Completion, error) { gotSystem = system b, _ := json.Marshal(map[string]string{"name": "N", "description": "D"}) return Completion{Text: string(b), TotalTokens: 1, Model: "test"}, nil }}} out, err := e.RunSteps(context.TODO(), "co", ProductInput{ Mapped: map[string]any{"name": "Widget"}, BrandPrompt: "Brand:\n- tone: bold", }, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if out.ProcessedName != "N" { t.Fatalf("name=%q", out.ProcessedName) } if !strings.Contains(gotSystem, "Brand:") || !strings.Contains(gotSystem, "bold") { t.Fatalf("system prompt missing brand: %q", gotSystem) } } func TestRunSteps_InjectsLanguageIntoAI(t *testing.T) { var gotSystem string e := &Engine{Completer: captureCompleter{fn: func(system, _ string) (Completion, error) { gotSystem = system b, _ := json.Marshal(map[string]string{"name": "N", "description": "D"}) return Completion{Text: string(b), TotalTokens: 1, Model: "test"}, nil }}} out, err := e.RunSteps(context.TODO(), "co", ProductInput{ Mapped: map[string]any{"name": "Widget"}, Language: "fr", }, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if out.ProcessedName != "N" { t.Fatalf("name=%q", out.ProcessedName) } if !strings.Contains(gotSystem, "French") { t.Fatalf("system prompt missing language: %q", gotSystem) } } type captureCompleter struct { fn func(system, user string) (Completion, error) } func (c captureCompleter) Complete(_ context.Context, system, user string) (Completion, error) { return c.fn(system, user) } func (c captureCompleter) Enabled() bool { return true }