test_core_refs.py
python
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9
docs: expand cache plan with all seven testing tiers and do…
Sonnet 4.6
131 days ago
| 1 | """Tests for muse/core/refs.py — canonical ref-file reading primitives. |
| 2 | |
| 3 | Coverage |
| 4 | -------- |
| 5 | read_ref |
| 6 | - returns commit ID for a well-formed ref file |
| 7 | - returns None for a missing file |
| 8 | - returns None for an empty file |
| 9 | - returns None for a whitespace-only file |
| 10 | - strips leading/trailing whitespace and newlines |
| 11 | - returns None on PermissionError (graceful degradation) |
| 12 | |
| 13 | iter_branch_refs |
| 14 | - empty heads dir → yields nothing |
| 15 | - missing heads dir → yields nothing |
| 16 | - single branch → yields (branch_name, commit_id) |
| 17 | - multiple branches → yields all (branch_name, commit_id) pairs |
| 18 | - skips symlinks |
| 19 | - skips non-file entries (subdirectories) |
| 20 | - skips empty ref files |
| 21 | - each yielded commit_id is non-empty string |
| 22 | - lazy — yields incrementally (returns an iterator, not a list) |
| 23 | - branch name is the filename, not the full path |
| 24 | """ |
| 25 | |
| 26 | from __future__ import annotations |
| 27 | |
| 28 | import json |
| 29 | import pathlib |
| 30 | |
| 31 | import pytest |
| 32 | |
| 33 | from muse.core.refs import iter_branch_refs, read_ref |
| 34 | from muse.core._types import long_id |
| 35 | |
| 36 | |
| 37 | # --------------------------------------------------------------------------- |
| 38 | # Helpers |
| 39 | # --------------------------------------------------------------------------- |
| 40 | |
| 41 | |
| 42 | def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 43 | muse = tmp_path / ".muse" |
| 44 | for d in ("objects", "commits", "snapshots", "refs/heads"): |
| 45 | (muse / d).mkdir(parents=True, exist_ok=True) |
| 46 | (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"})) |
| 47 | (muse / "HEAD").write_text("ref: refs/heads/main\n") |
| 48 | return tmp_path |
| 49 | |
| 50 | |
| 51 | def _write_branch_ref(repo: pathlib.Path, branch: str, commit_id: str) -> pathlib.Path: |
| 52 | """Write a branch ref file; return the path.""" |
| 53 | ref_path = repo / ".muse" / "refs" / "heads" / branch |
| 54 | ref_path.parent.mkdir(parents=True, exist_ok=True) |
| 55 | ref_path.write_text(commit_id + "\n", encoding="utf-8") |
| 56 | return ref_path |
| 57 | |
| 58 | |
| 59 | _FAKE_CID = long_id("ab" * 32) |
| 60 | _FAKE_CID2 = long_id("cd" * 32) |
| 61 | _FAKE_CID3 = long_id("ef" * 32) |
| 62 | |
| 63 | |
| 64 | # --------------------------------------------------------------------------- |
| 65 | # read_ref |
| 66 | # --------------------------------------------------------------------------- |
| 67 | |
| 68 | |
| 69 | class TestReadRef: |
| 70 | def test_returns_commit_id_for_well_formed_ref(self, tmp_path: pathlib.Path) -> None: |
| 71 | ref = tmp_path / "myref" |
| 72 | ref.write_text(_FAKE_CID + "\n", encoding="utf-8") |
| 73 | assert read_ref(ref) == _FAKE_CID |
| 74 | |
| 75 | def test_returns_none_for_missing_file(self, tmp_path: pathlib.Path) -> None: |
| 76 | assert read_ref(tmp_path / "nonexistent") is None |
| 77 | |
| 78 | def test_returns_none_for_empty_file(self, tmp_path: pathlib.Path) -> None: |
| 79 | ref = tmp_path / "empty" |
| 80 | ref.write_text("", encoding="utf-8") |
| 81 | assert read_ref(ref) is None |
| 82 | |
| 83 | def test_returns_none_for_whitespace_only(self, tmp_path: pathlib.Path) -> None: |
| 84 | ref = tmp_path / "ws" |
| 85 | ref.write_text(" \n \n", encoding="utf-8") |
| 86 | assert read_ref(ref) is None |
| 87 | |
| 88 | def test_strips_whitespace_and_newlines(self, tmp_path: pathlib.Path) -> None: |
| 89 | ref = tmp_path / "ref" |
| 90 | ref.write_text(f" {_FAKE_CID} \n", encoding="utf-8") |
| 91 | assert read_ref(ref) == _FAKE_CID |
| 92 | |
| 93 | def test_strips_trailing_newline_only(self, tmp_path: pathlib.Path) -> None: |
| 94 | ref = tmp_path / "ref" |
| 95 | ref.write_text(_FAKE_CID + "\n", encoding="utf-8") |
| 96 | assert read_ref(ref) == _FAKE_CID |
| 97 | |
| 98 | def test_returns_none_on_permission_error(self, tmp_path: pathlib.Path) -> None: |
| 99 | ref = tmp_path / "locked" |
| 100 | ref.write_text(_FAKE_CID, encoding="utf-8") |
| 101 | ref.chmod(0o000) |
| 102 | try: |
| 103 | result = read_ref(ref) |
| 104 | assert result is None |
| 105 | finally: |
| 106 | ref.chmod(0o644) |
| 107 | |
| 108 | |
| 109 | # --------------------------------------------------------------------------- |
| 110 | # iter_branch_refs |
| 111 | # --------------------------------------------------------------------------- |
| 112 | |
| 113 | |
| 114 | class TestIterBranchRefs: |
| 115 | def test_missing_heads_dir_yields_nothing(self, tmp_path: pathlib.Path) -> None: |
| 116 | repo = tmp_path # no .muse directory at all |
| 117 | result = list(iter_branch_refs(repo)) |
| 118 | assert result == [] |
| 119 | |
| 120 | def test_empty_heads_dir_yields_nothing(self, tmp_path: pathlib.Path) -> None: |
| 121 | repo = _make_repo(tmp_path) |
| 122 | result = list(iter_branch_refs(repo)) |
| 123 | assert result == [] |
| 124 | |
| 125 | def test_single_branch_yields_name_and_commit_id(self, tmp_path: pathlib.Path) -> None: |
| 126 | repo = _make_repo(tmp_path) |
| 127 | _write_branch_ref(repo, "main", _FAKE_CID) |
| 128 | result = list(iter_branch_refs(repo)) |
| 129 | assert len(result) == 1 |
| 130 | name, cid = result[0] |
| 131 | assert name == "main" |
| 132 | assert cid == _FAKE_CID |
| 133 | |
| 134 | def test_multiple_branches_yields_all(self, tmp_path: pathlib.Path) -> None: |
| 135 | repo = _make_repo(tmp_path) |
| 136 | _write_branch_ref(repo, "main", _FAKE_CID) |
| 137 | _write_branch_ref(repo, "dev", _FAKE_CID2) |
| 138 | _write_branch_ref(repo, "feat/x", _FAKE_CID3) |
| 139 | result = dict(iter_branch_refs(repo)) |
| 140 | assert result == {"main": _FAKE_CID, "dev": _FAKE_CID2, "feat/x": _FAKE_CID3} |
| 141 | |
| 142 | def test_skips_symlinks(self, tmp_path: pathlib.Path) -> None: |
| 143 | repo = _make_repo(tmp_path) |
| 144 | _write_branch_ref(repo, "main", _FAKE_CID) |
| 145 | heads_dir = repo / ".muse" / "refs" / "heads" |
| 146 | symlink = heads_dir / "alias" |
| 147 | symlink.symlink_to(heads_dir / "main") |
| 148 | names = [name for name, _ in iter_branch_refs(repo)] |
| 149 | assert "alias" not in names |
| 150 | assert "main" in names |
| 151 | |
| 152 | def test_skips_empty_subdirectories(self, tmp_path: pathlib.Path) -> None: |
| 153 | """Empty subdirectories (no ref files) yield nothing for that subtree.""" |
| 154 | repo = _make_repo(tmp_path) |
| 155 | _write_branch_ref(repo, "main", _FAKE_CID) |
| 156 | subdir = repo / ".muse" / "refs" / "heads" / "namespace" |
| 157 | subdir.mkdir() |
| 158 | names = [name for name, _ in iter_branch_refs(repo)] |
| 159 | assert "namespace" not in names |
| 160 | assert "main" in names |
| 161 | |
| 162 | def test_hierarchical_branch_name_uses_posix_slash(self, tmp_path: pathlib.Path) -> None: |
| 163 | """Branch names with slashes (task/foo) are yielded as relative POSIX paths.""" |
| 164 | repo = _make_repo(tmp_path) |
| 165 | _write_branch_ref(repo, "task/my-feature", _FAKE_CID) |
| 166 | result = list(iter_branch_refs(repo)) |
| 167 | assert len(result) == 1 |
| 168 | name, cid = result[0] |
| 169 | assert name == "task/my-feature" |
| 170 | assert cid == _FAKE_CID |
| 171 | |
| 172 | def test_skips_empty_ref_files(self, tmp_path: pathlib.Path) -> None: |
| 173 | repo = _make_repo(tmp_path) |
| 174 | _write_branch_ref(repo, "main", _FAKE_CID) |
| 175 | (repo / ".muse" / "refs" / "heads" / "empty-branch").write_text("") |
| 176 | result = list(iter_branch_refs(repo)) |
| 177 | names = [name for name, _ in result] |
| 178 | assert "empty-branch" not in names |
| 179 | assert "main" in names |
| 180 | |
| 181 | def test_yields_non_empty_commit_ids(self, tmp_path: pathlib.Path) -> None: |
| 182 | repo = _make_repo(tmp_path) |
| 183 | _write_branch_ref(repo, "main", _FAKE_CID) |
| 184 | for name, cid in iter_branch_refs(repo): |
| 185 | assert cid # non-empty |
| 186 | assert isinstance(cid, str) |
| 187 | |
| 188 | def test_returns_iterator_not_list(self, tmp_path: pathlib.Path) -> None: |
| 189 | """iter_branch_refs must return an iterator (lazy), not a pre-built list.""" |
| 190 | import collections.abc |
| 191 | repo = _make_repo(tmp_path) |
| 192 | result = iter_branch_refs(repo) |
| 193 | assert isinstance(result, collections.abc.Iterator) |
| 194 | |
| 195 | def test_branch_name_is_relative_posix_path(self, tmp_path: pathlib.Path) -> None: |
| 196 | """Branch name is relative to heads_dir — not an absolute filesystem path.""" |
| 197 | repo = _make_repo(tmp_path) |
| 198 | _write_branch_ref(repo, "simple", _FAKE_CID) |
| 199 | result = list(iter_branch_refs(repo)) |
| 200 | assert len(result) == 1 |
| 201 | name, _ = result[0] |
| 202 | assert name == "simple" |
| 203 | assert str(tmp_path) not in name |
| 204 | |
| 205 | def test_uses_generator_not_list_comprehension(self) -> None: |
| 206 | """iter_branch_refs must use a generator (structural check).""" |
| 207 | import inspect |
| 208 | from muse.core import refs as refs_module |
| 209 | source = inspect.getsource(refs_module.iter_branch_refs) |
| 210 | assert "yield" in source, "iter_branch_refs must use yield (generator)" |
File History
2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9
docs: expand cache plan with all seven testing tiers and do…
Sonnet 4.6
131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c
docs: docstring sprint for-each-ref→hotspots — idiomatic ru…
Sonnet 4.6
patch
137 days ago