File size: 6,216 Bytes
534b431
 
 
 
 
 
dfedf76
 
 
534b431
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dfedf76
534b431
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
"""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