|
| 1 | +"""Tests for hooks/lib/tiny_actor_presets.py — Tiny Actor cast presets.""" |
| 2 | +import os |
| 3 | +import re |
| 4 | +import sys |
| 5 | + |
| 6 | +import pytest |
| 7 | + |
| 8 | +# Add hooks/lib to path |
| 9 | +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "hooks", "lib")) |
| 10 | + |
| 11 | +from tiny_actor_presets import ( |
| 12 | + BUDDY_FACE, |
| 13 | + CAST_PRESETS, |
| 14 | + DEFAULT_MODERATOR_COPY, |
| 15 | + get_cast_preset, |
| 16 | + get_moderator_copy, |
| 17 | +) |
| 18 | + |
| 19 | +VALID_MODES = ["PLAN", "EVAL", "AUTO", "SHIP"] |
| 20 | +KEBAB_CASE_RE = re.compile(r"^[a-z]+(-[a-z]+)*$") |
| 21 | +MAX_MODERATOR_COPY_LEN = 40 |
| 22 | + |
| 23 | + |
| 24 | +# --------------------------------------------------------------------------- |
| 25 | +# Agents directory for cross-referencing agent IDs |
| 26 | +# --------------------------------------------------------------------------- |
| 27 | + |
| 28 | +def _find_agents_dir(): |
| 29 | + """Locate .ai-rules/agents relative to project root.""" |
| 30 | + project_root = os.path.abspath( |
| 31 | + os.path.join(os.path.dirname(__file__), "..", "..", "..") |
| 32 | + ) |
| 33 | + candidates = [ |
| 34 | + os.path.join(project_root, "packages", "rules", ".ai-rules", "agents"), |
| 35 | + os.path.join(project_root, ".ai-rules", "agents"), |
| 36 | + ] |
| 37 | + for c in candidates: |
| 38 | + if os.path.isdir(c): |
| 39 | + return c |
| 40 | + return None |
| 41 | + |
| 42 | + |
| 43 | +AGENTS_DIR = _find_agents_dir() |
| 44 | + |
| 45 | + |
| 46 | +# --------------------------------------------------------------------------- |
| 47 | +# Basic preset tests |
| 48 | +# --------------------------------------------------------------------------- |
| 49 | + |
| 50 | + |
| 51 | +class TestGetCastPreset: |
| 52 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 53 | + def test_valid_mode_returns_dict(self, mode: str): |
| 54 | + result = get_cast_preset(mode) |
| 55 | + assert isinstance(result, dict) |
| 56 | + |
| 57 | + @pytest.mark.parametrize("mode", ["UNKNOWN", "debug", "", "plan"]) |
| 58 | + def test_unknown_mode_returns_none(self, mode: str): |
| 59 | + assert get_cast_preset(mode) is None |
| 60 | + |
| 61 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 62 | + def test_preset_has_required_keys(self, mode: str): |
| 63 | + preset = get_cast_preset(mode) |
| 64 | + assert "primary" in preset |
| 65 | + assert "specialists" in preset |
| 66 | + assert "moderator_copy" in preset |
| 67 | + |
| 68 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 69 | + def test_primary_is_string(self, mode: str): |
| 70 | + preset = get_cast_preset(mode) |
| 71 | + assert isinstance(preset["primary"], str) |
| 72 | + assert len(preset["primary"]) > 0 |
| 73 | + |
| 74 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 75 | + def test_specialists_is_nonempty_list(self, mode: str): |
| 76 | + preset = get_cast_preset(mode) |
| 77 | + assert isinstance(preset["specialists"], list) |
| 78 | + assert len(preset["specialists"]) >= 2 |
| 79 | + |
| 80 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 81 | + def test_moderator_copy_is_nonempty_string(self, mode: str): |
| 82 | + preset = get_cast_preset(mode) |
| 83 | + assert isinstance(preset["moderator_copy"], str) |
| 84 | + assert len(preset["moderator_copy"]) > 0 |
| 85 | + |
| 86 | + |
| 87 | +# --------------------------------------------------------------------------- |
| 88 | +# Determinism |
| 89 | +# --------------------------------------------------------------------------- |
| 90 | + |
| 91 | + |
| 92 | +class TestDeterminism: |
| 93 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 94 | + def test_same_input_same_output(self, mode: str): |
| 95 | + first = get_cast_preset(mode) |
| 96 | + second = get_cast_preset(mode) |
| 97 | + assert first == second |
| 98 | + |
| 99 | + |
| 100 | +# --------------------------------------------------------------------------- |
| 101 | +# Agent ID format (kebab-case) |
| 102 | +# --------------------------------------------------------------------------- |
| 103 | + |
| 104 | + |
| 105 | +class TestAgentIdFormat: |
| 106 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 107 | + def test_primary_is_kebab_case(self, mode: str): |
| 108 | + preset = get_cast_preset(mode) |
| 109 | + assert KEBAB_CASE_RE.match(preset["primary"]), ( |
| 110 | + f"primary '{preset['primary']}' is not kebab-case" |
| 111 | + ) |
| 112 | + |
| 113 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 114 | + def test_specialists_are_kebab_case(self, mode: str): |
| 115 | + preset = get_cast_preset(mode) |
| 116 | + for agent_id in preset["specialists"]: |
| 117 | + assert KEBAB_CASE_RE.match(agent_id), ( |
| 118 | + f"specialist '{agent_id}' is not kebab-case" |
| 119 | + ) |
| 120 | + |
| 121 | + |
| 122 | +# --------------------------------------------------------------------------- |
| 123 | +# Agent IDs reference existing agent JSON files |
| 124 | +# --------------------------------------------------------------------------- |
| 125 | + |
| 126 | + |
| 127 | +@pytest.mark.skipif(AGENTS_DIR is None, reason="agents directory not found") |
| 128 | +class TestAgentReferences: |
| 129 | + def _all_agent_ids(self): |
| 130 | + ids = set() |
| 131 | + for mode in VALID_MODES: |
| 132 | + preset = get_cast_preset(mode) |
| 133 | + ids.add(preset["primary"]) |
| 134 | + ids.update(preset["specialists"]) |
| 135 | + return ids |
| 136 | + |
| 137 | + def test_all_agent_ids_have_json_files(self): |
| 138 | + for agent_id in self._all_agent_ids(): |
| 139 | + path = os.path.join(AGENTS_DIR, f"{agent_id}.json") |
| 140 | + assert os.path.isfile(path), ( |
| 141 | + f"Agent '{agent_id}' has no JSON at {path}" |
| 142 | + ) |
| 143 | + |
| 144 | + |
| 145 | +# --------------------------------------------------------------------------- |
| 146 | +# Display budget — moderator copy length |
| 147 | +# --------------------------------------------------------------------------- |
| 148 | + |
| 149 | + |
| 150 | +class TestDisplayBudget: |
| 151 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 152 | + def test_moderator_copy_within_budget(self, mode: str): |
| 153 | + preset = get_cast_preset(mode) |
| 154 | + assert len(preset["moderator_copy"]) <= MAX_MODERATOR_COPY_LEN, ( |
| 155 | + f"moderator_copy too long ({len(preset['moderator_copy'])} chars)" |
| 156 | + ) |
| 157 | + |
| 158 | + def test_default_moderator_copy_within_budget(self): |
| 159 | + assert len(DEFAULT_MODERATOR_COPY) <= MAX_MODERATOR_COPY_LEN |
| 160 | + |
| 161 | + |
| 162 | +# --------------------------------------------------------------------------- |
| 163 | +# get_moderator_copy function |
| 164 | +# --------------------------------------------------------------------------- |
| 165 | + |
| 166 | + |
| 167 | +class TestGetModeratorCopy: |
| 168 | + @pytest.mark.parametrize("mode", VALID_MODES) |
| 169 | + def test_returns_mode_specific_copy(self, mode: str): |
| 170 | + copy = get_moderator_copy(mode) |
| 171 | + preset = get_cast_preset(mode) |
| 172 | + assert copy == preset["moderator_copy"] |
| 173 | + |
| 174 | + def test_unknown_mode_returns_default(self): |
| 175 | + assert get_moderator_copy("UNKNOWN") == DEFAULT_MODERATOR_COPY |
| 176 | + |
| 177 | + def test_returns_string(self): |
| 178 | + assert isinstance(get_moderator_copy("PLAN"), str) |
| 179 | + |
| 180 | + |
| 181 | +# --------------------------------------------------------------------------- |
| 182 | +# Buddy identity |
| 183 | +# --------------------------------------------------------------------------- |
| 184 | + |
| 185 | + |
| 186 | +class TestBuddyIdentity: |
| 187 | + def test_buddy_face_is_defined(self): |
| 188 | + assert isinstance(BUDDY_FACE, str) |
| 189 | + assert len(BUDDY_FACE) > 0 |
| 190 | + |
| 191 | + def test_buddy_face_value(self): |
| 192 | + assert BUDDY_FACE == "\u25d5\u203f\u25d5" |
| 193 | + |
| 194 | + |
| 195 | +# --------------------------------------------------------------------------- |
| 196 | +# CAST_PRESETS dict integrity |
| 197 | +# --------------------------------------------------------------------------- |
| 198 | + |
| 199 | + |
| 200 | +class TestCastPresetsDict: |
| 201 | + def test_has_all_valid_modes(self): |
| 202 | + for mode in VALID_MODES: |
| 203 | + assert mode in CAST_PRESETS |
| 204 | + |
| 205 | + def test_no_extra_modes(self): |
| 206 | + assert set(CAST_PRESETS.keys()) == set(VALID_MODES) |
0 commit comments