# -*- coding: utf-8 -*- """ Tests fuer die Intent-Dekoratoren (@intent, @pattern, @example). Testet Stackbarkeit, Slot-Erkennung aus Funktionssignatur, Zusammenspiel mit @intent und discover_intent_handlers. """ import asyncio import inspect import pytest from typing import Any from trixy_core.nlp.decorators import ( intent, pattern, example, get_intent_metadata, is_intent_handler, discover_intent_handlers, _extract_slots_from_signature, INTENT_METADATA_ATTR, _PATTERN_LIST_ATTR, _EXAMPLE_LIST_ATTR, ) from trixy_core.nlp.intent_registry import IntentSlot # ============================================================================ # @pattern Dekorator # ============================================================================ class TestPatternDecorator: """Tests fuer den @pattern Dekorator.""" def test_einzelnes_pattern(self): """Ein einzelnes Pattern sollte gespeichert werden.""" @pattern("test muster") async def handler(data): pass patterns = getattr(handler, _PATTERN_LIST_ATTR, []) assert len(patterns) == 1 assert patterns[0] == "test muster" def test_mehrere_patterns_stackbar(self): """Mehrere @pattern Dekoratoren sollten gestackt werden.""" @pattern("(wie ist|wie wird) wetter") @pattern("wetter [in] {city}") @pattern("temperatur [draussen]") async def handler(data): pass patterns = getattr(handler, _PATTERN_LIST_ATTR, []) assert len(patterns) == 3 def test_pattern_behaelt_funktion(self): """@pattern sollte die Funktion nicht veraendern.""" @pattern("test") async def handler(data): return "result" assert asyncio.iscoroutinefunction(handler) def test_pattern_kein_intent_handler(self): """@pattern allein macht keine Funktion zum Intent-Handler.""" @pattern("test") async def handler(data): pass assert not is_intent_handler(handler) # ============================================================================ # @example Dekorator # ============================================================================ class TestExampleDecorator: """Tests fuer den @example Dekorator.""" def test_einzelnes_example(self): """Ein einzelnes Example sollte gespeichert werden.""" @example("Wie ist das Wetter?") async def handler(data): pass examples = getattr(handler, _EXAMPLE_LIST_ATTR, []) assert len(examples) == 1 assert examples[0] == "Wie ist das Wetter?" def test_mehrere_examples_in_einem_aufruf(self): """Mehrere Examples in einem @example sollten gespeichert werden.""" @example("Hallo", "Hi", "Guten Tag") async def handler(data): pass examples = getattr(handler, _EXAMPLE_LIST_ATTR, []) assert len(examples) == 3 def test_mehrere_example_dekoratoren_stackbar(self): """Mehrere @example Dekoratoren sollten gestackt werden.""" @example("Wie ist das Wetter?") @example("Wie warm ist es?") async def handler(data): pass examples = getattr(handler, _EXAMPLE_LIST_ATTR, []) assert len(examples) == 2 def test_example_behaelt_funktion(self): """@example sollte die Funktion nicht veraendern.""" @example("Test") async def handler(data): return "result" assert asyncio.iscoroutinefunction(handler) def test_example_kein_intent_handler(self): """@example allein macht keine Funktion zum Intent-Handler.""" @example("Test") async def handler(data): pass assert not is_intent_handler(handler) # ============================================================================ # @intent Dekorator # ============================================================================ class TestIntentDecorator: """Tests fuer den @intent Dekorator.""" def test_grundlegend(self): """@intent sollte Metadaten hinzufuegen.""" @intent("test_intent") async def handler(data): pass assert is_intent_handler(handler) metadata = get_intent_metadata(handler) assert metadata is not None assert metadata["name"] == "test_intent" def test_mit_description(self): """@intent sollte Description speichern.""" @intent("test", description="Testbeschreibung") async def handler(data): pass metadata = get_intent_metadata(handler) assert metadata["description"] == "Testbeschreibung" def test_mit_priority(self): """@intent sollte Priority speichern.""" @intent("test", priority=5) async def handler(data): pass metadata = get_intent_metadata(handler) assert metadata["priority"] == 5 def test_mit_expliziten_examples(self): """@intent sollte explizite Examples speichern.""" @intent("test", examples=["Hallo", "Hi"]) async def handler(data): pass metadata = get_intent_metadata(handler) assert "Hallo" in metadata["examples"] assert "Hi" in metadata["examples"] def test_mit_expliziten_patterns(self): """@intent sollte explizite Patterns speichern.""" @intent("test", patterns=["muster eins", "muster zwei"]) async def handler(data): pass metadata = get_intent_metadata(handler) assert "muster eins" in metadata["patterns"] assert "muster zwei" in metadata["patterns"] def test_mit_expliziten_slots_type(self): """@intent sollte Typ-basierte Slots normalisieren.""" @intent("test", slots={"device": str, "count": int}) async def handler(data): pass metadata = get_intent_metadata(handler) assert "device" in metadata["slots"] assert isinstance(metadata["slots"]["device"], IntentSlot) assert metadata["slots"]["device"].slot_type is str assert metadata["slots"]["count"].slot_type is int def test_mit_expliziten_slots_intentslot(self): """@intent sollte IntentSlot-Objekte direkt akzeptieren.""" @intent("test", slots={ "temp": IntentSlot(name="temp", slot_type=float, required=True), }) async def handler(data): pass metadata = get_intent_metadata(handler) assert metadata["slots"]["temp"].slot_type is float assert metadata["slots"]["temp"].required is True def test_wrapper_ist_async(self): """Der Wrapper sollte async sein.""" @intent("test") async def handler(data): return "result" assert asyncio.iscoroutinefunction(handler) # ============================================================================ # Zusammenspiel @intent + @pattern + @example # ============================================================================ class TestDekoratorZusammenspiel: """Tests fuer das Zusammenspiel der Dekoratoren.""" def test_intent_sammelt_pattern(self): """@intent sollte @pattern-Dekoratoren sammeln.""" @intent("weather") @pattern("(wie ist|wie wird) wetter") @pattern("wetter [in] {city}") async def handler(data): pass metadata = get_intent_metadata(handler) assert len(metadata["patterns"]) == 2 assert "(wie ist|wie wird) wetter" in metadata["patterns"] assert "wetter [in] {city}" in metadata["patterns"] def test_intent_sammelt_example(self): """@intent sollte @example-Dekoratoren sammeln.""" @intent("weather") @example("Wie ist das Wetter?") @example("Wetter in Berlin", "Wie warm ist es?") async def handler(data): pass metadata = get_intent_metadata(handler) assert len(metadata["examples"]) == 3 def test_intent_merged_explizite_und_dekorator_patterns(self): """Explizite Patterns und @pattern-Dekoratoren sollten gemergt werden.""" @intent("test", patterns=["explizit"]) @pattern("dekorator") async def handler(data): pass metadata = get_intent_metadata(handler) assert "explizit" in metadata["patterns"] assert "dekorator" in metadata["patterns"] def test_intent_merged_explizite_und_dekorator_examples(self): """Explizite Examples und @example-Dekoratoren sollten gemergt werden.""" @intent("test", examples=["Explizit"]) @example("Dekorator") async def handler(data): pass metadata = get_intent_metadata(handler) assert "Explizit" in metadata["examples"] assert "Dekorator" in metadata["examples"] def test_vollstaendiges_setup(self): """Komplettes Setup mit allen Dekoratoren.""" @intent("current_weather", description="Wetter abfragen") @pattern("(wie ist|wie wird) [denn] wetter") @pattern("wetter [in] {city}") @example("Wie ist das Wetter?", "Wie warm ist es?") @example("Wetter in Berlin") async def handle_weather(self, data, city: str = ""): pass metadata = get_intent_metadata(handle_weather) assert metadata["name"] == "current_weather" assert metadata["description"] == "Wetter abfragen" assert len(metadata["patterns"]) == 2 assert len(metadata["examples"]) == 3 assert "city" in metadata["slots"] assert metadata["slots"]["city"].required is False # ============================================================================ # Slot-Erkennung aus Signatur # ============================================================================ class TestSlotExtractionFromSignature: """Tests fuer _extract_slots_from_signature().""" def test_optionaler_slot(self): """Parameter mit Default-Wert sollte optionaler Slot sein.""" async def handler(self, data, city: str = ""): pass slots = _extract_slots_from_signature(handler) assert "city" in slots assert slots["city"].required is False assert slots["city"].slot_type is str def test_erforderlicher_slot(self): """Parameter ohne Default-Wert sollte erforderlicher Slot sein.""" async def handler(self, data, device: str): pass slots = _extract_slots_from_signature(handler) assert "device" in slots assert slots["device"].required is True assert slots["device"].slot_type is str def test_verschiedene_typen(self): """Verschiedene Typ-Annotationen sollten erkannt werden.""" async def handler(self, data, count: int = 0, temp: float = 0.0, name: str = ""): pass slots = _extract_slots_from_signature(handler) assert slots["count"].slot_type is int assert slots["temp"].slot_type is float assert slots["name"].slot_type is str def test_skip_self_und_data(self): """'self' und 'data' sollten uebersprungen werden.""" async def handler(self, data, city: str = ""): pass slots = _extract_slots_from_signature(handler) assert "self" not in slots assert "data" not in slots assert "city" in slots def test_skip_args_kwargs(self): """*args und **kwargs sollten uebersprungen werden.""" async def handler(self, data, *args, city: str = "", **kwargs): pass slots = _extract_slots_from_signature(handler) assert "args" not in slots assert "kwargs" not in slots assert "city" in slots def test_ohne_annotation(self): """Parameter ohne Annotation sollte als str behandelt werden.""" async def handler(self, data, city=""): pass slots = _extract_slots_from_signature(handler) assert "city" in slots assert slots["city"].slot_type is str def test_keine_slots(self): """Funktion ohne Slot-Parameter sollte leeres Dict ergeben.""" async def handler(self, data): pass slots = _extract_slots_from_signature(handler) assert len(slots) == 0 def test_intent_erkennt_signatur_slots(self): """@intent sollte Slots automatisch aus der Signatur erkennen.""" @intent("test") async def handler(self, data, city: str = "", count: int = 0): pass metadata = get_intent_metadata(handler) assert "city" in metadata["slots"] assert "count" in metadata["slots"] assert metadata["slots"]["city"].required is False assert metadata["slots"]["count"].slot_type is int def test_explizite_slots_ueberschreiben_signatur(self): """Explizite Slots sollten Signatur-Slots ueberschreiben.""" @intent("test", slots={ "city": IntentSlot(name="city", slot_type=str, required=True), }) async def handler(self, data, city: str = ""): pass metadata = get_intent_metadata(handler) # Explizit als required gesetzt, obwohl Signatur Default hat assert metadata["slots"]["city"].required is True # ============================================================================ # discover_intent_handlers # ============================================================================ class TestDiscoverIntentHandlers: """Tests fuer discover_intent_handlers().""" def test_findet_handler(self): """Sollte alle @intent-dekorierten Methoden finden.""" class TestPlugin: @intent("intent1") async def handler1(self, data): pass @intent("intent2") async def handler2(self, data): pass def normal_method(self): pass plugin = TestPlugin() handlers = discover_intent_handlers(plugin) assert len(handlers) == 2 names = [h[0] for h in handlers] assert "intent1" in names assert "intent2" in names def test_ueberspringe_private_methoden(self): """Private Methoden (mit _) sollten uebersprungen werden.""" class TestPlugin: @intent("public") async def public_handler(self, data): pass @intent("private") async def _private_handler(self, data): pass plugin = TestPlugin() handlers = discover_intent_handlers(plugin) names = [h[0] for h in handlers] assert "public" in names assert "private" not in names def test_handler_tupel_struktur(self): """Jeder Handler sollte (name, func, metadata) Tupel sein.""" class TestPlugin: @intent("test", description="Testbeschreibung") @pattern("test muster") @example("Test Beispiel") async def handler(self, data, city: str = ""): pass plugin = TestPlugin() handlers = discover_intent_handlers(plugin) assert len(handlers) == 1 name, func, metadata = handlers[0] assert name == "test" assert callable(func) assert metadata["description"] == "Testbeschreibung" assert len(metadata["patterns"]) == 1 assert len(metadata["examples"]) == 1 assert "city" in metadata["slots"] def test_leeres_objekt(self): """Objekt ohne Handler sollte leere Liste ergeben.""" class EmptyPlugin: def normal_method(self): pass plugin = EmptyPlugin() handlers = discover_intent_handlers(plugin) assert handlers == [] def test_mit_pattern_und_example_dekoratoren(self): """Handler mit @pattern und @example sollten korrekt gefunden werden.""" class TestPlugin: @intent("weather") @pattern("(wie ist|wie wird) wetter") @pattern("wetter [in] {city}") @example("Wie ist das Wetter?") async def handle_weather(self, data, city: str = ""): pass plugin = TestPlugin() handlers = discover_intent_handlers(plugin) assert len(handlers) == 1 _, _, metadata = handlers[0] assert len(metadata["patterns"]) == 2 assert len(metadata["examples"]) == 1 assert "city" in metadata["slots"] assert metadata["slots"]["city"].required is False # ============================================================================ # Hilfsfunktionen # ============================================================================ class TestHilfsfunktionen: """Tests fuer get_intent_metadata und is_intent_handler.""" def test_get_intent_metadata_handler(self): """Sollte Metadaten fuer Intent-Handler zurueckgeben.""" @intent("test") async def handler(data): pass metadata = get_intent_metadata(handler) assert metadata is not None assert metadata["name"] == "test" def test_get_intent_metadata_kein_handler(self): """Sollte None fuer Nicht-Handler zurueckgeben.""" async def handler(data): pass metadata = get_intent_metadata(handler) assert metadata is None def test_is_intent_handler_true(self): """Sollte True fuer Intent-Handler zurueckgeben.""" @intent("test") async def handler(data): pass assert is_intent_handler(handler) is True def test_is_intent_handler_false(self): """Sollte False fuer Nicht-Handler zurueckgeben.""" async def handler(data): pass assert is_intent_handler(handler) is False def test_is_intent_handler_mit_pattern_only(self): """@pattern allein sollte kein Intent-Handler sein.""" @pattern("test") async def handler(data): pass assert is_intent_handler(handler) is False def test_is_intent_handler_mit_example_only(self): """@example allein sollte kein Intent-Handler sein.""" @example("Test") async def handler(data): pass assert is_intent_handler(handler) is False