| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508 |
- # -*- coding: utf-8 -*-
- """
- Tests fuer den YAML Intent-Loader.
- Testet YAML-Laden, Sprach-Fallback, mtime-basiertes Caching,
- Merge-Logik und Fehlerbehandlung.
- """
- import time
- import pytest
- from pathlib import Path
- from unittest.mock import patch
- from trixy_core.nlp.intent_loader import (
- load_intent_yaml,
- merge_yaml_into_metadata,
- clear_cache,
- _yaml_cache,
- )
- # ============================================================================
- # Fixtures
- # ============================================================================
- @pytest.fixture(autouse=True)
- def clear_yaml_cache():
- """Leert den YAML-Cache vor und nach jedem Test."""
- clear_cache()
- yield
- clear_cache()
- @pytest.fixture
- def yaml_dir(tmp_path):
- """Erstellt ein Verzeichnis mit YAML-Testdateien."""
- # Deutsche YAML-Datei
- de_file = tmp_path / "intents_de.yml"
- de_file.write_text(
- "current_weather:\n"
- " patterns:\n"
- ' - "(wie ist|wie wird) wetter"\n'
- ' - "wetter [in] {city}"\n'
- " examples:\n"
- ' - "Wie ist das Wetter?"\n'
- ' - "Wetter in Berlin"\n'
- "\n"
- "stop:\n"
- " patterns:\n"
- ' - "stopp"\n'
- ' - "halt"\n'
- " examples:\n"
- ' - "Stopp"\n',
- encoding="utf-8",
- )
- # Englische YAML-Datei
- en_file = tmp_path / "intents_en.yml"
- en_file.write_text(
- "current_weather:\n"
- " patterns:\n"
- ' - "(what is|how is) [the] weather"\n'
- ' - "weather [in] {city}"\n'
- " examples:\n"
- ' - "What is the weather?"\n'
- ' - "Weather in London"\n',
- encoding="utf-8",
- )
- return tmp_path
- @pytest.fixture
- def yaml_dir_nur_de(tmp_path):
- """Verzeichnis mit nur deutscher YAML-Datei."""
- de_file = tmp_path / "intents_de.yml"
- de_file.write_text(
- "test_intent:\n"
- " patterns:\n"
- ' - "test muster"\n'
- " examples:\n"
- ' - "Test Beispiel"\n',
- encoding="utf-8",
- )
- return tmp_path
- @pytest.fixture
- def yaml_dir_leer(tmp_path):
- """Verzeichnis ohne YAML-Dateien."""
- return tmp_path
- # ============================================================================
- # YAML laden
- # ============================================================================
- class TestLoadIntentYaml:
- """Tests fuer load_intent_yaml()."""
- def test_laden_deutsch(self, yaml_dir):
- """Deutsche YAML-Datei sollte geladen werden."""
- data = load_intent_yaml(yaml_dir, "de")
- assert "current_weather" in data
- assert "stop" in data
- assert len(data["current_weather"]["patterns"]) == 2
- assert len(data["current_weather"]["examples"]) == 2
- def test_laden_englisch(self, yaml_dir):
- """Englische YAML-Datei sollte geladen werden."""
- data = load_intent_yaml(yaml_dir, "en")
- assert "current_weather" in data
- assert "stop" not in data # Nur in DE definiert
- assert "What is the weather?" in data["current_weather"]["examples"]
- def test_fallback_auf_deutsch(self, yaml_dir_nur_de):
- """Bei fehlender Sprach-Datei sollte Fallback auf DE erfolgen."""
- data = load_intent_yaml(yaml_dir_nur_de, "en")
- assert "test_intent" in data
- assert data["test_intent"]["patterns"] == ["test muster"]
- def test_kein_fallback_fuer_de(self, yaml_dir_leer):
- """Wenn DE-Datei fehlt, sollte leeres Dict zurueckkommen."""
- data = load_intent_yaml(yaml_dir_leer, "de")
- assert data == {}
- def test_leeres_verzeichnis(self, yaml_dir_leer):
- """Leeres Verzeichnis sollte leeres Dict ergeben."""
- data = load_intent_yaml(yaml_dir_leer, "de")
- assert data == {}
- def test_ungueltige_yaml(self, tmp_path):
- """Ungueltige YAML sollte leeres Dict ergeben."""
- bad_file = tmp_path / "intents_de.yml"
- bad_file.write_text("{{invalid yaml: [", encoding="utf-8")
- data = load_intent_yaml(tmp_path, "de")
- assert data == {}
- def test_yaml_kein_dict(self, tmp_path):
- """YAML die kein Dict ist sollte leeres Dict ergeben."""
- bad_file = tmp_path / "intents_de.yml"
- bad_file.write_text("- item1\n- item2\n", encoding="utf-8")
- data = load_intent_yaml(tmp_path, "de")
- assert data == {}
- def test_patterns_als_strings(self, yaml_dir):
- """Patterns sollten als Strings geladen werden."""
- data = load_intent_yaml(yaml_dir, "de")
- for pattern in data["current_weather"]["patterns"]:
- assert isinstance(pattern, str)
- def test_examples_als_strings(self, yaml_dir):
- """Examples sollten als Strings geladen werden."""
- data = load_intent_yaml(yaml_dir, "de")
- for example in data["current_weather"]["examples"]:
- assert isinstance(example, str)
- def test_intent_ohne_patterns(self, tmp_path):
- """Intent ohne Patterns sollte nur Examples haben."""
- yaml_file = tmp_path / "intents_de.yml"
- yaml_file.write_text(
- "simple_intent:\n"
- " examples:\n"
- ' - "Einfach"\n',
- encoding="utf-8",
- )
- data = load_intent_yaml(tmp_path, "de")
- assert "simple_intent" in data
- assert "patterns" not in data["simple_intent"]
- assert "examples" in data["simple_intent"]
- def test_intent_ohne_examples(self, tmp_path):
- """Intent ohne Examples sollte nur Patterns haben."""
- yaml_file = tmp_path / "intents_de.yml"
- yaml_file.write_text(
- "pattern_only:\n"
- " patterns:\n"
- ' - "test muster"\n',
- encoding="utf-8",
- )
- data = load_intent_yaml(tmp_path, "de")
- assert "pattern_only" in data
- assert "patterns" in data["pattern_only"]
- assert "examples" not in data["pattern_only"]
- # ============================================================================
- # Caching
- # ============================================================================
- class TestCaching:
- """Tests fuer den mtime-basierten Cache."""
- def test_cache_wird_genutzt(self, yaml_dir):
- """Zweiter Aufruf sollte aus dem Cache kommen."""
- data1 = load_intent_yaml(yaml_dir, "de")
- data2 = load_intent_yaml(yaml_dir, "de")
- # Gleiche Daten
- assert data1 == data2
- # Cache sollte Eintrag haben
- assert len(_yaml_cache) > 0
- def test_cache_invalidiert_bei_aenderung(self, yaml_dir):
- """Cache sollte bei Datei-Aenderung invalidiert werden."""
- data1 = load_intent_yaml(yaml_dir, "de")
- assert "current_weather" in data1
- # Datei aendern
- time.sleep(0.05) # mtime-Aufloesung sicherstellen
- yaml_file = yaml_dir / "intents_de.yml"
- yaml_file.write_text(
- "new_intent:\n"
- " patterns:\n"
- ' - "neues muster"\n',
- encoding="utf-8",
- )
- data2 = load_intent_yaml(yaml_dir, "de")
- assert "new_intent" in data2
- assert "current_weather" not in data2
- def test_clear_cache(self, yaml_dir):
- """clear_cache() sollte den Cache leeren."""
- load_intent_yaml(yaml_dir, "de")
- assert len(_yaml_cache) > 0
- clear_cache()
- assert len(_yaml_cache) == 0
- def test_verschiedene_sprachen_separat_gecached(self, yaml_dir):
- """Verschiedene Sprachen sollten separat gecached werden."""
- data_de = load_intent_yaml(yaml_dir, "de")
- data_en = load_intent_yaml(yaml_dir, "en")
- # Unterschiedliche Daten
- assert data_de != data_en
- # Beide sollten im Cache sein
- assert len(_yaml_cache) >= 2
- # ============================================================================
- # Merge-Logik
- # ============================================================================
- class TestMergeYamlIntoMetadata:
- """Tests fuer merge_yaml_into_metadata()."""
- def test_yaml_ueberschreibt_patterns(self):
- """YAML-Patterns sollten Dekorator-Patterns ersetzen."""
- metadata = {
- "name": "test_intent",
- "patterns": ["altes muster"],
- "examples": ["Altes Beispiel"],
- }
- yaml_data = {
- "test_intent": {
- "patterns": ["neues muster eins", "neues muster zwei"],
- }
- }
- result = merge_yaml_into_metadata(metadata, yaml_data)
- assert result["patterns"] == ["neues muster eins", "neues muster zwei"]
- # Examples bleiben (nicht in YAML ueberschrieben)
- assert result["examples"] == ["Altes Beispiel"]
- def test_yaml_ueberschreibt_examples(self):
- """YAML-Examples sollten Dekorator-Examples ersetzen."""
- metadata = {
- "name": "test_intent",
- "patterns": ["muster"],
- "examples": ["Alt"],
- }
- yaml_data = {
- "test_intent": {
- "examples": ["Neu 1", "Neu 2"],
- }
- }
- result = merge_yaml_into_metadata(metadata, yaml_data)
- assert result["examples"] == ["Neu 1", "Neu 2"]
- # Patterns bleiben (nicht in YAML ueberschrieben)
- assert result["patterns"] == ["muster"]
- def test_yaml_ueberschreibt_beides(self):
- """YAML sollte beides ersetzen wenn vorhanden."""
- metadata = {
- "name": "test_intent",
- "patterns": ["alt"],
- "examples": ["Alt"],
- }
- yaml_data = {
- "test_intent": {
- "patterns": ["neu"],
- "examples": ["Neu"],
- }
- }
- result = merge_yaml_into_metadata(metadata, yaml_data)
- assert result["patterns"] == ["neu"]
- assert result["examples"] == ["Neu"]
- def test_kein_match_in_yaml(self):
- """Ohne Match in YAML sollte Metadata unveraendert bleiben."""
- metadata = {
- "name": "other_intent",
- "patterns": ["original"],
- "examples": ["Original"],
- }
- yaml_data = {
- "test_intent": {
- "patterns": ["ueberschrieben"],
- }
- }
- result = merge_yaml_into_metadata(metadata, yaml_data)
- assert result["patterns"] == ["original"]
- assert result["examples"] == ["Original"]
- def test_leerer_name(self):
- """Metadata ohne Name sollte unveraendert bleiben."""
- metadata = {
- "name": "",
- "patterns": ["muster"],
- }
- yaml_data = {"": {"patterns": ["test"]}}
- result = merge_yaml_into_metadata(metadata, yaml_data)
- assert result["patterns"] == ["muster"]
- def test_leere_yaml_data(self):
- """Leere YAML-Daten sollten Metadata unveraendert lassen."""
- metadata = {
- "name": "test_intent",
- "patterns": ["muster"],
- }
- result = merge_yaml_into_metadata(metadata, {})
- assert result["patterns"] == ["muster"]
- def test_in_place_modifikation(self):
- """Merge sollte das gleiche Dict zurueckgeben (in-place)."""
- metadata = {
- "name": "test_intent",
- "patterns": ["alt"],
- }
- yaml_data = {"test_intent": {"patterns": ["neu"]}}
- result = merge_yaml_into_metadata(metadata, yaml_data)
- assert result is metadata # Gleiche Referenz
- # ============================================================================
- # Echte YAML-Dateien
- # ============================================================================
- class TestEchteYamlDateien:
- """Tests mit den echten YAML-Dateien des Projekts."""
- def _find_project_root(self) -> Path:
- """Findet das Projekt-Root-Verzeichnis."""
- current = Path(__file__).parent.parent
- if (current / "trixy_core").exists():
- return current
- return current
- def test_system_intents_yaml_existiert(self):
- """System-Intents YAML-Datei sollte existieren."""
- root = self._find_project_root()
- yaml_path = root / "trixy_core" / "nlp" / "intents_de.yml"
- assert yaml_path.exists(), f"System-Intents YAML nicht gefunden: {yaml_path}"
- def test_system_intents_yaml_ladbar(self):
- """System-Intents YAML sollte ladbar sein."""
- root = self._find_project_root()
- nlp_dir = root / "trixy_core" / "nlp"
- data = load_intent_yaml(nlp_dir, "de")
- assert len(data) > 0, "System-Intents YAML ist leer"
- # Bekannte Intents pruefen
- for expected in ["stop", "pause", "resume", "help", "get_time"]:
- assert expected in data, f"Intent '{expected}' fehlt in System-YAML"
- def test_weather_intents_yaml_existiert(self):
- """Weather-Plugin YAML-Datei sollte existieren."""
- root = self._find_project_root()
- yaml_path = root / "plugins" / "weather" / "intents_de.yml"
- assert yaml_path.exists(), f"Weather-Intents YAML nicht gefunden: {yaml_path}"
- def test_weather_intents_yaml_ladbar(self):
- """Weather-Plugin YAML sollte ladbar sein."""
- root = self._find_project_root()
- weather_dir = root / "plugins" / "weather"
- data = load_intent_yaml(weather_dir, "de")
- assert "current_weather" in data
- assert "patterns" in data["current_weather"]
- assert "examples" in data["current_weather"]
- def test_timer_intents_yaml_ladbar(self):
- """Timer-Plugin YAML sollte ladbar sein."""
- root = self._find_project_root()
- timer_dir = root / "plugins" / "timer"
- if not (timer_dir / "intents_de.yml").exists():
- pytest.skip("Timer-Intents YAML nicht vorhanden")
- data = load_intent_yaml(timer_dir, "de")
- assert "set_timer" in data
- def test_calendar_intents_yaml_ladbar(self):
- """Calendar-Plugin YAML sollte ladbar sein."""
- root = self._find_project_root()
- calendar_dir = root / "plugins" / "calendar"
- if not (calendar_dir / "intents_de.yml").exists():
- pytest.skip("Calendar-Intents YAML nicht vorhanden")
- data = load_intent_yaml(calendar_dir, "de")
- assert "next_appointment" in data
- def test_music_intents_yaml_ladbar(self):
- """Music-Plugin YAML sollte ladbar sein."""
- root = self._find_project_root()
- music_dir = root / "plugins" / "music"
- if not (music_dir / "intents_de.yml").exists():
- pytest.skip("Music-Intents YAML nicht vorhanden")
- data = load_intent_yaml(music_dir, "de")
- assert "play_music" in data
- # ============================================================================
- # Fehlerbehandlung
- # ============================================================================
- class TestFehlerbehandlung:
- """Tests fuer Fehlerbehandlung."""
- def test_nicht_existierendes_verzeichnis(self):
- """Nicht existierendes Verzeichnis sollte leeres Dict ergeben."""
- data = load_intent_yaml(Path("/nicht/existent"), "de")
- assert data == {}
- def test_yaml_mit_ungueltigem_intent(self, tmp_path):
- """Ungueltiger Intent-Eintrag (kein Dict) sollte uebersprungen werden."""
- yaml_file = tmp_path / "intents_de.yml"
- yaml_file.write_text(
- 'valid_intent:\n'
- ' patterns:\n'
- ' - "test"\n'
- 'invalid_intent: "nicht ein dict"\n',
- encoding="utf-8",
- )
- data = load_intent_yaml(tmp_path, "de")
- assert "valid_intent" in data
- assert "invalid_intent" not in data
- def test_yaml_mit_leeren_patterns(self, tmp_path):
- """Leere Pattern-Eintraege sollten gefiltert werden."""
- yaml_file = tmp_path / "intents_de.yml"
- yaml_file.write_text(
- "test_intent:\n"
- " patterns:\n"
- ' - "gueltig"\n'
- " - \n"
- ' - "auch gueltig"\n',
- encoding="utf-8",
- )
- data = load_intent_yaml(tmp_path, "de")
- # None/leere Eintraege sollten gefiltert sein
- patterns = data["test_intent"]["patterns"]
- assert all(p for p in patterns)
- def test_kein_pyyaml_installiert(self, tmp_path):
- """Fehlende PyYAML-Installation sollte leeres Dict ergeben."""
- yaml_file = tmp_path / "intents_de.yml"
- yaml_file.write_text("test: {}", encoding="utf-8")
- with patch.dict("sys.modules", {"yaml": None}):
- # Importfehler simulieren
- import importlib
- import trixy_core.nlp.intent_loader as loader_module
- # Cache leeren damit neu geladen wird
- clear_cache()
- # Der Import-Guard in _load_yaml_file faengt das ab
- # (in der echten Implementierung passiert das beim import yaml)
|