gabriel / musehub public
test_clones_unit.py python
252 lines 11.3 KB
Raw
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days 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 10 commits
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 30 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 36 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago