package processing import ( "context" "encoding/json" "strings" "testing" "github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts" "github.com/descrybe/descrybe-v2/apps/api/internal/company" ) // Regression: enhance used to stamp processed_* onto name/description, so Review // always showed Matched — and formula invent ("Ključne lastnosti") overwrote the // feed Original. Original must stay supplier; enriched must change. func TestRunSteps_enhancePreservesOriginalVsEnriched(t *testing.T) { t.Parallel() prompt := aiprompts.SectionTitleStart + "\n" + `Napiši novo ime izdelka po formuli: tip znamka model barva.` + "\n" + aiprompts.SectionTitleEnd + "\n\n" + aiprompts.SectionDescriptionStart + "\n" + `

{Naslov}

{Odstavek}

` + "\n" + aiprompts.SectionDescriptionEnd feedName := "BRUNNER zložljiv stol za kampiranje ONE SHOT 0404164N.C20" feedDesc := "Predstavljajte si eleganten in izjemno lahek stol, zasnovan posebej za umetniške direktorje." newName := "zložljiv stol Brunner ONE SHOT siv" newDesc := "

Lahek kamp stol

" + strings.Repeat("Udobje na poti. ", 15) + "

" reply, _ := json.Marshal(map[string]any{ "name": newName, "description": newDesc, "meta_title": newName, "meta_description": "Brunner ONE SHOT zložljiv kamp stol", "attrs": map[string]any{"brand": "BRUNNER"}, }) e := &Engine{ Completer: stubCompleter{fn: func(_, _ string) (Completion, error) { return Completion{Text: string(reply), TotalTokens: 20, Model: "mock"}, nil }}, Vector: NoopVectorCategorizer{}, } descTpl := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt) out, err := e.RunSteps(context.Background(), "co-preserve", ProductInput{ GTIN: "8022068075495", Name: feedName, Description: feedDesc, Mapped: map[string]any{"name": feedName, "description": feedDesc, "brand": "BRUNNER", "category": "stolcki"}, Language: "sl", CategoryEnhancePrompt: prompt, DescriptionTemplate: descTpl, CategoryNamesByUID: map[string]string{"stolcki": "Stolčki"}, CategoryUniqueID: "stolcki", PriorCategory: "stolcki", }, "enhance_only", nil, StepPolicy{AllowAI: true}) if err != nil { t.Fatal(err) } if out.Name != feedName { t.Fatalf("Original name must stay feed, got %q", out.Name) } if out.Description != feedDesc { t.Fatalf("Original description must stay feed, got %q", out.Description) } if out.ProcessedName != newName { t.Fatalf("ProcessedName=%q want %q", out.ProcessedName, newName) } if !strings.Contains(out.ProcessedDescription, "

") { t.Fatalf("ProcessedDescription missing formula HTML: %s", out.ProcessedDescription) } if out.ProcessedName == out.Name { t.Fatal("Review must show name Changed (enriched ≠ original)") } if out.ProcessedDescription == out.Description { t.Fatal("Review must show description Changed (enriched ≠ original)") } } func TestRunSteps_formulaInventDoesNotOverwriteOriginal(t *testing.T) { t.Parallel() prompt := aiprompts.SectionDescriptionStart + "\n" + `

{Naslov}

{Odstavek}

` + "\n" + aiprompts.SectionDescriptionEnd feedName := "BRUNNER ONE SHOT" feedDesc := "Predstavljajte si eleganten in izjemno lahek stol za umetniške direktorje." // Completer echoes supplier — formula gate synthesizes processed HTML. reply, _ := json.Marshal(map[string]any{ "name": feedName, "description": feedDesc, }) e := &Engine{ Completer: stubCompleter{fn: func(_, _ string) (Completion, error) { return Completion{Text: string(reply), TotalTokens: 8, Model: "mock"}, nil }}, Vector: NoopVectorCategorizer{}, } descTpl := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt) out, err := e.RunSteps(context.Background(), "co-invent", ProductInput{ Name: feedName, Description: feedDesc, Mapped: map[string]any{"name": feedName, "description": feedDesc, "brand": "BRUNNER", "width": "0.44m"}, Language: "sl", CategoryEnhancePrompt: prompt, DescriptionTemplate: descTpl, }, "enhance_only", nil, StepPolicy{AllowAI: true}) if err != nil { t.Fatal(err) } if out.Description != feedDesc { t.Fatalf("invent must not overwrite Original description, got %q", out.Description) } if !strings.Contains(strings.ToLower(out.ProcessedDescription), "ključne lastnosti") && !company.LooksLikeHeuristicSynthesize(out.ProcessedDescription) && !descriptionSatisfiesFormula(out.ProcessedDescription, descTpl) { t.Fatalf("expected processed formula/synth HTML, got %q", out.ProcessedDescription) } if out.ProcessedDescription == out.Description { t.Fatal("Original and Enriched must differ when invent fills processed only") } } func TestEnhanceHashForceReason_descCopyPaste(t *testing.T) { t.Parallel() long := "Supplier copy about the widget with enough characters to clear the weak-description floor for hash skip checks." in := ProductInput{ Name: "Widget Pro Retail Title", Description: long, PriorProcessedName: "Widget Pro Retail Title Enriched", PriorProcessedDescription: long, DescriptionTemplate: map[string]any{ "sections": []any{ map[string]any{"type": "h2", "instructions": "x"}, map[string]any{"type": "p", "instructions": "y"}, }, }, } if got := enhanceHashForceReason(in); got != "formula-mismatch" { t.Fatalf("got %q want formula-mismatch", got) } in.PriorProcessedDescription = "

A

" + long + "

" in.Description = "

A

" + long + "

" if got := enhanceHashForceReason(in); got != "desc-copy-paste" { t.Fatalf("got %q want desc-copy-paste", got) } }