175 lines
5.1 KiB
Go
175 lines
5.1 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|