test_core_repo.py file-level

at main · View file ↗ · Intel ↗

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