test_clones_unit.py
python
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2
feat: add repair-commit wire endpoint (API parity with repa…
Opus 4.8
minor
⚠ breaking
1 day ago
| 1 | """Tier 1 — Unit tests for clone browser helper functions (issue #17). |
| 2 | |
| 3 | Tests are strictly pure: no DB, no HTTP, no I/O. Every test exercises a |
| 4 | single helper from ``musehub.api.routes.musehub.ui_intel`` in isolation so |
| 5 | failures point exactly to the broken function. |
| 6 | |
| 7 | Helpers under test: |
| 8 | _cl_tier_class — tier name → CSS modifier |
| 9 | _cl_language_set — members_json → sorted distinct languages |
| 10 | _cl_file_count — members_json → distinct file count |
| 11 | _cl_is_cross_file — members_json → bool (files > 1) |
| 12 | _cl_parse_members — members_json → normalised list with ``file`` key |
| 13 | _cl_files_breakdown — normalised members → grouped-by-file breakdown |
| 14 | |
| 15 | Coverage target: 13 primary cases + edge cases for every parse-error path. |
| 16 | """ |
| 17 | from __future__ import annotations |
| 18 | |
| 19 | import json |
| 20 | |
| 21 | import pytest |
| 22 | |
| 23 | from musehub.api.routes.musehub.ui_intel import ( |
| 24 | _cl_file_count, |
| 25 | _cl_files_breakdown, |
| 26 | _cl_is_cross_file, |
| 27 | _cl_language_set, |
| 28 | _cl_parse_members, |
| 29 | _cl_tier_class, |
| 30 | ) |
| 31 | |
| 32 | # ───────────────────────────────────────────────────────────────────────────── |
| 33 | # Fixtures |
| 34 | # ───────────────────────────────────────────────────────────────────────────── |
| 35 | |
| 36 | _ONE_FILE = json.dumps( |
| 37 | [ |
| 38 | {"address": "src/a.py::foo", "kind": "function", "language": "Python"}, |
| 39 | {"address": "src/a.py::bar", "kind": "function", "language": "Python"}, |
| 40 | ] |
| 41 | ) |
| 42 | |
| 43 | _TWO_FILES = json.dumps( |
| 44 | [ |
| 45 | {"address": "src/a.py::foo", "kind": "function", "language": "Python"}, |
| 46 | {"address": "src/b.py::foo", "kind": "function", "language": "Python"}, |
| 47 | {"address": "docs/README.md::heading", "kind": "section", "language": "Markdown"}, |
| 48 | ] |
| 49 | ) |
| 50 | |
| 51 | _EMPTY_ARRAY = "[]" |
| 52 | _INVALID_JSON = "not json" |
| 53 | _BLANK = "" |
| 54 | |
| 55 | |
| 56 | # ───────────────────────────────────────────────────────────────────────────── |
| 57 | # _cl_tier_class |
| 58 | # ───────────────────────────────────────────────────────────────────────────── |
| 59 | |
| 60 | |
| 61 | class TestClTierClass: |
| 62 | """U01–U03: tier name maps to correct CSS modifier class.""" |
| 63 | |
| 64 | def test_U01_exact_returns_exact_class(self) -> None: |
| 65 | assert _cl_tier_class("exact") == "cl-badge--exact" |
| 66 | |
| 67 | def test_U02_near_returns_near_class(self) -> None: |
| 68 | assert _cl_tier_class("near") == "cl-badge--near" |
| 69 | |
| 70 | def test_U03_unknown_tier_returns_near_as_safe_default(self) -> None: |
| 71 | """Unknown tier must never raise — falls back to near (less alarming).""" |
| 72 | assert _cl_tier_class("unknown") == "cl-badge--near" |
| 73 | assert _cl_tier_class("") == "cl-badge--near" |
| 74 | |
| 75 | |
| 76 | # ───────────────────────────────────────────────────────────────────────────── |
| 77 | # _cl_language_set |
| 78 | # ───────────────────────────────────────────────────────────────────────────── |
| 79 | |
| 80 | |
| 81 | class TestClLanguageSet: |
| 82 | """U04–U07: members_json → sorted distinct language list.""" |
| 83 | |
| 84 | def test_U04_deduplicates_same_language(self) -> None: |
| 85 | blob = json.dumps([ |
| 86 | {"language": "Python"}, |
| 87 | {"language": "Python"}, |
| 88 | ]) |
| 89 | assert _cl_language_set(blob) == ["Python"] |
| 90 | |
| 91 | def test_U05_multiple_languages_sorted(self) -> None: |
| 92 | blob = json.dumps([ |
| 93 | {"language": "Python"}, |
| 94 | {"language": "Markdown"}, |
| 95 | ]) |
| 96 | assert _cl_language_set(blob) == ["Markdown", "Python"] |
| 97 | |
| 98 | def test_U06_invalid_json_returns_placeholder(self) -> None: |
| 99 | assert _cl_language_set(_INVALID_JSON) == ["—"] |
| 100 | |
| 101 | def test_U07_empty_array_returns_placeholder(self) -> None: |
| 102 | assert _cl_language_set(_EMPTY_ARRAY) == ["—"] |
| 103 | |
| 104 | def test_blank_string_returns_placeholder(self) -> None: |
| 105 | assert _cl_language_set(_BLANK) == ["—"] |
| 106 | |
| 107 | def test_members_missing_language_key_skipped(self) -> None: |
| 108 | blob = json.dumps([{"address": "a.py::fn", "kind": "function"}]) |
| 109 | assert _cl_language_set(blob) == ["—"] |
| 110 | |
| 111 | |
| 112 | # ───────────────────────────────────────────────────────────────────────────── |
| 113 | # _cl_file_count |
| 114 | # ───────────────────────────────────────────────────────────────────────────── |
| 115 | |
| 116 | |
| 117 | class TestClFileCount: |
| 118 | """U08–U10: members_json → count of distinct source files.""" |
| 119 | |
| 120 | def test_U08_cross_file_fixture_returns_correct_count(self) -> None: |
| 121 | # _TWO_FILES contains 3 distinct files: src/a.py, src/b.py, docs/README.md |
| 122 | assert _cl_file_count(_TWO_FILES) == 3 |
| 123 | |
| 124 | def test_U09_one_file_returns_one(self) -> None: |
| 125 | assert _cl_file_count(_ONE_FILE) == 1 |
| 126 | |
| 127 | def test_U10_blank_input_returns_zero(self) -> None: |
| 128 | assert _cl_file_count(_BLANK) == 0 |
| 129 | |
| 130 | def test_invalid_json_returns_zero(self) -> None: |
| 131 | assert _cl_file_count(_INVALID_JSON) == 0 |
| 132 | |
| 133 | def test_empty_array_returns_zero(self) -> None: |
| 134 | assert _cl_file_count(_EMPTY_ARRAY) == 0 |
| 135 | |
| 136 | def test_no_double_colon_uses_whole_address_as_file(self) -> None: |
| 137 | blob = json.dumps([{"address": "plain_path.py", "language": "Python"}]) |
| 138 | assert _cl_file_count(blob) == 1 |
| 139 | |
| 140 | def test_three_files(self) -> None: |
| 141 | blob = json.dumps([ |
| 142 | {"address": "a.py::x", "language": "Python"}, |
| 143 | {"address": "b.py::x", "language": "Python"}, |
| 144 | {"address": "c.py::x", "language": "Python"}, |
| 145 | ]) |
| 146 | assert _cl_file_count(blob) == 3 |
| 147 | |
| 148 | |
| 149 | # ───────────────────────────────────────────────────────────────────────────── |
| 150 | # _cl_is_cross_file |
| 151 | # ───────────────────────────────────────────────────────────────────────────── |
| 152 | |
| 153 | |
| 154 | class TestClIsCrossFile: |
| 155 | """U11–U13: members span multiple files → True.""" |
| 156 | |
| 157 | def test_U11_two_files_is_cross_file(self) -> None: |
| 158 | assert _cl_is_cross_file(_TWO_FILES) is True |
| 159 | |
| 160 | def test_U12_one_file_is_not_cross_file(self) -> None: |
| 161 | assert _cl_is_cross_file(_ONE_FILE) is False |
| 162 | |
| 163 | def test_U13_malformed_json_returns_false(self) -> None: |
| 164 | """Parse error → assume same-file; no false positive cross-file alarm.""" |
| 165 | assert _cl_is_cross_file(_INVALID_JSON) is False |
| 166 | |
| 167 | def test_blank_input_returns_false(self) -> None: |
| 168 | assert _cl_is_cross_file(_BLANK) is False |
| 169 | |
| 170 | |
| 171 | # ───────────────────────────────────────────────────────────────────────────── |
| 172 | # _cl_parse_members |
| 173 | # ───────────────────────────────────────────────────────────────────────────── |
| 174 | |
| 175 | |
| 176 | class TestClParseMembers: |
| 177 | """Members JSON is normalised with a synthetic ``file`` key.""" |
| 178 | |
| 179 | def test_file_key_added_from_address(self) -> None: |
| 180 | blob = json.dumps([{"address": "src/a.py::fn", "kind": "function", "language": "Python"}]) |
| 181 | members = _cl_parse_members(blob) |
| 182 | assert len(members) == 1 |
| 183 | assert members[0]["file"] == "src/a.py" |
| 184 | |
| 185 | def test_address_without_double_colon_uses_whole_address_as_file(self) -> None: |
| 186 | blob = json.dumps([{"address": "plain.py", "kind": "variable", "language": "Python"}]) |
| 187 | members = _cl_parse_members(blob) |
| 188 | assert members[0]["file"] == "plain.py" |
| 189 | |
| 190 | def test_missing_address_key_defaults_to_empty(self) -> None: |
| 191 | blob = json.dumps([{"kind": "variable", "language": "Python"}]) |
| 192 | members = _cl_parse_members(blob) |
| 193 | assert members[0]["address"] == "" |
| 194 | assert members[0]["file"] == "" |
| 195 | |
| 196 | def test_invalid_json_returns_empty_list(self) -> None: |
| 197 | assert _cl_parse_members(_INVALID_JSON) == [] |
| 198 | |
| 199 | def test_blank_input_returns_empty_list(self) -> None: |
| 200 | assert _cl_parse_members(_BLANK) == [] |
| 201 | |
| 202 | def test_preserves_kind_and_language(self) -> None: |
| 203 | blob = json.dumps([{"address": "a.py::fn", "kind": "class", "language": "Python"}]) |
| 204 | m = _cl_parse_members(blob)[0] |
| 205 | assert m["kind"] == "class" |
| 206 | assert m["language"] == "Python" |
| 207 | |
| 208 | |
| 209 | # ───────────────────────────────────────────────────────────────────────────── |
| 210 | # _cl_files_breakdown |
| 211 | # ───────────────────────────────────────────────────────────────────────────── |
| 212 | |
| 213 | |
| 214 | class TestClFilesBreakdown: |
| 215 | """Parsed members are grouped by file, sorted by count desc, with pct.""" |
| 216 | |
| 217 | def test_groups_by_file(self) -> None: |
| 218 | members = _cl_parse_members(_TWO_FILES) |
| 219 | breakdown = _cl_files_breakdown(members) |
| 220 | files = [b["file"] for b in breakdown] |
| 221 | assert "src/a.py" in files |
| 222 | assert "src/b.py" in files |
| 223 | |
| 224 | def test_sorted_by_count_descending(self) -> None: |
| 225 | blob = json.dumps([ |
| 226 | {"address": "src/a.py::x", "kind": "function", "language": "Python"}, |
| 227 | {"address": "src/a.py::y", "kind": "function", "language": "Python"}, |
| 228 | {"address": "src/b.py::x", "kind": "function", "language": "Python"}, |
| 229 | ]) |
| 230 | members = _cl_parse_members(blob) |
| 231 | breakdown = _cl_files_breakdown(members) |
| 232 | assert breakdown[0]["file"] == "src/a.py" |
| 233 | assert breakdown[0]["count"] == 2 |
| 234 | assert breakdown[1]["count"] == 1 |
| 235 | |
| 236 | def test_pct_100_for_largest_file(self) -> None: |
| 237 | blob = json.dumps([ |
| 238 | {"address": "src/a.py::x", "kind": "function", "language": "Python"}, |
| 239 | {"address": "src/a.py::y", "kind": "function", "language": "Python"}, |
| 240 | {"address": "src/b.py::x", "kind": "function", "language": "Python"}, |
| 241 | ]) |
| 242 | breakdown = _cl_files_breakdown(_cl_parse_members(blob)) |
| 243 | assert breakdown[0]["pct"] == 100 |
| 244 | |
| 245 | def test_empty_members_returns_empty_list(self) -> None: |
| 246 | assert _cl_files_breakdown([]) == [] |
| 247 | |
| 248 | def test_single_file_100_pct(self) -> None: |
| 249 | members = _cl_parse_members(_ONE_FILE) |
| 250 | breakdown = _cl_files_breakdown(members) |
| 251 | assert len(breakdown) == 1 |
| 252 | assert breakdown[0]["pct"] == 100 |
File History
1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2
feat: add repair-commit wire endpoint (API parity with repa…
Opus 4.8
minor
⚠
1 day ago