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" ) // Regressions for "enhance just copies the feed" on A1-style category formulas. // Fixture is the reported product (GTIN 8022068075495, category Stolčki) with the // committed A1 seed prompt and the formula columns as the live A1 catalog stores // them: a brand-only title_template stub and a generic English description_template. type formulaRegressionCompleter struct { system, user string calls int reply string } func (c *formulaRegressionCompleter) Complete(_ context.Context, system, user string) (Completion, error) { c.calls++ c.system, c.user = system, user return Completion{Text: c.reply, TotalTokens: 10}, nil } func (c *formulaRegressionCompleter) Enabled() bool { return true } // a1StolckiLegacyPrompt is the "Stolčki" entry of scripts/seed/a1-category-prompts.json. const a1StolckiLegacyPrompt = "Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\n" + "Star_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\n" + "Uporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n" + "{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", " + "\"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}\n" + "{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}\n\n" + "

{Napiši Novo ime izdelka in izpostavi en benefit}

\n" + "

{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}

\n" + "

{Izpostavi en benefit, in NE napiši Novo ime izdelka}

\n" + "

{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}

" + "{Tehnične specifikacije}" const a1BrandOnlyTitleTemplate = `{"separator":" ","elements":[` + `{"id":"0-variable-brand","type":"variable","label":"Znamka","value":"brand","example":"Samsung"}]}` const a1GenericEnglishDescTemplate = `{"metaTitle":"Create a concise and compelling Meta Title.",` + `"metaDescription":"Write a short Meta Description.",` + `"sections":[{"id":"a7908e85","type":"p","instructions":"Write a detailed paragraph about specific features or benefits."}]}` const a1FeedName = "BRUNNER zlažljiv stol za kampiranje ONE SHOT sivo črn 0404164N.C20" const a1FeedDesc = "Eleganten in lahek stol za umetniške direktorje. Zložljiv aluminijast okvir, udobno sedišče in naslon za roke." const a1FormulaName = "BRUNNER zložljiv stol za kampiranje ONE SHOT 0404164N.C20" // a1FormulaReply obeys the Stolčki formula: h2 + p + h2 + p + spec list, in // Slovenian. Its prose contains " je " and its spec list contains "Znamka:" — // the combination that used to be misread as invent fallback and discarded. const a1FormulaReply = `{"name":"` + a1FormulaName + `", "description":"

BRUNNER zložljiv stol ONE SHOT za udobje na poti

Zložljiv stol iz aluminija je zasnovan za dolge ure udobnega sedenja na terenu, na snemanju ali ob kampiranju. Okvir se razpre v enem gibu in ostane stabilen tudi na neravnih tleh, medtem ko tkanina sedišča prijetno diha in se hitro suši. Naslonjala za roke razbremenijo ramena, hrbtni del pa podpira naravno držo. Zaradi majhne teže ga brez napora prenesete od avtomobila do prizorišča, zložen pa zavzame le malo prostora v prtljažniku.

Stabilnost in dolga življenjska doba

Konstrukcija združuje trpežne materiale in premišljene detajle. Aluminijasti profili so odporni proti koroziji, spoji pa so ojačani na mestih največjih obremenitev. Sivo črna kombinacija ostaja videti urejena tudi po sezoni uporabe na prostem, saj se madeži manj poznajo. Stol je enostavno očistiti z vlažno krpo, po uporabi pa ga preprosto zložite in shranite.

Tehnične specifikacije", "meta_title":"BRUNNER zložljiv stol ONE SHOT", "meta_description":"BRUNNER zložljiv stol za kampiranje ONE SHOT je lahek in stabilen zložljiv stol z udobnim sediščem za teren.", "attrs":{"brand":"BRUNNER","product_model":"ONE SHOT 0404164N.C20"}}` func jsonObject(t *testing.T, raw string) map[string]any { t.Helper() var m map[string]any if err := json.Unmarshal([]byte(raw), &m); err != nil { t.Fatal(err) } return m } func a1ProductInput(t *testing.T, catPrompt string, titleTpl, descTpl any) ProductInput { t.Helper() in := ProductInput{ GTIN: "8022068075495", Name: a1FeedName, Description: a1FeedDesc, Mapped: map[string]any{ "name": a1FeedName, "description": a1FeedDesc, "brand": "BRUNNER", "gtin": "8022068075495", "category": "stolcki", "specifications": map[string]any{"Barva": "sivo črna", "Material": "aluminij"}, }, Raw: map[string]any{}, Language: "sl", ContentLanguages: []string{"sl"}, CategoryFormulasByKey: map[string]CategoryFormulas{ "stolcki": {TitleTemplate: titleTpl, DescriptionTemplate: descTpl}, }, CategoryNamesByUID: map[string]string{"stolcki": "Stolčki"}, OmitSEOMeta: true, } if catPrompt != "" { in.CategoryPromptsByLang = map[string]company.LangPromptMap{ "stolcki": {"sl": catPrompt, "*": catPrompt}, } } return in } // Formula-compliant Slovenian copy must reach processed_* on every processing type // the dashboard starts. It used to be dropped by the invent heuristic, leaving the // supplier feed text on both sides of Review ("Matched"). func TestRunSteps_A1FormulaCopyReachesProcessedFields(t *testing.T) { t.Parallel() catPrompt := aiprompts.SplitLegacyCombinedEnhancePrompt(a1StolckiLegacyPrompt) in := a1ProductInput(t, catPrompt, jsonObject(t, a1BrandOnlyTitleTemplate), jsonObject(t, a1GenericEnglishDescTemplate)) // "title" / "description" / "enhance" are what products/+page.svelte sends. for _, ptype := range []string{"full", "enhance", "title", "description"} { t.Run(ptype, func(t *testing.T) { c := &formulaRegressionCompleter{reply: a1FormulaReply} out, err := (&Engine{Completer: c}).RunSteps( context.Background(), "co", in, ptype, []string{"stolcki"}, StepPolicy{AllowAI: true}) if err != nil { t.Fatal(err) } if c.calls != 1 { t.Fatalf("compliant reply must not trigger a formula retry, calls=%d", c.calls) } if out.ProcessedName != a1FormulaName { t.Fatalf("ProcessedName=%q want the formula name %q", out.ProcessedName, a1FormulaName) } if !strings.Contains(out.ProcessedDescription, "

") || !strings.Contains(out.ProcessedDescription, "