package processing import ( "context" "strings" "testing" "github.com/descrybe/descrybe-v2/apps/api/internal/company" ) func TestHashEnhanceInput_stableAndSensitive(t *testing.T) { attrs := map[string]any{"brand": "Acme", "color": "Red"} a := HashEnhanceInput("Shoes", "Runner", "A shoe", "", "", "", "", attrs) b := HashEnhanceInput("Shoes", "Runner", "A shoe", "", "", "", "", attrs) if a == "" || a != b { t.Fatalf("expected stable hash, got %q vs %q", a, b) } if HashEnhanceInput("Shoes", "Runner X", "A shoe", "", "", "", "", attrs) == a { t.Fatal("name change must change hash") } if HashEnhanceInput("Shoes", "Runner", "A shoe", "Brand:\n- tone: bold", "", "", "", attrs) == a { t.Fatal("brand prompt must change hash") } if HashEnhanceInput("Shoes", "Runner", "A shoe", "", "", "sys-a", "user-a", attrs) == a { t.Fatal("prompt template change must change hash") } if HashEnhanceInput("Shoes", "Runner", "A shoe", "", "fr", "", "", attrs) == a { t.Fatal("language change must change hash") } // Attr key order must not matter (CompactAttrs + json map sort). attrs2 := map[string]any{"color": "Red", "brand": "Acme"} if HashEnhanceInput("Shoes", "Runner", "A shoe", "", "", "", "", attrs2) != a { t.Fatal("attr key order must not change hash") } } func TestRunSteps_skipsEnhanceWhenInputHashUnchanged(t *testing.T) { calls := 0 e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { calls++ return Completion{Text: `{"name":"ShouldNotRun","description":"Nope"}`, TotalTokens: 9}, nil }}, Vector: NoopVectorCategorizer{}, } priorDesc := "Cached description with enough retail detail for listing copy." in := ProductInput{ Name: "Widget", Description: "A widget with enough mapped detail for hashing.", Mapped: map[string]any{"name": "Widget", "description": "A widget with enough mapped detail for hashing."}, PriorProcessedName: "Cached Widget", PriorProcessedDescription: priorDesc, } // First pass without prior hash to compute the hash shape via enhance path is awkward; // compute the same hash RunSteps will see after normalize (name/desc from mapped). // enhance_only: normalize then enhance with out.Name from normalized. normName := "Widget" normDesc := "A widget with enough mapped detail for hashing." sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{}) in.PriorEnhanceHash = HashEnhanceInput("", normName, normDesc, "", "", sysTpl, userTpl, map[string]any{}) out, err := e.RunSteps(context.Background(), "co", in, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if calls != 0 { t.Fatalf("expected LLM skip, calls=%d", calls) } if out.ProcessedName != "Cached Widget" { t.Fatalf("name=%q", out.ProcessedName) } if out.ProcessedDescription != priorDesc { t.Fatalf("desc=%q", out.ProcessedDescription) } if out.TotalTokens != 0 { t.Fatalf("tokens=%d want 0", out.TotalTokens) } if out.FieldSources[FieldEnhanceInputHash] != in.PriorEnhanceHash { t.Fatalf("hash field=%v", out.FieldSources[FieldEnhanceInputHash]) } if src, _ := out.FieldSources["name"].(string); src != "ai_enhance_unchanged" { t.Fatalf("name source=%v", out.FieldSources["name"]) } if !out.SkipCreditDebit { t.Fatal("expected SkipCreditDebit when enhance hash unchanged") } if shouldDebitProductProcessing(false, out) { t.Fatal("processOne must not debit when enhance unchanged") } // Hash-skip (0 tokens) must still persist known engine mode, not "unknown". if out.AIProviderMode != AIProviderInternal { t.Fatalf("ai_provider_mode=%q want %q on hash-skip", out.AIProviderMode, AIProviderInternal) } // processOne treats unknown as empty and prefers job modeLabel. got := preferKnownProviderMode(AIProviderUnknown, AIProviderInternal) if got != AIProviderInternal { t.Fatalf("preferKnownProviderMode(unknown, internal)=%q", got) } joined := strings.Join(out.Notes, ";") if !strings.Contains(joined, "unchanged") { t.Fatalf("notes=%v", out.Notes) } } func TestRunSteps_callsEnhanceWhenInputHashDiffers(t *testing.T) { calls := 0 e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { calls++ return Completion{Text: `{"name":"Fresh","description":"Fresh retail copy with enough detail for a quality enhance hash."}`, TotalTokens: 3}, nil }}, Vector: NoopVectorCategorizer{}, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Mapped: map[string]any{"name": "Widget", "description": "A widget"}, PriorEnhanceHash: "deadbeef", PriorProcessedName: "Old", PriorProcessedDescription: "Old desc", }, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if calls != 1 { t.Fatalf("calls=%d", calls) } if out.ProcessedName != "Fresh" { t.Fatalf("name=%q", out.ProcessedName) } if out.TotalTokens != 3 { t.Fatalf("tokens=%d", out.TotalTokens) } if out.SkipCreditDebit { t.Fatal("hash miss must still debit") } if !shouldDebitProductProcessing(false, out) { t.Fatal("expected debit when enhance ran") } h, _ := out.FieldSources[FieldEnhanceInputHash].(string) if h == "" || h == "deadbeef" { t.Fatalf("expected new hash in field_sources, got %q", h) } } func TestRunSteps_reenhancesWhenPriorDescEqualsTitle(t *testing.T) { calls := 0 e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { calls++ return Completion{Text: `{"name":"Widget Pro","description":"A durable retail widget for everyday use with clear specs."}`, TotalTokens: 5}, nil }}, Vector: NoopVectorCategorizer{}, } in := ProductInput{ Name: "Widget", Description: "Widget", Mapped: map[string]any{"name": "Widget", "description": "Widget"}, PriorProcessedName: "Widget", PriorProcessedDescription: "Widget", } sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{}) in.PriorEnhanceHash = HashEnhanceInput("", "Widget", "Widget", "", "", sysTpl, userTpl, map[string]any{}) out, err := e.RunSteps(context.Background(), "co", in, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if calls != 1 { t.Fatalf("expected LLM re-enhance when prior desc equals title, calls=%d", calls) } if out.ProcessedDescription == "Widget" || out.ProcessedDescription == "" { t.Fatalf("desc=%q", out.ProcessedDescription) } if out.SkipCreditDebit { t.Fatal("title-echo prior must still debit") } } func TestPreferredProductDescription_skipsTitleEcho(t *testing.T) { title := "Acme Widget Pro" if got := preferredProductDescription(title, title, "Acme Widget Pro.", "A durable retail widget for everyday use."); got != "A durable retail widget for everyday use." { t.Fatalf("got %q", got) } if got := preferredProductDescription(title, title, strings.ToUpper(title)); got != "" { t.Fatalf("expected empty when all candidates echo title, got %q", got) } if got := preferredProductDescription(title, "", "", "Category:"); got != "" { t.Fatalf("expected empty for unusable candidates, got %q", got) } } func TestRunSteps_emptyDescriptionGetsNonemptyProcessedDesc(t *testing.T) { calls := 0 e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { calls++ return Completion{Text: `{"name":"Gigabyte Monitor","description":""}`, TotalTokens: 4}, nil }}, Vector: NoopVectorCategorizer{}, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "Gigabyte Monitor", Mapped: map[string]any{"name": "Gigabyte Monitor", "brand": "Gigabyte"}, }, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if calls != 1 { t.Fatalf("expected enhance call, calls=%d", calls) } if strings.TrimSpace(out.ProcessedDescription) == "" { t.Fatal("expected nonempty ProcessedDescription when title set") } if descriptionEchoesTitle(out.ProcessedDescription, out.ProcessedName) { t.Fatalf("ProcessedDescription must not echo title: name=%q desc=%q", out.ProcessedName, out.ProcessedDescription) } if descriptionEchoesTitle(out.ProcessedDescription, "Gigabyte Monitor") { t.Fatalf("ProcessedDescription must not echo mapped title: desc=%q", out.ProcessedDescription) } } func TestRunSteps_thinOKDoesNotPersistEnhanceHash(t *testing.T) { e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { return Completion{Text: `{"name":"Monitor","description":"ok"}`, TotalTokens: 2}, nil }}, Vector: NoopVectorCategorizer{}, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "Monitor", Description: "Monitor", Mapped: map[string]any{"name": "Monitor", "description": "Monitor"}, }, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h != "" && isWeakPriorEnhanceDescription(out.ProcessedDescription, out.ProcessedName) { t.Fatalf("must not persist hash with weak desc: hash=%q desc=%q", h, out.ProcessedDescription) } for _, lf := range out.LocalizedContent { if lf.EnhanceInputHash != "" && isWeakPriorEnhanceDescription(lf.ProcessedDescription, lf.ProcessedName) { t.Fatalf("localized hash poisoned with weak desc: %+v", lf) } } } func TestRunSteps_synthesizedDescDoesNotPersistEnhanceHash(t *testing.T) { e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { // Empty description forces synthesizeProductDescription. return Completion{Text: `{"name":"GIGABYTE GS27QC","description":""}`, TotalTokens: 3}, nil }}, Vector: NoopVectorCategorizer{}, } descTpl := map[string]any{ "sections": []any{ map[string]any{"type": "h1", "instructions": "Heading"}, map[string]any{"type": "p", "instructions": "Body"}, }, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "GIGABYTE GS27QC", Mapped: map[string]any{"name": "GIGABYTE GS27QC", "category": "7", "brand": "GIGABYTE"}, DescriptionTemplate: descTpl, Language: "sl", }, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if !strings.Contains(out.ProcessedDescription, "

") { t.Fatalf("expected formula synthesize HTML, got %q", out.ProcessedDescription) } if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h != "" { t.Fatalf("synthesized description must not persist enhance hash (got %q)", h) } for _, lf := range out.LocalizedContent { if lf.EnhanceInputHash != "" { t.Fatalf("localized synthesized hash must be empty: %+v", lf) } } } func TestRunSteps_weakPriorHashDoesNotSkipReprocess(t *testing.T) { calls := 0 e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { calls++ return Completion{Text: `{"name":"Widget Pro","description":"A durable retail widget for everyday use with clear specs."}`, TotalTokens: 5}, nil }}, Vector: NoopVectorCategorizer{}, } normName := "Widget" normDesc := "Widget" sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{}) priorHash := HashEnhanceInput("", normName, normDesc, "", "", sysTpl, userTpl, map[string]any{}) out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "Widget", Description: "Widget", Mapped: map[string]any{"name": "Widget", "description": "Widget"}, PriorEnhanceHash: priorHash, PriorProcessedName: "Widget", PriorProcessedDescription: "ok", PriorLocalized: company.LocalizedContent{ "en": {ProcessedName: "Widget", ProcessedDescription: "ok", EnhanceInputHash: priorHash}, }, }, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if calls != 1 { t.Fatalf("weak prior desc must force completer, calls=%d", calls) } if out.SkipCreditDebit { t.Fatal("weak prior must not SkipCreditDebit") } if out.ProcessedDescription == "ok" || out.ProcessedDescription == "Widget" { t.Fatalf("desc=%q", out.ProcessedDescription) } } func TestRunSteps_failedEnhanceDoesNotPoisonLocalizedHash(t *testing.T) { e := &Engine{ Completer: stubCompleter{fn: func(system, user string) (Completion, error) { return Completion{}, context.DeadlineExceeded }}, Vector: NoopVectorCategorizer{}, } out, err := e.RunSteps(context.Background(), "co", ProductInput{ Name: "Widget", Mapped: map[string]any{"name": "Widget", "description": "A widget with enough mapped detail for hashing."}, PriorEnhanceHash: "should-not-survive", PriorLocalized: company.LocalizedContent{ "en": {ProcessedName: "Old", ProcessedDescription: "Old desc that is long enough to look real.", EnhanceInputHash: "poison"}, }, }, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true}) if err != nil { t.Fatal(err) } if _, ok := out.FieldSources[FieldEnhanceInputHash]; ok { t.Fatalf("failed enhance must not keep field_sources hash, got %v", out.FieldSources[FieldEnhanceInputHash]) } for lang, lf := range out.LocalizedContent { if lf.EnhanceInputHash != "" { t.Fatalf("lang %s: failed enhance must clear localized hash, got %q", lang, lf.EnhanceInputHash) } } }