Files
descrybe/apps/api/internal/processing/categorize_ai_test.go
T

175 lines
5.1 KiB
Go
Raw Normal View History

2026-08-16 23:07:32 +02:00
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)
}
}