2026-08-09 22:47:43 +02:00
package processing
import (
"context"
"fmt"
"strings"
"testing"
"github.com/descrybe/descrybe-v2/apps/api/internal/eprel"
)
type stubCompleter struct {
fn func ( system , user string ) ( Completion , error )
}
func ( s stubCompleter ) Complete ( _ context . Context , system , user string ) ( Completion , error ) {
return s . fn ( system , user )
}
func TestResolveSteps ( t * testing . T ) {
cases := map [ string ][] string {
"full" : { StepNormalize , StepParseSpecs , StepFillFields , StepEPREL , StepAIEnhance },
"enhance_only" : { StepNormalize , StepAIEnhance },
"attributes_only" : { StepNormalize , StepParseSpecs , StepFillFields },
2026-08-16 16:57:36 +02:00
"eprel_only" : { StepNormalize , StepParseSpecs , StepEPREL },
2026-08-09 22:47:43 +02:00
"normalize_only" : { StepNormalize },
}
for in , want := range cases {
got := resolveSteps ( in )
if strings . Join ( got , "," ) != strings . Join ( want , "," ) {
t . Fatalf ( "%s: got %v want %v" , in , got , want )
}
}
}
func TestRunSteps_fullMock ( t * testing . T ) {
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"Red Runner","description":"A fine shoe."}` , TotalTokens : 7 , Raw : map [ string ] any { "ok" : true }}, nil
}},
Vector : NoopVectorCategorizer {},
EPREL : nil ,
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "123" , Name : "Runner" , Description : "shoe" ,
Mapped : map [ string ] any { "brand" : "Acme" },
}, "full" , nil , StepPolicy { AllowAI : true , AllowEPREL : true })
if err != nil {
t . Fatal ( err )
}
if out . ProcessedName != "Red Runner" {
t . Fatalf ( "name=%q" , out . ProcessedName )
}
if out . TotalTokens < 1 {
t . Fatalf ( "tokens=%d" , out . TotalTokens )
}
}
func TestRunSteps_enhanceOnly ( 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
}},
Vector : NoopVectorCategorizer {},
}
_ , err := e . RunSteps ( context . Background (), "co" , ProductInput { Name : "x" }, "enhance_only" , nil , StepPolicy { AllowAI : true , AllowEPREL : true })
if err != nil {
t . Fatal ( err )
}
if calls != 1 {
t . Fatalf ( "calls=%d" , calls )
}
}
func TestRunSteps_enhanceOnly_keepsPriorCategoryWhenMappedEmpty ( t * testing . T ) {
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"N","description":"D"}` , TotalTokens : 1 }, nil
}},
Vector : NoopVectorCategorizer {},
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "5905575903198" ,
Name : "Adler" ,
Mapped : map [ string ] any { "name" : "Adler" , "description" : "radiator" },
PriorCategory : "Radiators" ,
}, "enhance_only" , nil , StepPolicy { AllowAI : true , AllowEPREL : true })
if err != nil {
t . Fatal ( err )
}
if out . Category != "Radiators" {
t . Fatalf ( "category=%q want Radiators" , out . Category )
}
if src , _ := out . FieldSources [ "category" ].( string ); src != "prior_processed" {
t . Fatalf ( "field_sources.category=%v" , out . FieldSources [ "category" ])
}
}
func TestHeuristicCompleter_usesNameNotCategoryLine ( t * testing . T ) {
h := HeuristicCompleter {}
user := ProductEnhanceUser ( "120" , "Adler LED radiator" , "Bathroom heater" , map [ string ] any { "brand" : "ADLER" })
comp , err := h . Complete ( context . Background (), `Return JSON with "name" and "description".` , user )
if err != nil {
t . Fatal ( err )
}
obj , err := ParseJSONObject ( comp . Text )
if err != nil {
t . Fatal ( err )
}
name := SanitizeOutput ( fmt . Sprint ( obj [ "name" ]))
if isPromptLabelTitle ( name ) || strings . HasPrefix ( strings . ToLower ( name ), "category:" ) {
t . Fatalf ( "heuristic echoed prompt label as title: %q" , name )
}
if ! strings . Contains ( strings . ToLower ( name ), "adler" ) {
t . Fatalf ( "name=%q want Adler from Name: line" , name )
}
}
func TestEnhance_rejectsPromptLabelTitle ( t * testing . T ) {
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"Category:","description":"ok"}` , TotalTokens : 2 }, nil
}},
Vector : NoopVectorCategorizer {},
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "1" ,
Name : "Good Title" ,
Mapped : map [ string ] any { "name" : "Good Title" , "description" : "d" , "category" : "demo-electronics" },
}, "enhance_only" , nil , StepPolicy { AllowAI : true })
if err != nil {
t . Fatal ( err )
}
if out . ProcessedName != "Good Title" {
t . Fatalf ( "ProcessedName=%q want Good Title (reject Category:)" , out . ProcessedName )
}
if out . Name == "Category:" || isPromptLabelTitle ( out . Name ) {
t . Fatalf ( "Name polluted: %q" , out . Name )
}
if out . Category != "demo-electronics" {
t . Fatalf ( "category=%q" , out . Category )
}
}
func TestRunSteps_enhanceOnly_keepsMappedTitleWhenAIReturnsCategoryLabel ( t * testing . T ) {
// A1 Elkotex Adler shape: title in mapped_data, not name.
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"Category:","description":"Category: 120"}` , TotalTokens : 2 }, nil
}},
Vector : NoopVectorCategorizer {},
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "5905575903198" ,
Mapped : map [ string ] any {
"title" : "Adler LED kopalniski radiator lestev 600W AD7824" ,
"description" : "Bathroom radiator" ,
"category" : "120" ,
},
PriorCategory : "120" ,
PriorProcessedName : "Category:" ,
}, "enhance_only" , nil , StepPolicy { AllowAI : true })
if err != nil {
t . Fatal ( err )
}
if isPromptLabelTitle ( out . ProcessedName ) || out . ProcessedName == "Category:" {
t . Fatalf ( "ProcessedName=%q" , out . ProcessedName )
}
if ! strings . Contains ( out . ProcessedName , "Adler" ) {
t . Fatalf ( "ProcessedName=%q want mapped title" , out . ProcessedName )
}
if out . Category != "120" {
t . Fatalf ( "category=%q" , out . Category )
}
}
func TestPreferredProductTitle_skipsPromptLabels ( t * testing . T ) {
got := preferredProductTitle ( "99" , "Category:" , "Category: 120" , "Name:" , "Real Product" )
if got != "Real Product" {
t . Fatalf ( "got %q" , got )
}
got = preferredProductTitle ( "99" , "Category:" , "" )
if got != "Product 99" {
t . Fatalf ( "fallback got %q" , got )
}
}
2026-08-16 16:57:36 +02:00
func TestPreferredProductTitle_skipsFormulaLeakage ( t * testing . T ) {
leak := "short retail title; follow any Title formula constraints that follow; use Attrs"
got := preferredProductTitle ( "1" , leak , "follow any Title formula constraints that follow; use Attrs" , "Vox SWA-8000W" )
if got != "Vox SWA-8000W" {
t . Fatalf ( "got %q want feed title" , got )
}
got = preferredProductTitle ( "" , leak , "Schema: {\"name\":\"string\"}" , "Reply with ONLY JSON" )
if got != "Product" {
t . Fatalf ( "all-leakage fallback got %q" , got )
}
}
2026-08-09 22:47:43 +02:00
func TestIsPromptLabelTitle ( t * testing . T ) {
cases := map [ string ] bool {
"Category:" : true ,
"category: 120" : true ,
"Category : 46" : true ,
"Name:" : true ,
"Desc:" : true ,
"Adler LED" : false ,
"" : false ,
}
for in , want := range cases {
if got := isPromptLabelTitle ( in ); got != want {
t . Fatalf ( "%q: got %v want %v" , in , got , want )
}
}
}
2026-08-16 16:57:36 +02:00
func TestIsPromptLeakageTitle ( t * testing . T ) {
cases := map [ string ] bool {
"follow any Title formula constraints that follow; use Attrs" : true ,
"short retail title; follow any Title formula constraints…" : true ,
"write name in {{language}}" : true ,
"Schema: {\"name\":\"string\",\"description\":\"string\"}" : true ,
"Reply with ONLY JSON (no markdown)" : true ,
2026-08-16 21:35:48 +02:00
"Emit description as ONE HTML string covering each section in order" : true ,
"Description formula (REQUIRED — overrides any shorter \"1-2 sentences\" rule)" : true ,
"when a Description formula follows, emit ONE HTML string" : true ,
"never copy name as description" : true ,
"Vox SWA-8000W dishwasher" : false ,
"Acme Widget Pro" : false ,
2026-08-16 16:57:36 +02:00
"" : false ,
}
for in , want := range cases {
if got := isPromptLeakageTitle ( in ); got != want {
t . Fatalf ( "%q: leakage got %v want %v" , in , got , want )
}
if want && ! isPromptLabelTitle ( in ) {
t . Fatalf ( "%q: isPromptLabelTitle should include leakage" , in )
}
}
// Long dump with formula keyword (>120 runes).
long := strings . Repeat ( "x" , 100 ) + " Title formula scaffolding " + strings . Repeat ( "y" , 30 )
if ! isPromptLeakageTitle ( long ) {
t . Fatalf ( "long formula dump should leak: len=%d" , len ([] rune ( long )))
}
}
2026-08-16 21:35:48 +02:00
func TestIsUnusableCategoryValue_rejectsNameAndFormula ( t * testing . T ) {
t . Parallel ()
title := "NOSILEC W53070 81-140CM 180ST VOGELS"
if ! isUnusableCategoryValue ( title , title ) {
t . Fatal ( "product title must not be usable as category" )
}
if ! isUnusableCategoryValue ( "Title formula (order matters; join with \" \")" , title ) {
t . Fatal ( "title formula must not be usable as category" )
}
if ! isUnusableCategoryValue ( "Emit description as ONE HTML string covering each section" , title ) {
t . Fatal ( "description formula must not be usable as category" )
}
if ! isUnusableCategoryValue ( "Category:" , title ) {
t . Fatal ( "prompt label must not be usable as category" )
}
if isUnusableCategoryValue ( "28" , title ) {
t . Fatal ( "taxonomy unique_id must remain usable" )
}
if isUnusableCategoryValue ( "TV mounts" , title ) {
t . Fatal ( "real category display name must remain usable" )
}
}
func TestScrubCategoryPollution_clearsNameAsCategory ( t * testing . T ) {
t . Parallel ()
title := "Vogel's WALL 3245 TV Wall Mount"
out := StepResult {
Name : title ,
ProcessedName : title ,
Category : title ,
CategoryName : title ,
FieldSources : map [ string ] any { "category" : "vector" },
}
scrubCategoryPollution ( & out , nil , nil )
if out . Category != "" || out . CategoryName != "" {
t . Fatalf ( "expected category cleared, got Category=%q CategoryName=%q" , out . Category , out . CategoryName )
}
if src , _ := out . FieldSources [ "category" ].( string ); src != "cleared_invalid" {
t . Fatalf ( "field_sources.category=%v want cleared_invalid" , out . FieldSources [ "category" ])
}
}
func TestEnhance_rejectsDescriptionFormulaLeakageTitle ( t * testing . T ) {
leak := "Emit description as ONE HTML string covering each section in order in Slovenian"
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"` + leak + `","description":"A solid washer."}` , TotalTokens : 2 }, nil
}},
Vector : NoopVectorCategorizer {},
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "8712285326882" ,
Name : "Vox SWA-8000W" ,
Mapped : map [ string ] any { "name" : "Vox SWA-8000W" , "description" : "Washer" , "category" : "demo-electronics" },
}, "enhance_only" , nil , StepPolicy { AllowAI : true })
if err != nil {
t . Fatal ( err )
}
if out . ProcessedName != "Vox SWA-8000W" {
t . Fatalf ( "ProcessedName=%q want Vox SWA-8000W (reject description formula leakage)" , out . ProcessedName )
}
}
func TestRunSteps_rejectsProductNameAsCategory ( t * testing . T ) {
title := "NOSILEC W53070 81-140CM 180ST VOGELS"
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"` + title + `","description":"TV wall mount for large screens."}` , TotalTokens : 2 }, nil
}},
Vector : NoopVectorCategorizer {},
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "8712285326882" ,
Name : title ,
PriorCategory : title ,
Mapped : map [ string ] any { "name" : title , "description" : title , "category" : title },
}, "enhance_only" , nil , StepPolicy { AllowAI : true })
if err != nil {
t . Fatal ( err )
}
if out . Category == title || strings . EqualFold ( out . Category , out . ProcessedName ) {
t . Fatalf ( "Category=%q must not equal product title" , out . Category )
}
if categoryDisplayLabel ( out ) == title {
t . Fatalf ( "display category leaked product title: %q" , categoryDisplayLabel ( out ))
}
}
func TestRunSteps_rejectsFormulaAsCategory ( t * testing . T ) {
leak := "Title formula (order matters; join with \" \"). Build name from Attrs"
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"Vox SWA-8000W","description":"Washer."}` , TotalTokens : 2 }, nil
}},
Vector : NoopVectorCategorizer {},
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "8712285326882" ,
Name : "Vox SWA-8000W" ,
PriorCategory : leak ,
Mapped : map [ string ] any { "name" : "Vox SWA-8000W" , "description" : "Washer" , "category" : leak },
}, "enhance_only" , nil , StepPolicy { AllowAI : true })
if err != nil {
t . Fatal ( err )
}
if out . Category != "" {
t . Fatalf ( "Category=%q want empty (formula rejected)" , out . Category )
}
if categoryDisplayLabel ( out ) != "" {
t . Fatalf ( "display=%q want empty" , categoryDisplayLabel ( out ))
}
}
2026-08-16 16:57:36 +02:00
func TestEnhance_rejectsFormulaLeakageTitle ( t * testing . T ) {
leak := "short retail title; follow any Title formula constraints that follow; use Attrs"
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"` + leak + `","description":"A solid washer."}` , TotalTokens : 2 }, nil
}},
Vector : NoopVectorCategorizer {},
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "8712285326882" ,
Name : "Vox SWA-8000W" ,
Mapped : map [ string ] any { "name" : "Vox SWA-8000W" , "description" : "Washer" , "category" : "demo-electronics" },
}, "enhance_only" , nil , StepPolicy { AllowAI : true })
if err != nil {
t . Fatal ( err )
}
if out . ProcessedName != "Vox SWA-8000W" {
t . Fatalf ( "ProcessedName=%q want Vox SWA-8000W (reject formula leakage)" , out . ProcessedName )
}
if isPromptLeakageTitle ( out . Name ) || isPromptLabelTitle ( out . Name ) {
t . Fatalf ( "Name polluted: %q" , out . Name )
}
}
func TestHeuristicCompleter_keepsNameWhenFormulaConstraintsPresent ( t * testing . T ) {
h := HeuristicCompleter {}
// Shape matches aiprompts.CategoryEnhanceUserTemplate: instruction "- name:"
// appears BEFORE the real "Name:" field — labeledPromptValue must skip leakage.
user := `Your reply is parsed as JSON {"name":"string","description":"string"} only (system schema). Write name and description in English (do not hardcode a language).
- name: short retail title; follow any Title formula constraints that follow; use Attrs
- description: prefer 1-3 factual paragraphs as ONE string
Category: demo-electronics
Name: Vox SWA-8000W
Desc: Washer
Attrs: {"brand":"Vox"}
Title formula (order matters; join with " "). Build name from Attrs using this structure:
1. attr [brand]
Prefer Attrs values for [attr] slots; write name in English.`
comp , err := h . Complete ( context . Background (), `Return JSON with "name" and "description".` , user )
if err != nil {
t . Fatal ( err )
}
obj , err := ParseJSONObject ( comp . Text )
if err != nil {
t . Fatal ( err )
}
name := SanitizeOutput ( fmt . Sprint ( obj [ "name" ]))
if isPromptLeakageTitle ( name ) || isPromptLabelTitle ( name ) {
t . Fatalf ( "heuristic leaked formula into name: %q" , name )
}
if ! strings . Contains ( strings . ToLower ( name ), "vox" ) {
t . Fatalf ( "name=%q want Vox from Name: line" , name )
}
}
func TestLabeledPromptValue_skipsInstructionNameBullet ( t * testing . T ) {
user := `- name: short retail title; follow any Title formula constraints that follow; use Attrs
Name: Vox SWA-8000W
Desc: Washer
Attrs: {}`
got := labeledPromptValue ( user , "name:" , "current name:" )
if got != "Vox SWA-8000W" {
t . Fatalf ( "got %q want Vox SWA-8000W" , got )
}
}
2026-08-09 22:47:43 +02:00
func TestRunSteps_full_mappedCategoryWinsOverPrior ( t * testing . T ) {
e := & Engine {
Completer : stubCompleter { fn : func ( system , user string ) ( Completion , error ) {
return Completion { Text : `{"name":"N","description":"D"}` , TotalTokens : 1 }, nil
}},
Vector : NoopVectorCategorizer {},
}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
GTIN : "6970995789942" ,
Mapped : map [ string ] any {
"name" : "Roborock" ,
"description" : "vacuum" ,
"category" : "Robot Vacuums" ,
},
PriorCategory : "Old Category" ,
}, "full" , nil , StepPolicy { AllowAI : true , AllowEPREL : false })
if err != nil {
t . Fatal ( err )
}
if out . Category != "Robot Vacuums" {
t . Fatalf ( "category=%q want Robot Vacuums" , out . Category )
}
}
func TestRunSteps_normalizeOnly_keepsPriorWhenMappedMissing ( t * testing . T ) {
e := & Engine { Vector : NoopVectorCategorizer {}}
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
Mapped : map [ string ] any { "name" : "x" },
PriorCategory : "FromProcessed" ,
}, "normalize_only" , nil , StepPolicy {})
if err != nil {
t . Fatal ( err )
}
if out . Category != "FromProcessed" {
t . Fatalf ( "category=%q" , out . Category )
}
}
func TestRunSteps_skipsEPRELWithoutEntitlement ( t * testing . T ) {
st := & stubEprel { enabled : true , data : & eprel . Data { ID : "1" , EnergyClass : "A" }}
e := & Engine { EPREL : st }
out , err := e . RunSteps ( context . Background (), "co" , ProductInput {
Raw : map [ string ] any { "EPRELID" : "1" },
}, "eprel_only" , nil , StepPolicy { AllowEPREL : false })
if err != nil {
t . Fatal ( err )
}
if st . calls != 0 {
t . Fatal ( "EPREL should not run without entitlement" )
}
if len ( out . Notes ) == 0 {
t . Fatal ( "expected skip note" )
}
}