"""test_multilang.py -- Unit tests for Issue #167 multi-language support.""" from __future__ import annotations import json, os from typing import Any, Dict, Optional import pytest def _pkg_root(): return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def _read_src(rel_path): with open(os.path.join(_pkg_root(), rel_path)) as f: return f.read() def _extract_lang_names(): import ast src = _read_src("saltybot_social/conversation_node.py") start = src.index("_LANG_NAMES: Dict[str, str] = {") end = src.index("\n}", start) + 2 return ast.literal_eval(src[start:end].split("=",1)[1].strip()) class TestLangNames: @pytest.fixture(scope="class") def ln(self): return _extract_lang_names() def test_english(self, ln): assert ln["en"] == "English" def test_french(self, ln): assert ln["fr"] == "French" def test_spanish(self, ln): assert ln["es"] == "Spanish" def test_german(self, ln): assert ln["de"] == "German" def test_japanese(self, ln):assert ln["ja"] == "Japanese" def test_chinese(self, ln): assert ln["zh"] == "Chinese" def test_arabic(self, ln): assert ln["ar"] == "Arabic" def test_at_least_15(self, ln): assert len(ln) >= 15 def test_lowercase_keys(self, ln): for k in ln: assert k == k.lower() and 2 <= len(k) <= 3 def test_nonempty_values(self, ln): for k, v in ln.items(): assert v class TestLanguageHint: def _h(self, sl, sid, ln): lang = sl.get(sid, "en") return f"[Please respond in {ln.get(lang, lang)}.]" if lang and lang != "en" else "" @pytest.fixture(scope="class") def ln(self): return _extract_lang_names() def test_english_no_hint(self, ln): assert self._h({"p": "en"}, "p", ln) == "" def test_unknown_no_hint(self, ln): assert self._h({}, "p", ln) == "" def test_french(self, ln): assert self._h({"p":"fr"},"p",ln) == "[Please respond in French.]" def test_spanish(self, ln): assert self._h({"p":"es"},"p",ln) == "[Please respond in Spanish.]" def test_unknown_code(self, ln): assert "xx" in self._h({"p":"xx"},"p",ln) def test_brackets(self, ln): h = self._h({"p":"de"},"p",ln) assert h.startswith("[") and h.endswith("]") class TestVoiceMap: def _parse(self, jstr, dl, dp): try: extra = json.loads(jstr) if jstr.strip() not in ("{}","") else {} except: extra = {} r = {dl: dp}; r.update(extra); return r def test_empty(self): assert self._parse("{}","en","/e") == {"en":"/e"} def test_extra(self): vm = self._parse('{"fr":"/f"}', "en", "/e") assert vm["fr"] == "/f" def test_invalid(self): assert self._parse("BAD","en","/e") == {"en":"/e"} def test_multi(self): assert len(self._parse(json.dumps({"fr":"/f","es":"/s"}),"en","/e")) == 3 class TestVoiceSelect: def _s(self, voices, lang, default): return voices.get(lang) or voices.get(default) def test_exact(self): assert self._s({"en":"E","fr":"F"},"fr","en") == "F" def test_fallback(self): assert self._s({"en":"E"},"fr","en") == "E" def test_none(self): assert self._s({},"fr","en") is None class TestSttFields: @pytest.fixture(scope="class") def src(self): return _read_src("saltybot_social/speech_pipeline_node.py") def test_param(self, src): assert "whisper_language" in src def test_detected_lang(self, src): assert "detected_lang" in src def test_msg_language(self, src): assert "msg.language = language" in src def test_auto_detect(self, src): assert "language=self._whisper_language" in src class TestConvFields: @pytest.fixture(scope="class") def src(self): return _read_src("saltybot_social/conversation_node.py") def test_speaker_lang(self, src): assert "_speaker_lang" in src def test_lang_hint_method(self, src): assert "_language_hint" in src def test_msg_language(self, src): assert "msg.language = language" in src def test_lang_names(self, src): assert "_LANG_NAMES" in src def test_please_respond(self, src): assert "Please respond in" in src def test_emotion_coexists(self, src): assert "_emotion_hint" in src class TestTtsFields: @pytest.fixture(scope="class") def src(self): return _read_src("saltybot_social/tts_node.py") def test_voice_map_json(self, src): assert "voice_map_json" in src def test_default_lang(self, src): assert "default_language" in src def test_voices_dict(self, src): assert "_voices" in src def test_get_voice(self, src): assert "_get_voice" in src def test_load_voice_for_lang(self, src): assert "_load_voice_for_lang" in src def test_queue_tuple(self, src): assert "(text.strip(), lang)" in src def test_synthesize_voice_arg(self, src): assert "_synthesize(text, voice)" in src class TestMsgDefs: @pytest.fixture(scope="class") def tr(self): return _read_src("../saltybot_social_msgs/msg/SpeechTranscript.msg") @pytest.fixture(scope="class") def re(self): return _read_src("../saltybot_social_msgs/msg/ConversationResponse.msg") def test_transcript_lang(self, tr): assert "string language" in tr def test_transcript_bcp47(self, tr): assert "BCP-47" in tr def test_response_lang(self, re): assert "string language" in re class TestEdgeCases: def test_empty_lang_no_hint(self): lang = "" or "en"; assert lang == "en" def test_lang_flows(self): d: Dict[str,str] = {}; d["p1"] = "fr" assert d.get("p1","en") == "fr" def test_multi_speakers(self): d = {"p1":"fr","p2":"es"} assert d["p1"] == "fr" and d["p2"] == "es" def test_voice_map_code_in_tts(self): src = _read_src("saltybot_social/tts_node.py") assert "voice_map_json" in src and "json.loads" in src