"""Tier 1 — Unit tests for clone browser helper functions (issue #17). Tests are strictly pure: no DB, no HTTP, no I/O. Every test exercises a single helper from ``musehub.api.routes.musehub.ui_intel`` in isolation so failures point exactly to the broken function. Helpers under test: _cl_tier_class — tier name → CSS modifier _cl_language_set — members_json → sorted distinct languages _cl_file_count — members_json → distinct file count _cl_is_cross_file — members_json → bool (files > 1) _cl_parse_members — members_json → normalised list with ``file`` key _cl_files_breakdown — normalised members → grouped-by-file breakdown Coverage target: 13 primary cases + edge cases for every parse-error path. """ from __future__ import annotations import json import pytest from musehub.api.routes.musehub.ui_intel import ( _cl_file_count, _cl_files_breakdown, _cl_is_cross_file, _cl_language_set, _cl_parse_members, _cl_tier_class, ) # ───────────────────────────────────────────────────────────────────────────── # Fixtures # ───────────────────────────────────────────────────────────────────────────── _ONE_FILE = json.dumps( [ {"address": "src/a.py::foo", "kind": "function", "language": "Python"}, {"address": "src/a.py::bar", "kind": "function", "language": "Python"}, ] ) _TWO_FILES = json.dumps( [ {"address": "src/a.py::foo", "kind": "function", "language": "Python"}, {"address": "src/b.py::foo", "kind": "function", "language": "Python"}, {"address": "docs/README.md::heading", "kind": "section", "language": "Markdown"}, ] ) _EMPTY_ARRAY = "[]" _INVALID_JSON = "not json" _BLANK = "" # ───────────────────────────────────────────────────────────────────────────── # _cl_tier_class # ───────────────────────────────────────────────────────────────────────────── class TestClTierClass: """U01–U03: tier name maps to correct CSS modifier class.""" def test_U01_exact_returns_exact_class(self) -> None: assert _cl_tier_class("exact") == "cl-badge--exact" def test_U02_near_returns_near_class(self) -> None: assert _cl_tier_class("near") == "cl-badge--near" def test_U03_unknown_tier_returns_near_as_safe_default(self) -> None: """Unknown tier must never raise — falls back to near (less alarming).""" assert _cl_tier_class("unknown") == "cl-badge--near" assert _cl_tier_class("") == "cl-badge--near" # ───────────────────────────────────────────────────────────────────────────── # _cl_language_set # ───────────────────────────────────────────────────────────────────────────── class TestClLanguageSet: """U04–U07: members_json → sorted distinct language list.""" def test_U04_deduplicates_same_language(self) -> None: blob = json.dumps([ {"language": "Python"}, {"language": "Python"}, ]) assert _cl_language_set(blob) == ["Python"] def test_U05_multiple_languages_sorted(self) -> None: blob = json.dumps([ {"language": "Python"}, {"language": "Markdown"}, ]) assert _cl_language_set(blob) == ["Markdown", "Python"] def test_U06_invalid_json_returns_placeholder(self) -> None: assert _cl_language_set(_INVALID_JSON) == ["—"] def test_U07_empty_array_returns_placeholder(self) -> None: assert _cl_language_set(_EMPTY_ARRAY) == ["—"] def test_blank_string_returns_placeholder(self) -> None: assert _cl_language_set(_BLANK) == ["—"] def test_members_missing_language_key_skipped(self) -> None: blob = json.dumps([{"address": "a.py::fn", "kind": "function"}]) assert _cl_language_set(blob) == ["—"] # ───────────────────────────────────────────────────────────────────────────── # _cl_file_count # ───────────────────────────────────────────────────────────────────────────── class TestClFileCount: """U08–U10: members_json → count of distinct source files.""" def test_U08_cross_file_fixture_returns_correct_count(self) -> None: # _TWO_FILES contains 3 distinct files: src/a.py, src/b.py, docs/README.md assert _cl_file_count(_TWO_FILES) == 3 def test_U09_one_file_returns_one(self) -> None: assert _cl_file_count(_ONE_FILE) == 1 def test_U10_blank_input_returns_zero(self) -> None: assert _cl_file_count(_BLANK) == 0 def test_invalid_json_returns_zero(self) -> None: assert _cl_file_count(_INVALID_JSON) == 0 def test_empty_array_returns_zero(self) -> None: assert _cl_file_count(_EMPTY_ARRAY) == 0 def test_no_double_colon_uses_whole_address_as_file(self) -> None: blob = json.dumps([{"address": "plain_path.py", "language": "Python"}]) assert _cl_file_count(blob) == 1 def test_three_files(self) -> None: blob = json.dumps([ {"address": "a.py::x", "language": "Python"}, {"address": "b.py::x", "language": "Python"}, {"address": "c.py::x", "language": "Python"}, ]) assert _cl_file_count(blob) == 3 # ───────────────────────────────────────────────────────────────────────────── # _cl_is_cross_file # ───────────────────────────────────────────────────────────────────────────── class TestClIsCrossFile: """U11–U13: members span multiple files → True.""" def test_U11_two_files_is_cross_file(self) -> None: assert _cl_is_cross_file(_TWO_FILES) is True def test_U12_one_file_is_not_cross_file(self) -> None: assert _cl_is_cross_file(_ONE_FILE) is False def test_U13_malformed_json_returns_false(self) -> None: """Parse error → assume same-file; no false positive cross-file alarm.""" assert _cl_is_cross_file(_INVALID_JSON) is False def test_blank_input_returns_false(self) -> None: assert _cl_is_cross_file(_BLANK) is False # ───────────────────────────────────────────────────────────────────────────── # _cl_parse_members # ───────────────────────────────────────────────────────────────────────────── class TestClParseMembers: """Members JSON is normalised with a synthetic ``file`` key.""" def test_file_key_added_from_address(self) -> None: blob = json.dumps([{"address": "src/a.py::fn", "kind": "function", "language": "Python"}]) members = _cl_parse_members(blob) assert len(members) == 1 assert members[0]["file"] == "src/a.py" def test_address_without_double_colon_uses_whole_address_as_file(self) -> None: blob = json.dumps([{"address": "plain.py", "kind": "variable", "language": "Python"}]) members = _cl_parse_members(blob) assert members[0]["file"] == "plain.py" def test_missing_address_key_defaults_to_empty(self) -> None: blob = json.dumps([{"kind": "variable", "language": "Python"}]) members = _cl_parse_members(blob) assert members[0]["address"] == "" assert members[0]["file"] == "" def test_invalid_json_returns_empty_list(self) -> None: assert _cl_parse_members(_INVALID_JSON) == [] def test_blank_input_returns_empty_list(self) -> None: assert _cl_parse_members(_BLANK) == [] def test_preserves_kind_and_language(self) -> None: blob = json.dumps([{"address": "a.py::fn", "kind": "class", "language": "Python"}]) m = _cl_parse_members(blob)[0] assert m["kind"] == "class" assert m["language"] == "Python" # ───────────────────────────────────────────────────────────────────────────── # _cl_files_breakdown # ───────────────────────────────────────────────────────────────────────────── class TestClFilesBreakdown: """Parsed members are grouped by file, sorted by count desc, with pct.""" def test_groups_by_file(self) -> None: members = _cl_parse_members(_TWO_FILES) breakdown = _cl_files_breakdown(members) files = [b["file"] for b in breakdown] assert "src/a.py" in files assert "src/b.py" in files def test_sorted_by_count_descending(self) -> None: blob = json.dumps([ {"address": "src/a.py::x", "kind": "function", "language": "Python"}, {"address": "src/a.py::y", "kind": "function", "language": "Python"}, {"address": "src/b.py::x", "kind": "function", "language": "Python"}, ]) members = _cl_parse_members(blob) breakdown = _cl_files_breakdown(members) assert breakdown[0]["file"] == "src/a.py" assert breakdown[0]["count"] == 2 assert breakdown[1]["count"] == 1 def test_pct_100_for_largest_file(self) -> None: blob = json.dumps([ {"address": "src/a.py::x", "kind": "function", "language": "Python"}, {"address": "src/a.py::y", "kind": "function", "language": "Python"}, {"address": "src/b.py::x", "kind": "function", "language": "Python"}, ]) breakdown = _cl_files_breakdown(_cl_parse_members(blob)) assert breakdown[0]["pct"] == 100 def test_empty_members_returns_empty_list(self) -> None: assert _cl_files_breakdown([]) == [] def test_single_file_100_pct(self) -> None: members = _cl_parse_members(_ONE_FILE) breakdown = _cl_files_breakdown(members) assert len(breakdown) == 1 assert breakdown[0]["pct"] == 100