fix
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func categorizeEngine(reply string) (*Engine, *int) {
|
||||
enhanceCalls := 0
|
||||
e := &Engine{
|
||||
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
||||
if strings.Contains(strings.ToLower(system), "categoryid") {
|
||||
return Completion{Text: reply, TotalTokens: 3}, nil
|
||||
}
|
||||
enhanceCalls++
|
||||
return Completion{Text: `{"name":"Enhanced","description":"Body"}`, TotalTokens: 5}, nil
|
||||
}},
|
||||
Vector: NoopVectorCategorizer{},
|
||||
}
|
||||
return e, &enhanceCalls
|
||||
}
|
||||
|
||||
func categorizeInput() ProductInput {
|
||||
return ProductInput{
|
||||
Name: "BRUNNER folding camping chair ONE SHOT",
|
||||
Description: "A light folding aluminium chair for outdoor use.",
|
||||
Mapped: map[string]any{
|
||||
"name": "BRUNNER folding camping chair ONE SHOT",
|
||||
"description": "A light folding aluminium chair for outdoor use.",
|
||||
},
|
||||
CategoryNamesByUID: map[string]string{"1": "Generators", "34": "Smartwatches"},
|
||||
}
|
||||
}
|
||||
|
||||
// The reported case: the model always answers with SOME id, so a low score is it
|
||||
// saying "I do not know". Taking it at face value files a camping chair under
|
||||
// Generators and then drives that category's formula.
|
||||
func TestRunSteps_lowConfidenceCategoryFailsTheProduct(t *testing.T) {
|
||||
t.Parallel()
|
||||
e, enhanceCalls := categorizeEngine(`{"categoryId":"1","confidence":0.08}`)
|
||||
|
||||
out, err := e.RunSteps(context.Background(), "co", categorizeInput(), "full", nil,
|
||||
StepPolicy{AllowAI: true})
|
||||
if !errors.Is(err, ErrCategoryNotFound) {
|
||||
t.Fatalf("err=%v want ErrCategoryNotFound", err)
|
||||
}
|
||||
for _, want := range []string{"0.08", "0.75"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("error should report the score and the minimum, got %v", err)
|
||||
}
|
||||
}
|
||||
if out.Category != "" {
|
||||
t.Fatalf("a rejected pick must not be persisted, got %q", out.Category)
|
||||
}
|
||||
// The whole point: stop before spending an enhance call on the wrong category.
|
||||
if *enhanceCalls != 0 {
|
||||
t.Fatalf("enhance ran %d time(s) after the category was rejected", *enhanceCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSteps_confidentCategoryProceeds(t *testing.T) {
|
||||
t.Parallel()
|
||||
e, enhanceCalls := categorizeEngine(`{"categoryId":"1","confidence":0.92}`)
|
||||
|
||||
out, err := e.RunSteps(context.Background(), "co", categorizeInput(), "full", nil,
|
||||
StepPolicy{AllowAI: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out.Category != "1" {
|
||||
t.Fatalf("Category=%q want 1", out.Category)
|
||||
}
|
||||
if *enhanceCalls == 0 {
|
||||
t.Fatal("enhance should run for a confident category")
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly at the floor is acceptable — the threshold is a minimum, not a bar to clear.
|
||||
func TestRunSteps_confidenceAtThresholdIsAccepted(t *testing.T) {
|
||||
t.Parallel()
|
||||
e, _ := categorizeEngine(fmt.Sprintf(`{"categoryId":"34","confidence":%v}`, DefaultMinCategorizeConfidence))
|
||||
out, err := e.RunSteps(context.Background(), "co", categorizeInput(), "full", nil,
|
||||
StepPolicy{AllowAI: true})
|
||||
if err != nil {
|
||||
t.Fatalf("confidence == minimum must pass, got %v", err)
|
||||
}
|
||||
if out.Category != "34" {
|
||||
t.Fatalf("Category=%q want 34", out.Category)
|
||||
}
|
||||
}
|
||||
|
||||
// A model that reports no score is not penalised — only one that reports a low one.
|
||||
func TestRunSteps_missingConfidenceIsAccepted(t *testing.T) {
|
||||
t.Parallel()
|
||||
e, _ := categorizeEngine(`{"categoryId":"34"}`)
|
||||
out, err := e.RunSteps(context.Background(), "co", categorizeInput(), "full", nil,
|
||||
StepPolicy{AllowAI: true})
|
||||
if err != nil {
|
||||
t.Fatalf("missing confidence must not fail the product, got %v", err)
|
||||
}
|
||||
if out.Category != "34" {
|
||||
t.Fatalf("Category=%q want 34", out.Category)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepPolicy_confidenceOverride(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := (StepPolicy{}).CategorizeConfidence(); got != DefaultMinCategorizeConfidence {
|
||||
t.Fatalf("default = %v want %v", got, DefaultMinCategorizeConfidence)
|
||||
}
|
||||
if got := (StepPolicy{MinCategorizeConfidence: 0.4}).CategorizeConfidence(); got != 0.4 {
|
||||
t.Fatalf("override = %v want 0.4", got)
|
||||
}
|
||||
// A tenant that lowers the bar accepts what the default would reject.
|
||||
e, _ := categorizeEngine(`{"categoryId":"1","confidence":0.5}`)
|
||||
if _, err := e.RunSteps(context.Background(), "co", categorizeInput(), "full", nil,
|
||||
StepPolicy{AllowAI: true, MinCategorizeConfidence: 0.4}); err != nil {
|
||||
t.Fatalf("0.5 should pass a 0.4 floor, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A category already resolved from the feed is never second-guessed: categorize
|
||||
// does not run, so its confidence cannot fail the product.
|
||||
func TestRunSteps_mappedCategorySkipsConfidenceGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
e, _ := categorizeEngine(`{"categoryId":"1","confidence":0.01}`)
|
||||
in := categorizeInput()
|
||||
in.Mapped["category"] = "34"
|
||||
|
||||
out, err := e.RunSteps(context.Background(), "co", in, "full", nil, StepPolicy{AllowAI: true})
|
||||
if err != nil {
|
||||
t.Fatalf("a feed-supplied category must not be gated, got %v", err)
|
||||
}
|
||||
if out.Category != "34" {
|
||||
t.Fatalf("Category=%q want 34", out.Category)
|
||||
}
|
||||
}
|
||||
|
||||
// Free plan / AI disabled must keep working — nothing was asked of a model, so
|
||||
// there is no failed categorisation to report.
|
||||
func TestRunSteps_noAIDoesNotFailOnMissingCategory(t *testing.T) {
|
||||
t.Parallel()
|
||||
e, _ := categorizeEngine(`{"categoryId":"1","confidence":0.01}`)
|
||||
if _, err := e.RunSteps(context.Background(), "co", categorizeInput(), "full", nil,
|
||||
StepPolicy{AllowAI: false}); err != nil {
|
||||
t.Fatalf("AI-disabled processing must not fail on category, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategorizeConfidenceFromJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
obj map[string]any
|
||||
want float64
|
||||
ok bool
|
||||
}{
|
||||
{map[string]any{"confidence": 0.08}, 0.08, true},
|
||||
{map[string]any{"confidence": "0.42"}, 0.42, true},
|
||||
{map[string]any{"score": 0.9}, 0.9, true},
|
||||
// Some models answer on a 0-100 scale.
|
||||
{map[string]any{"confidence": 85.0}, 0.85, true},
|
||||
{map[string]any{"confidence": 1}, 1, true},
|
||||
{map[string]any{"categoryId": "1"}, 0, false},
|
||||
{map[string]any{"confidence": "high"}, 0, false},
|
||||
{nil, 0, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, ok := categorizeConfidenceFromJSON(c.obj)
|
||||
if ok != c.ok || (ok && got != c.want) {
|
||||
t.Fatalf("%v → (%v, %v) want (%v, %v)", c.obj, got, ok, c.want, c.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user