gabriel / muse public

test_core_repo.py file-level

at sha256:8 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:4 Merge branch 'dev' into main · gabriel · Jun 17, 2026
1 """Tests for muse/core/repo.py β€” find_repo_root, require_repo, read_repo_id."""
2
3 from __future__ import annotations
4
5 import json
6 import pathlib
7 import tempfile
8
9 import pytest
10
11 from muse.core.errors import ExitCode
12 from muse.core.repo import find_repo_root, read_repo_id, require_repo
13 from muse.core.paths import muse_dir
14
15
16 # ---------------------------------------------------------------------------
17 # Helpers
18 # ---------------------------------------------------------------------------
19
20
21 def _make_muse_dir(root: pathlib.Path, repo_id: str = "test-repo-id") -> pathlib.Path:
22 muse = muse_dir(root)
23 muse.mkdir(parents=True)
24 (muse / "repo.json").write_text(json.dumps({"repo_id": repo_id}))
25 return muse
26
27
28 # ---------------------------------------------------------------------------
29 # find_repo_root
30 # ---------------------------------------------------------------------------
31
32
33 class TestFindRepoRoot:
34 def test_returns_none_outside_repo(self, tmp_path: pathlib.Path) -> None:
35 assert find_repo_root(tmp_path) is None
36
37 def test_finds_muse_in_cwd(self, tmp_path: pathlib.Path) -> None:
38 _make_muse_dir(tmp_path)
39 assert find_repo_root(tmp_path) == tmp_path
40
41 def test_finds_muse_in_ancestor(self, tmp_path: pathlib.Path) -> None:
42 _make_muse_dir(tmp_path)
43 subdir = tmp_path / "src" / "pkg"
44 subdir.mkdir(parents=True)
45 assert find_repo_root(subdir) == tmp_path
46
47
48 # ---------------------------------------------------------------------------
49 # require_repo
50 # ---------------------------------------------------------------------------
51
52
53 class TestRequireRepo:
54 def test_exits_outside_repo(self, tmp_path: pathlib.Path) -> None:
55 with pytest.raises(SystemExit) as exc:
56 require_repo(tmp_path)
57 assert exc.value.code == ExitCode.REPO_NOT_FOUND
58
59 def test_returns_root_inside_repo(self, tmp_path: pathlib.Path) -> None:
60 _make_muse_dir(tmp_path)
61 assert require_repo(tmp_path) == tmp_path
62
63
64 # ---------------------------------------------------------------------------
65 # read_repo_id β€” 4 belt-and-suspenders tests covering every exit path
66 # ---------------------------------------------------------------------------
67
68
69 class TestReadRepoId:
70 def test_returns_repo_id_from_valid_file(self, tmp_path: pathlib.Path) -> None:
71 """Happy path: valid repo.json returns the repo_id string."""
72 _make_muse_dir(tmp_path, repo_id="my-special-repo")
73 assert read_repo_id(tmp_path) == "my-special-repo"
74
75 def test_missing_repo_json_exits_repo_not_found(
76 self, tmp_path: pathlib.Path
77 ) -> None:
78 """FileNotFoundError β†’ SystemExit(REPO_NOT_FOUND)."""
79 muse_dir(tmp_path).mkdir()
80 # repo.json intentionally absent.
81 with pytest.raises(SystemExit) as exc:
82 read_repo_id(tmp_path)
83 assert exc.value.code == ExitCode.REPO_NOT_FOUND
84
85 def test_malformed_json_exits_internal_error(
86 self, tmp_path: pathlib.Path
87 ) -> None:
88 """JSONDecodeError β†’ SystemExit(INTERNAL_ERROR)."""
89 muse = muse_dir(tmp_path)
90 muse.mkdir()
91 (muse / "repo.json").write_text("this is not json{{{")
92 with pytest.raises(SystemExit) as exc:
93 read_repo_id(tmp_path)
94 assert exc.value.code == ExitCode.INTERNAL_ERROR
95
96 def test_missing_key_exits_internal_error(self, tmp_path: pathlib.Path) -> None:
97 """KeyError (no 'repo_id' key) β†’ SystemExit(INTERNAL_ERROR)."""
98 muse = muse_dir(tmp_path)
99 muse.mkdir()
100 (muse / "repo.json").write_text(json.dumps({"wrong_key": "value"}))
101 with pytest.raises(SystemExit) as exc:
102 read_repo_id(tmp_path)
103 assert exc.value.code == ExitCode.INTERNAL_ERROR
104
105
106 # ---------------------------------------------------------------------------
107 # parse_date_arg β€” 6 belt-and-suspenders tests
108 # ---------------------------------------------------------------------------
109
110
111 class TestParseDateArg:
112 """parse_date_arg handles YYYY-MM-DD and YYYY-MM-DDTHH:MM:SS; rejects bad input."""
113
114 def test_parses_date_only(self) -> None:
115 from muse.core.repo import parse_date_arg
116 import datetime as dt
117 result = parse_date_arg("2026-03-25", "--since")
118 assert result == dt.datetime(2026, 3, 25, tzinfo=dt.timezone.utc)
119
120 def test_parses_full_datetime(self) -> None:
121 from muse.core.repo import parse_date_arg
122 import datetime as dt
123 result = parse_date_arg("2026-03-25T14:30:00", "--until")
124 assert result == dt.datetime(2026, 3, 25, 14, 30, 0, tzinfo=dt.timezone.utc)
125
126 def test_invalid_value_exits_1(self) -> None:
127 from muse.core.repo import parse_date_arg
128 with pytest.raises(SystemExit) as exc:
129 parse_date_arg("not-a-date", "--since")
130 assert exc.value.code == 1
131
132 def test_since_flag_name_in_error_message(
133 self, capsys: pytest.CaptureFixture[str]
134 ) -> None:
135 from muse.core.repo import parse_date_arg
136 with pytest.raises(SystemExit):
137 parse_date_arg("bad", "--since")
138 err = capsys.readouterr().err
139 assert "--since" in err
140
141 def test_until_flag_name_in_error_message(
142 self, capsys: pytest.CaptureFixture[str]
143 ) -> None:
144 from muse.core.repo import parse_date_arg
145 with pytest.raises(SystemExit):
146 parse_date_arg("bad", "--until")
147 err = capsys.readouterr().err
148 assert "--until" in err
149
150 def test_result_is_utc_aware(self) -> None:
151 from muse.core.repo import parse_date_arg
152 import datetime as dt
153 result = parse_date_arg("2026-01-01", "--since")
154 assert result.tzinfo == dt.timezone.utc