Files
descrybe/apps/api/internal/processing/prompt_render_test.go
T
2026-08-23 20:49:40 +02:00

258 lines
9.8 KiB
Go

package processing
import (
"strings"
"testing"
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
)
func TestResolveProductPromptTemplates_categoryOverridesUser(t *testing.T) {
t.Parallel()
sys, user := resolveProductPromptTemplates(ProductInput{
EnhanceSystemTemplate: "sys {{brand_voice}}",
EnhanceUserTemplate: "company user",
CategoryEnhancePrompt: "category user {{description}}",
})
if !strings.Contains(sys, "sys {{brand_voice}}") {
t.Fatalf("system=%q want company system kept", sys)
}
if !strings.Contains(sys, `apply it to "name", "description", and "attrs"`) {
t.Fatalf("system=%q want category title+description+attrs overlay", sys)
}
if !strings.Contains(user, "category user {{description}}") {
t.Fatalf("user=%q want category override text", user)
}
if !strings.Contains(user, "applies to name, description, and attrs") {
t.Fatalf("user=%q want title+description+attrs framing", user)
}
if !strings.Contains(user, "{{attrs}}") {
t.Fatalf("user=%q want injected {{attrs}}", user)
}
if !strings.Contains(user, "{{name}}") || !strings.Contains(user, "{{category}}") {
t.Fatalf("user=%q want name/category placeholders", user)
}
}
func TestResolveProductPromptTemplates_categoryWithAttrsUnchanged(t *testing.T) {
t.Parallel()
_, user := resolveProductPromptTemplates(ProductInput{
CategoryEnhancePrompt: "Write copy.\nAttrs: {{attrs}}\nName: {{name}}",
})
if !strings.Contains(user, "Write copy.\nAttrs: {{attrs}}\nName: {{name}}") {
t.Fatalf("user=%q want category text preserved when attrs present", user)
}
if !strings.Contains(user, "applies to name, description, and attrs") {
t.Fatalf("user=%q want title framing on free-form overlay", user)
}
}
func TestResolveProductPromptTemplates_categoryCanonicalSkipsFrame(t *testing.T) {
t.Parallel()
_, user := resolveProductPromptTemplates(ProductInput{
CategoryEnhancePrompt: aiprompts.CategoryEnhanceUserTemplate,
})
if strings.Count(user, "applies to name, description, and attrs") != 0 ||
strings.Count(user, "applies to name and description") != 0 {
t.Fatalf("canonical template must not get double framing: %q", user)
}
if !strings.Contains(user, "Title formula") || !strings.Contains(user, `"name"`) {
t.Fatalf("canonical template should retain name guidance: %q", user)
}
}
func TestResolveProductPromptTemplates_categoryWithTitleFormula(t *testing.T) {
t.Parallel()
title := map[string]any{
"separator": " ",
"elements": []any{
map[string]any{"type": "variable", "value": "brand"},
map[string]any{"type": "variable", "value": "product_model"},
},
}
desc := map[string]any{
"sections": []any{
map[string]any{"type": "p", "instructions": "Factual summary"},
},
}
sys, user := resolveProductPromptTemplates(ProductInput{
EnhanceSystemTemplate: "Retail copywriter.\n- name: short retail title\n- description: 1-2 sentences",
CategoryEnhancePrompt: "Emphasize energy class and Slovenian retail tone.",
TitleTemplate: title,
DescriptionTemplate: desc,
})
// Structured formulas trigger the A1-style template (legacy generator.php behavior);
// this fixture's instructions are English so the scaffolding renders in English.
if !strings.Contains(sys, "You are a copywriter") {
t.Fatalf("expected formula system when formulas set: %s", sys)
}
if !strings.Contains(user, "Product template:") {
t.Fatalf("expected formula template user: %s", user)
}
if !strings.Contains(user, "Emphasize energy class") && !strings.Contains(user, "<name>") {
t.Fatalf("expected title/desc formula in predloga: %s", user)
}
if !strings.Contains(user, "<p>{Factual summary}</p>") && !strings.Contains(user, "Factual summary") {
t.Fatalf("missing description formula HTML: %s", user)
}
}
func TestResolveProductPromptTemplates_derivesFormulaFromPromptHTML(t *testing.T) {
t.Parallel()
prompt := aiprompts.SectionTitleStart + "\n" +
`Napiši novo ime izdelka po formuli: "tip izdelka sentence case", "znamka", "model". Ne uporabljaj vejic.` + "\n" +
aiprompts.SectionTitleEnd + "\n\n" +
aiprompts.SectionDescriptionStart + "\n" +
`<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>` +
`<p>{Napiši odstavek, ki je dolg 100 besed.}</p>` +
`<H2>{Izpostavi en benefit}</H2>` +
`<p>{Napiši odstavek z tipom izdelka lowercase.}</p>` +
`<b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij}</li></ul>` + "\n" +
aiprompts.SectionDescriptionEnd
sys, user := resolveProductPromptTemplates(ProductInput{
EnhanceSystemTemplate: "Retail copywriter.\n- description: 1-2 sentences",
CategoryEnhancePrompt: prompt,
// Empty structured template — production A1 path often only has prompt HTML.
DescriptionTemplate: nil,
})
if !strings.Contains(sys, "Si tekstopisec") {
t.Fatalf("expected A1 copywriter system:\n%s", sys)
}
if !strings.Contains(user, "GPT predloga:") || !strings.Contains(user, "<name>{") {
t.Fatalf("expected legacy GPT predloga with <name>:\n%s", user)
}
if !strings.Contains(user, "<H2>{") && !strings.Contains(user, "<h2>{") {
t.Fatalf("expected HTML description formula in predloga:\n%s", user)
}
if strings.Contains(user, "Category guidance (applies to name") {
t.Fatal("A1 path must not use soft category guidance overlay")
}
effective := aiprompts.EffectiveDescriptionTemplateAny(nil, prompt)
if descriptionSatisfiesFormula("plain Slovenian prose without tags", effective) {
t.Fatal("plain prose must fail derived formula gate")
}
// One h2 is not enough when formula asks for two.
thin := "<h2>A</h2><p>One</p><p>Two</p><ul><li>x</li></ul>"
if descriptionSatisfiesFormula(thin, effective) {
t.Fatal("under-counted h2 must fail formula gate")
}
ok := "<h2>A</h2><p>One</p><h2>B</h2><p>Two</p><ul><li>x</li></ul>"
if !descriptionSatisfiesFormula(ok, effective) {
t.Fatal("full section count must pass formula gate")
}
}
func TestResolveProductPromptTemplates_fallsBackToCompany(t *testing.T) {
t.Parallel()
_, user := resolveProductPromptTemplates(ProductInput{
EnhanceUserTemplate: "company user {{name}}",
})
if user != "company user {{name}}" {
t.Fatalf("user=%q", user)
}
}
func TestRenderProductEnhancePrompts_categoryOverrideGetsAttrs(t *testing.T) {
t.Parallel()
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{
CategoryEnhancePrompt: "Write a monitor listing. Focus on panel tech.",
})
_, user := RenderProductEnhancePrompts(
sysTpl, userTpl,
"Monitors", "UltraView 27", "", "", "", "en",
map[string]any{"brand": "Acme", "size": "27 inch"},
)
if !strings.Contains(user, "Attrs:") {
t.Fatalf("user missing Attrs block:\n%s", user)
}
if !strings.Contains(user, "Acme") || !strings.Contains(user, "27") {
t.Fatalf("user missing attr values:\n%s", user)
}
if !strings.Contains(user, thinEnhanceUserInstruction) {
t.Fatalf("thin input should prepend instruction:\n%s", user)
}
if !strings.Contains(user, "UltraView 27") {
t.Fatalf("user missing name:\n%s", user)
}
}
func TestCategoryEnhancePromptFor(t *testing.T) {
t.Parallel()
m := map[string]company.LangPromptMap{
"monitorji": {"sl": "prompt-a", "en": "prompt-a-en"},
"televizorji": {"sl": "prompt-b"},
}
if got := categoryEnhancePromptFor(m, " Monitorji ", "sl", "sl"); got != "prompt-a" {
t.Fatalf("got %q", got)
}
if got := categoryEnhancePromptFor(m, " Monitorji ", "en", "sl"); got != "prompt-a-en" {
t.Fatalf("got %q", got)
}
if got := categoryEnhancePromptFor(m, "missing", "sl", "sl"); got != "" {
t.Fatalf("expected empty, got %q", got)
}
// sl-only category: en falls back to primary=sl
if got := categoryEnhancePromptFor(m, "televizorji", "en", "sl"); got != "prompt-b" {
t.Fatalf("en→primary sl: got %q", got)
}
if got := categoryEnhancePromptFor(m, "televizorji", "en", "en"); got != "" {
t.Fatalf("en with primary=en should stay empty, got %q", got)
}
}
func TestCategoryEnhancePromptFor_uniqueIDAndNameKeys(t *testing.T) {
t.Parallel()
overlay := company.LangPromptMap{"*": aiprompts.CategoryEnhanceUserTemplate}
byName := map[string]company.LangPromptMap{
"štedilniki": overlay,
}
byUID := map[string]company.LangPromptMap{
"50": overlay,
}
if got := categoryEnhancePromptFor(byName, "50", "sl", "sl"); got != "" {
t.Fatalf("name-only map must miss unique_id lookup, got %q", got)
}
if got := categoryEnhancePromptFor(byUID, "50", "sl", "sl"); !strings.Contains(got, "{{attrs}}") {
t.Fatalf("unique_id key should resolve overlay: %q", got)
}
// Dual-index map (as loadCategoryEnhanceOverlays builds): unique_id OR name works.
dual := map[string]company.LangPromptMap{}
for _, k := range categoryOverlayKeys("50", "Štedilniki") {
dual[k] = overlay
}
if got := categoryEnhancePromptFor(dual, "50", "en", "sl"); !strings.Contains(got, "{{category}}") {
t.Fatalf("dual unique_id lookup: %q", got)
}
if got := categoryEnhancePromptFor(dual, "Štedilniki", "en", "sl"); !strings.Contains(got, "{{attrs}}") {
t.Fatalf("dual name lookup: %q", got)
}
}
func TestCategoryOverlayKeys_indexesUIDAndName(t *testing.T) {
t.Parallel()
keys := categoryOverlayKeys("50", "Štedilniki")
want := map[string]bool{"50": true, "štedilniki": true}
if len(keys) != 2 {
t.Fatalf("keys=%v want 2", keys)
}
for _, k := range keys {
if !want[k] {
t.Fatalf("unexpected key %q in %v", k, keys)
}
}
}
func TestRenderProductEnhancePrompts_languageLabelsSLEN(t *testing.T) {
t.Parallel()
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{})
sysSL, _ := RenderProductEnhancePrompts(sysTpl, userTpl, "Cat", "Name", "Desc", "", "", "sl", nil)
if !strings.Contains(sysSL, "Slovenian") {
t.Fatalf("sl system missing Slovenian label:\n%s", sysSL)
}
sysEN, _ := RenderProductEnhancePrompts(sysTpl, userTpl, "Cat", "Name", "Desc", "", "", "en", nil)
if !strings.Contains(sysEN, "English") {
t.Fatalf("en system missing English label:\n%s", sysEN)
}
}