Files
2026-08-16 16:57:36 +02:00

402 lines
12 KiB
Go

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(`<ul><li>Color: Red</li><li>Material: Steel</li></ul>`)
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": "<br>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<br>World<br/>&amp; more</p><b>bold</b>")
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 }