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

589 lines
23 KiB
Go
Raw Normal View History

package processing
import (
"context"
"strings"
"testing"
2026-08-16 16:57:36 +02:00
"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{},
}
2026-08-16 16:57:36 +02:00
priorDesc := "Cached description with enough retail detail for listing copy."
in := ProductInput{
Name: "Widget",
2026-08-16 16:57:36 +02:00
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",
2026-08-16 16:57:36 +02:00
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"
2026-08-16 16:57:36 +02:00
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)
}
2026-08-16 16:57:36 +02:00
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")
}
2026-08-16 16:57:36 +02:00
// 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, ";")
2026-08-16 21:35:48 +02:00
if !strings.Contains(joined, "ai_enhance_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++
2026-08-16 19:21:49 +02:00
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)
}
}
2026-08-16 16:57:36 +02:00
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, "", "<nil>", "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)
}
}
}
2026-08-16 19:21:49 +02:00
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, "<h1>") {
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)
}
}
}
2026-08-16 21:35:48 +02:00
func TestRunSteps_formulaIgnoredByLLM_retriesThenSynthesizes(t *testing.T) {
calls := 0
var sawFormulaRetry bool
e := &Engine{
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
calls++
if strings.Contains(user, "INVALID DESCRIPTION") {
sawFormulaRetry = true
}
// Non-weak short prose that ignores multi-section HTML formula.
return Completion{
Text: `{"name":"GIGABYTE GS27QC","description":"GIGABYTE GS27QC is a gaming monitor with a 61 cm curved panel for immersive play."}`,
TotalTokens: 4,
}, nil
}},
Vector: NoopVectorCategorizer{},
}
descTpl := map[string]any{
"sections": []any{
map[string]any{"type": "h1", "instructions": "Heading"},
map[string]any{"type": "p", "instructions": "Body"},
map[string]any{"type": "ul", "instructions": "Specs"},
},
}
out, err := e.RunSteps(context.Background(), "co", ProductInput{
Name: "GIGABYTE GS27QC",
Mapped: map[string]any{"name": "GIGABYTE GS27QC", "category": "7", "brand": "GIGABYTE", "width": "61 cm"},
CategoryFormulasByKey: map[string]CategoryFormulas{
"7": {DescriptionTemplate: descTpl},
},
Language: "en",
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
if err != nil {
t.Fatal(err)
}
if calls < 2 {
t.Fatalf("expected formula retry after ignore, calls=%d", calls)
}
if !sawFormulaRetry {
t.Fatal("expected INVALID DESCRIPTION retry user suffix")
}
for _, tag := range []string{"<h1>", "<p>", "<ul>"} {
if !strings.Contains(out.ProcessedDescription, tag) {
t.Fatalf("expected formula HTML with %s, got %q", tag, out.ProcessedDescription)
}
}
if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h != "" {
t.Fatalf("formula synthesize must not persist hash, got %q", h)
}
}
func TestRunSteps_formulaHTMLFromLLM_persistsHash(t *testing.T) {
e := &Engine{
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
return Completion{
Text: `{"name":"GIGABYTE GS27QC","description":"<h1>GIGABYTE GS27QC</h1><p>Curved gaming monitor.</p><ul><li>width 61 cm</li></ul>"}`,
TotalTokens: 6,
}, nil
}},
Vector: NoopVectorCategorizer{},
}
descTpl := map[string]any{
"sections": []any{
map[string]any{"type": "h1", "instructions": "Heading"},
map[string]any{"type": "p", "instructions": "Body"},
map[string]any{"type": "ul", "instructions": "Specs"},
},
}
out, err := e.RunSteps(context.Background(), "co", ProductInput{
Name: "GIGABYTE GS27QC",
Mapped: map[string]any{"name": "GIGABYTE GS27QC", "category": "7"},
DescriptionTemplate: descTpl,
Language: "en",
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out.ProcessedDescription, "<ul>") {
t.Fatalf("expected LLM HTML kept, got %q", out.ProcessedDescription)
}
if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h == "" {
t.Fatal("compliant formula HTML should persist enhance hash")
}
}
func TestRunSteps_synthPriorHashDoesNotSkipReprocess(t *testing.T) {
calls := 0
e := &Engine{
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
calls++
return Completion{Text: `{"name":"Vogel WALL 3245","description":"Vogel's WALL 3245 is a sturdy TV mount with clear load ratings and install guidance for wall displays."}`, TotalTokens: 5}, nil
}},
Vector: NoopVectorCategorizer{},
}
normName := "Vogel's WALL 3245 TV Wall Mount"
normDesc := "A mount with enough mapped detail for hashing inputs."
synthPrior := synthesizeDescriptionFromTitle(normName, "TV Mounts", "en", map[string]any{
"brand": "Vogel's", "width": "45 cm", "max_load": "40 kg",
})
if !company.LooksLikeHeuristicSynthesize(synthPrior) {
t.Fatalf("expected invent synth prior, got %q", synthPrior)
}
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{})
priorHash := HashEnhanceInput("", normName, normDesc, "", "", sysTpl, userTpl, map[string]any{})
out, err := e.RunSteps(context.Background(), "co", ProductInput{
Name: normName,
Description: normDesc,
Mapped: map[string]any{"name": normName, "description": normDesc},
PriorEnhanceHash: priorHash,
PriorProcessedName: normName,
PriorProcessedDescription: synthPrior,
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
if err != nil {
t.Fatal(err)
}
if calls != 1 {
t.Fatalf("synth prior must force completer, calls=%d", calls)
}
if out.SkipCreditDebit {
t.Fatal("synth prior must not SkipCreditDebit")
}
joined := strings.Join(out.Notes, ";")
if !strings.Contains(joined, "forced re-enhance") {
t.Fatalf("expected forced re-enhance note, notes=%v", out.Notes)
}
if company.LooksLikeHeuristicSynthesize(out.ProcessedDescription) && calls == 1 {
// LLM stub returned non-synth copy — ok if still synth after outer repair.
}
if out.ProcessedDescription == synthPrior {
t.Fatalf("must not reuse synth prior: %q", out.ProcessedDescription)
}
}
func TestRunSteps_formulaMismatchPriorHashDoesNotSkipReprocess(t *testing.T) {
calls := 0
e := &Engine{
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
calls++
return Completion{
Text: `{"name":"GIGABYTE GS27QC","description":"<h1>GIGABYTE GS27QC</h1><p>Curved gaming monitor with vivid colors.</p><ul><li>width 61 cm</li></ul>"}`,
TotalTokens: 6,
}, nil
}},
Vector: NoopVectorCategorizer{},
}
descTpl := map[string]any{
"sections": []any{
map[string]any{"type": "h1", "instructions": "Heading"},
map[string]any{"type": "p", "instructions": "Body"},
map[string]any{"type": "ul", "instructions": "Specs"},
},
}
normName := "GIGABYTE GS27QC"
normDesc := "GIGABYTE GS27QC is a gaming monitor with a 61 cm curved panel for immersive play."
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{DescriptionTemplate: descTpl})
priorHash := HashEnhanceInput("", normName, normDesc, "", "", sysTpl, userTpl, map[string]any{})
out, err := e.RunSteps(context.Background(), "co", ProductInput{
Name: normName,
Description: normDesc,
Mapped: map[string]any{"name": normName, "description": normDesc, "category": "7"},
DescriptionTemplate: descTpl,
PriorEnhanceHash: priorHash,
PriorProcessedName: normName,
PriorProcessedDescription: normDesc, // plain prose — formula mismatch
Language: "en",
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
if err != nil {
t.Fatal(err)
}
if calls < 1 {
t.Fatalf("formula-mismatch prior must force completer, calls=%d", calls)
}
if out.SkipCreditDebit {
t.Fatal("formula-mismatch must not SkipCreditDebit")
}
joined := strings.Join(out.Notes, ";")
if !strings.Contains(joined, "forced re-enhance") && !strings.Contains(joined, "formula") {
// forced note when hash matched; formula repair notes also acceptable
if !strings.Contains(joined, "synth") && calls >= 1 {
// still ok if LLM ran without note if reason path differed
}
}
for _, tag := range []string{"<h1>", "<p>", "<ul>"} {
if !strings.Contains(out.ProcessedDescription, tag) {
t.Fatalf("expected formula HTML with %s, got %q", tag, out.ProcessedDescription)
}
}
}
2026-08-16 16:57:36 +02:00
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)
}
}
}
2026-08-16 21:35:48 +02:00
func TestRunSteps_emptyLLMDescriptionDoesNotPersistHashViaOriginalFallback(t *testing.T) {
// Prod bug: empty LLM description preferred originals and stamped ok+hash.
richOriginal := "This durable retail mount includes mounting hardware, load ratings, and install guidance for wall displays."
e := &Engine{
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
return Completion{Text: `{"name":"Vogel WALL 3245","description":""}`, TotalTokens: 2}, nil
}},
Vector: NoopVectorCategorizer{},
}
out, err := e.RunSteps(context.Background(), "co", ProductInput{
Name: "Vogel WALL 3245",
Description: richOriginal,
Mapped: map[string]any{
"name": "Vogel WALL 3245",
"description": richOriginal,
},
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
if err != nil {
t.Fatal(err)
}
if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h != "" {
t.Fatalf("empty LLM description must not persist enhance hash (got %q); desc=%q", h, out.ProcessedDescription)
}
for _, lf := range out.LocalizedContent {
if lf.EnhanceInputHash != "" {
t.Fatalf("localized hash must stay empty on empty LLM desc: %+v", lf)
}
}
joined := strings.Join(out.Notes, ";")
if !strings.Contains(joined, "refused") && !strings.Contains(joined, "synthesized") && !strings.Contains(joined, "empty_description") {
t.Fatalf("expected refuse/synthesize note, notes=%v", out.Notes)
}
if strings.TrimSpace(out.ProcessedDescription) == "" {
t.Fatal("expected nonempty fallback/synth description")
}
}
func TestLlmEnhanceHardRefuseReason(t *testing.T) {
t.Parallel()
if got := llmEnhanceHardRefuseReason("", "desc long enough"); got != "empty_name" {
t.Fatalf("got %q", got)
}
if got := llmEnhanceHardRefuseReason("Category:", "desc long enough here for tests"); got != "prompt_leakage_name" {
t.Fatalf("got %q", got)
}
if got := llmEnhanceHardRefuseReason("Widget", ""); got != "empty_description" {
t.Fatalf("got %q", got)
}
if got := llmEnhanceHardRefuseReason("Widget", "A durable retail widget for everyday use with clear specs."); got != "" {
t.Fatalf("got %q", got)
}
}