package processing import ( "context" "strings" "testing" ) func TestTryAICategorize_picksTaxonomyUniqueID(t *testing.T) { e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { if strings.Contains(strings.ToLower(system), "categoryid") { if !strings.Contains(user, "Available Categories:") { t.Fatalf("expected Available Categories in user prompt") } if !strings.Contains(user, "(ID: 50)") { t.Fatalf("expected ID 50 in list: %s", user) } return Completion{ Text: `{"categoryId":"50","confidence":0.91}`, TotalTokens: 12, PromptTokens: 8, OutputTokens: 4, }, nil } return Completion{Text: `{"name":"Gorenje Cooker","description":"Freestanding cooker for the kitchen."}`, TotalTokens: 5}, nil }}, Vector: NoopVectorCategorizer{}, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "Gorenje stove", Description: "Freestanding cooker", CategoryNamesByUID: map[string]string{ "28": "TV", "50": "Štedilniki", }, }, "full", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if out.Category != "50" { t.Fatalf("Category=%q want 50", out.Category) } if out.CategoryName != "Štedilniki" { t.Fatalf("CategoryName=%q want Štedilniki", out.CategoryName) } if src, _ := out.FieldSources["category"].(string); src != "llm" { t.Fatalf("field_sources.category=%v want llm", out.FieldSources["category"]) } if out.TotalTokens < 12 { t.Fatalf("TotalTokens=%d want >=12 from categorize", out.TotalTokens) } } func TestTryAICategorize_rejectsInventedID(t *testing.T) { e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { if strings.Contains(strings.ToLower(system), "categoryid") { return Completion{Text: `{"categoryId":"99999","confidence":0.9}`, TotalTokens: 3}, nil } return Completion{Text: `{"name":"X","description":"Y"}`, TotalTokens: 5}, nil }}, Vector: NoopVectorCategorizer{}, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "Widget", CategoryNamesByUID: map[string]string{ "50": "Štedilniki", }, }, "full", nil, StepPolicy{AllowAI: true}) if err != nil { t.Fatal(err) } if out.Category != "" { t.Fatalf("Category=%q want empty (invented id rejected)", out.Category) } found := false for _, n := range out.Notes { if strings.Contains(n, "ai_categorize: ignored") { found = true break } } if !found { t.Fatalf("expected reject note, got %v", out.Notes) } } func TestTryAICategorize_resolvesDisplayName(t *testing.T) { e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { if strings.Contains(strings.ToLower(system), "categoryid") { return Completion{Text: `{"categoryId":"Štedilniki","confidence":0.8}`, TotalTokens: 3}, nil } return Completion{Text: `{"name":"X","description":"Y"}`, TotalTokens: 5}, nil }}, Vector: NoopVectorCategorizer{}, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "Cooker", CategoryNamesByUID: map[string]string{ "50": "Štedilniki", }, }, "full", nil, StepPolicy{AllowAI: true}) if err != nil { t.Fatal(err) } if out.Category != "50" { t.Fatalf("Category=%q want 50 (name coerced)", out.Category) } } func TestTryAICategorize_skippedWithoutTaxonomy(t *testing.T) { calls := 0 e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { calls++ return Completion{Text: `{"name":"A","description":"B"}`, TotalTokens: 2}, nil }}, Vector: NoopVectorCategorizer{}, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "Lone product", }, "full", nil, StepPolicy{AllowAI: true}) if err != nil { t.Fatal(err) } if out.Category != "" { t.Fatalf("Category=%q want empty", out.Category) } // Only enhance should call Completer (categorize skips with no taxonomy). if calls != 1 { t.Fatalf("completer calls=%d want 1 (enhance only)", calls) } } func TestHeuristicCompleter_categorizeReturnsJSON(t *testing.T) { h := HeuristicCompleter{} user := ProductCategorizeUser("Stove", "Cooker", []categoryOption{ {UniqueID: "50", Name: "Štedilniki"}, {UniqueID: "28", Name: "TV"}, }) comp, err := h.Complete(context.Background(), ProductCategorizeSystem, user) if err != nil { t.Fatal(err) } obj, err := ParseJSONObject(comp.Text) if err != nil { t.Fatal(err) } got := categoryIDFromCategorizeJSON(obj) if got != "50" && got != "28" { t.Fatalf("categoryId=%q want 50 or 28 from Available Categories", got) } opts := categoryOptionsFromNames(map[string]string{"50": "Štedilniki", "28": "TV"}) found := false for _, o := range opts { if o.UniqueID == got { found = true break } } if !found { t.Fatalf("categoryId=%q not in options", got) } } func TestCategoryOptionsFromNames_sortAndCap(t *testing.T) { names := map[string]string{"2": "Beta", "1": "Alpha", "3": "Gamma"} opts := categoryOptionsFromNames(names) if len(opts) != 3 || opts[0].UniqueID != "1" || opts[1].UniqueID != "2" { t.Fatalf("opts=%v", opts) } }