from __future__ import annotations import pytest from app.lang import detect_language @pytest.mark.parametrize( "text", [ # The live regression: langdetect scores this pt at 99.9%. "hola, ¿hacéis envíos a Portugal?", "que productos vendeis y de que material son", "quiero devolver mi pedido", # langdetect scores this one as Catalan. "envianme a Lisboa el paquete por favor", "¿cuánto tarda el envío a Canarias?", # live bug: this all-caps Spanish was answered in HUNGARIAN (langdetect # misfired 'hu'); the expanded markers ("soy","mandais") now win. "SOY MELILLA, MANDAIS A MELILLA", "soy de melilla, mandais cosas a melilla?", ], ) def test_spanish_detected(text): assert detect_language(text) == "español" @pytest.mark.parametrize( "text", [ "bom dia, que produtos tendes?", "qual é o prazo de entrega para Lisboa?", "obrigado, quero devolver o meu pedido", "não recebi a minha encomenda", ], ) def test_portuguese_detected(text): assert detect_language(text) == "portugués de Portugal (pt-PT)" def test_catalan_detected_not_portuguese(): # regression: "bon dia, què veneu?" was answered in Portuguese assert detect_language("bon dia, què veneu?") == "català" assert detect_language("bona tarda, voleu ajudar-me amb això?") == "català" @pytest.mark.parametrize( "text,expected", [ ("مرحبا، ماذا تبيعون في متجركم؟", "árabe (Arabic)"), ("你们卖什么产品?我想了解一下", "chino simplificado (Chinese)"), ("こんにちは、どんな商品がありますか?", "japonés (Japanese)"), ("안녕하세요, 어떤 제품을 판매하나요?", "coreano (Korean)"), ("Здравствуйте, какие товары у вас есть?", "ruso (Russian)"), ("नमस्ते, आप क्या बेचते हैं?", "hindi (Hindi)"), ("Merhaba, hangi ürünleri satıyorsunuz?", "turco (Turkish)"), ("Hallo, welke producten verkoopt u?", "neerlandés (Dutch)"), # genuine Hungarian must STILL detect (the Melilla fix must not over-suppress hu) ("Szeretném tudni a szállítási költségeket Budapestre kérem", "húngaro (Hungarian)"), ], ) def test_world_languages_detected(text, expected): assert detect_language(text) == expected def test_english_detected(): assert detect_language("where is my order, can you check the tracking?") == "English" def test_english_with_stray_spanish_word_stays_english(): # regression: "mi" must NOT flip a clearly-English sentence to Spanish assert detect_language("where is mi product please") == "English" assert detect_language("hello, can you help me find a product") == "English" @pytest.mark.parametrize( "text", [ # the real production bug: plain Spanish WITHOUT ¿¡ñ accents was pinned # English because the Spanish preposition "a" tripped _looks_english. "Hola, voy a comprar la Toorx msx70 y quiero saber los plazos de entrega " "para envios a la peninsula.", # same defect without a leading greeting (exercises the score path) "quiero comprar la bici y saber los plazos de envios a la peninsula", "necesito ayuda para elegir y comprar productos para mi pedido", ], ) def test_plain_spanish_without_accents_is_spanish(text): # regression: a strong Spanish word-score must beat the "a"/"to" English bias assert detect_language(text) == "español" @pytest.mark.parametrize( "text", [ "i want to talk to a human", # was detected Swahili "hello can you help me", # was detected Welsh "i need help with my order please", "can you tell me where my product is", ], ) def test_short_english_not_misdetected(text): # regression: short English must NOT be pinned to an exotic language assert detect_language(text) == "English" @pytest.mark.parametrize( "text", [ # regression: langdetect guessed Iberian, no markers → was pinned español "do you have garden hoses?", "do you sell garden products?", ], ) def test_english_misclassified_as_iberian_stays_english(text): assert detect_language(text) == "English" @pytest.mark.parametrize( "text,expected", [ # regression: dense non-Latin under 12 chars returned None (no pin → drift) ("你们卖什么?", "chino simplificado (Chinese)"), ("你们有花园用的水管吗?", "chino simplificado (Chinese)"), ("정원용 호스 있나요?", "coreano (Korean)"), ("ガーデンホース", "japonés (Japanese)"), ("لديكم خراطيم؟", "árabe (Arabic)"), ], ) def test_short_nonlatin_detected_by_script(text, expected): assert detect_language(text) == expected def test_short_cyrillic_pins_a_cyrillic_language(): # exact Slavic language varies, but it must pin a Cyrillic language (not None) cyrillic = {"ruso (Russian)", "ucraniano (Ukrainian)", "búlgaro (Bulgarian)", "macedonio (Macedonian)"} assert detect_language("у вас есть садовые шланги?") in cyrillic def test_unsure_latin_text_not_forced(): # gibberish/ambiguous Latin → don't force a wrong language (let LLM decide) assert detect_language("zxcv qwer asdf zxcv") is None def test_short_greetings_pinned_by_lexicon(): # regression: "buenas" was answered in Portuguese assert detect_language("buenas") == "español" assert detect_language("hola") == "español" assert detect_language("bonjour") == "français" assert detect_language("hello") == "English" assert detect_language("olá") == "portugués de Portugal (pt-PT)" def test_unknown_short_text_not_pinned(): assert detect_language("xy") is None assert detect_language("") is None def test_language_guard_flags_drift(): from app.lang import needs_language_repair ko = "coreano (Korean)" zh = "chino simplificado (Chinese)" # drift: Korean pinned but answer is Spanish → must repair assert needs_language_repair("Sí, disponemos de mangueras para jardín.", ko) is True assert needs_language_repair("Sí, tenemos varias opciones.", zh) is True # correct: answer in the pinned script → no repair (Latin product names allowed) assert needs_language_repair("네, 정원용 호스가 있습니다. Gobeflat Medium 모델입니다.", ko) is False assert needs_language_repair("我们有花园用的水管。PVC 平面水管。", zh) is False # Latin-script languages are NOT force-repaired by design: the model mirrors the # customer's language (system prompt), and a heuristic Latin pin can be wrong — # repairing toward a wrong pin is exactly what caused the Hungarian/Catalan # misfire. So Latin always returns False (trust the model), only non-Latin repairs. assert needs_language_repair("Sí, tenemos varias mangueras de jardín disponibles para ti.", "français") is False assert needs_language_repair("Bonjour, nous avons plusieurs tuyaux d'arrosage disponibles.", "français") is False assert needs_language_repair("anything", None) is False def test_strip_emojis_removes_pictographs_keeps_text(): from app.lang import strip_emojis assert strip_emojis("¡Hola! 😊 ¿En qué te ayudo?") == "¡Hola! ¿En qué te ayudo?" assert strip_emojis("Tenemos mangueras 🌿✅ disponibles") == "Tenemos mangueras disponibles" # non-Latin text must survive untouched (emoji ranges don't overlap scripts) assert strip_emojis("정원용 호스 😀 있습니다") == "정원용 호스 있습니다" assert strip_emojis("我们有水管 🚀") == "我们有水管" assert strip_emojis("normal text") == "normal text" def test_strip_links_removes_fabricated_links(): from app.lang import strip_links # markdown link → plain text (the inline form is the real CTA, no 404 link) out = strip_links("Rellena el [Formulario de contacto](https://flexigobe.com/contacto) abajo.") assert "https://" not in out and "](" not in out assert "Formulario de contacto" in out # keep the words, drop the link # bare URL removed assert "http" not in strip_links("Visita https://flexigobe.com/contacto para más.") # clean text untouched assert strip_links("Rellena el formulario que aparece abajo.") == \ "Rellena el formulario que aparece abajo." def test_prompt_forbids_emojis(): from app.prompts import build_system_prompt assert "emoji" in build_system_prompt("X").lower() def test_prompt_pins_detected_language_else_mirrors(): # The store context is heavily Spanish and drags the model into Spanish even for # an English customer (observed live), so a detected language is pinned EXPLICITLY; # when detection is unsure (None) we mirror. The Hungarian misfire is fixed at the # DETECTOR (see test_spanish_detected), not by refusing to pin. from app.prompts import build_system_prompt pinned = build_system_prompt("Tienda", "English").lower() assert "english" in pinned and "entire reply" in pinned unsure = build_system_prompt("Tienda", None).lower() assert "same language" in unsure and "mirror" in unsure def test_iso_code_reverses_the_pin_friendly_display_name(): """detect_language() returns a display name meant for the LLM prompt ("English", "español"), not a wire code -- iso_code() converts it back for callers (the widget matching its own UI language to the conversation).""" from app.lang import iso_code assert iso_code("English") == "en" assert iso_code("español") == "es" assert iso_code("français") == "fr" assert iso_code("chino simplificado (Chinese)") == "zh" assert iso_code("chino tradicional (Chinese)") == "zh" # both zh variants collapse assert iso_code("not a real language") is None assert iso_code(None) is None assert iso_code("") is None def test_prompt_forbids_redirecting_customer_to_third_parties(): """Regression: the bot once told a Spanish customer to 'visit Toorx's website / contact their customer service' instead of helping. The prompt must forbid sending the customer to a manufacturer/third party and keep them with the store's own team.""" from app.prompts import build_system_prompt p = build_system_prompt("Tienda X").lower() assert "third party" in p and "manufacturer" in p assert "escalate_to_human" in p