Lyon_chatbox / tests /cascade /test_loader.py
Jacid23's picture
Rebrand app as Lyon Chatbox
dfedf76
Raw
History Blame Contribute Delete
6.22 kB
"""Tests for transcript analysis loader."""
import types
import yaml
from lyon_chatbox.cascade.transcript_analysis import loader as loader_mod
from lyon_chatbox.cascade.transcript_analysis.base import TriggerConfig
from lyon_chatbox.cascade.transcript_analysis.loader import _parse_trigger
# --- _parse_trigger ---
def test_parse_trigger_words_only():
"""Parse trigger with only words."""
t = _parse_trigger({"words": ["guitar", "bass"]})
assert t.words == ["guitar", "bass"]
assert t.entities == []
assert t.all == []
def test_parse_trigger_entities_only():
"""Parse trigger with only entities."""
t = _parse_trigger({"entities": ["PERSON", "ORG"]})
assert t.entities == ["PERSON", "ORG"]
assert t.words == []
def test_parse_trigger_all_groups():
"""Parse trigger with nested all sub-triggers."""
t = _parse_trigger({"all": [{"words": ["danc*"]}, {"words": ["groov*"]}]})
assert len(t.all) == 2
assert t.all[0].words == ["danc*"]
assert t.all[1].words == ["groov*"]
assert t.words == []
def test_parse_trigger_empty_dict():
"""Parse empty dict into default TriggerConfig."""
t = _parse_trigger({})
assert t == TriggerConfig()
# --- _import_callback ---
def test_import_callback_success(monkeypatch):
"""Return function when import and getattr succeed."""
def fake_fn():
pass
fake_module = types.ModuleType("fake")
setattr(fake_module, "my_callback", fake_fn)
monkeypatch.setattr("importlib.import_module", lambda path: fake_module)
result = loader_mod._import_callback("test_profile", "my_callback")
assert result is fake_fn
def test_import_callback_import_error(monkeypatch):
"""Return None when module import fails."""
monkeypatch.setattr("importlib.import_module", lambda path: (_ for _ in ()).throw(ImportError("no module")))
result = loader_mod._import_callback("test_profile", "missing_mod")
assert result is None
def test_import_callback_missing_attribute(monkeypatch):
"""Return None when module lacks the callback attribute."""
fake_module = types.ModuleType("fake")
monkeypatch.setattr("importlib.import_module", lambda path: fake_module)
result = loader_mod._import_callback("test_profile", "nonexistent")
assert result is None
# --- get_profile_reactions ---
def _setup_profile(monkeypatch, tmp_path, profile_name, yaml_content=None, callback_fn=None):
"""Set up a fake profile directory with optional YAML and callback."""
monkeypatch.setattr(loader_mod, "PROFILES_DIRECTORY", tmp_path)
# Monkeypatch config object
config_obj = types.SimpleNamespace(LYON_CHATBOX_CUSTOM_PROFILE=profile_name)
monkeypatch.setattr(loader_mod, "config", config_obj)
if profile_name:
profile_dir = tmp_path / profile_name
profile_dir.mkdir(parents=True, exist_ok=True)
if yaml_content is not None:
reactions_file = profile_dir / "reactions.yaml"
reactions_file.write_text(yaml.dump(yaml_content))
if callback_fn is not None:
monkeypatch.setattr(loader_mod, "_import_callback", callback_fn)
def test_no_profile_returns_none(monkeypatch, tmp_path):
"""Return None when no profile is configured."""
_setup_profile(monkeypatch, tmp_path, profile_name=None)
assert loader_mod.get_profile_reactions() is None
def test_no_reactions_yaml_returns_none(monkeypatch, tmp_path):
"""Return None when profile dir exists but has no reactions.yaml."""
_setup_profile(monkeypatch, tmp_path, profile_name="myprofile")
assert loader_mod.get_profile_reactions() is None
def test_valid_yaml_loads_reactions(monkeypatch, tmp_path):
"""Load a complete reaction from valid YAML."""
async def fake_cb(deps, match):
pass
_setup_profile(
monkeypatch, tmp_path, profile_name="myprofile",
yaml_content=[{
"name": "greet",
"callback": "greet",
"trigger": {"words": ["hello", "hi"]},
}],
callback_fn=lambda profile, name: fake_cb,
)
reactions = loader_mod.get_profile_reactions()
assert reactions is not None
assert len(reactions) == 1
assert reactions[0].name == "greet"
assert reactions[0].trigger.words == ["hello", "hi"]
assert reactions[0].callback is fake_cb
def test_skips_entry_missing_name(monkeypatch, tmp_path):
"""Skip YAML entries that lack a name field."""
async def fake_cb(deps, match):
pass
_setup_profile(
monkeypatch, tmp_path, profile_name="myprofile",
yaml_content=[{"callback": "something", "trigger": {"words": ["x"]}}],
callback_fn=lambda profile, name: fake_cb,
)
assert loader_mod.get_profile_reactions() is None
def test_skips_entry_bad_callback(monkeypatch, tmp_path):
"""Skip entries whose callback fails to import."""
_setup_profile(
monkeypatch, tmp_path, profile_name="myprofile",
yaml_content=[{
"name": "broken",
"callback": "broken",
"trigger": {"words": ["x"]},
}],
callback_fn=lambda profile, name: None,
)
assert loader_mod.get_profile_reactions() is None
def test_empty_yaml_returns_none(monkeypatch, tmp_path):
"""Return None when YAML contains an empty list."""
_setup_profile(
monkeypatch, tmp_path, profile_name="myprofile",
yaml_content=[],
)
assert loader_mod.get_profile_reactions() is None
def test_preserves_params_and_repeatable(monkeypatch, tmp_path):
"""Preserve params dict and repeatable flag from YAML."""
async def fake_cb(deps, match):
pass
_setup_profile(
monkeypatch, tmp_path, profile_name="myprofile",
yaml_content=[{
"name": "wave",
"callback": "wave",
"trigger": {"words": ["wave"]},
"params": {"direction": "left", "speed": 2},
"repeatable": True,
}],
callback_fn=lambda profile, name: fake_cb,
)
reactions = loader_mod.get_profile_reactions()
assert reactions is not None
r = reactions[0]
assert r.params == {"direction": "left", "speed": 2}
assert r.repeatable is True