"""Tests for muse/core/repo.py — find_repo_root, require_repo, read_repo_id.""" from __future__ import annotations import json import pathlib import tempfile import pytest from muse.core.errors import ExitCode from muse.core.repo import find_repo_root, read_repo_id, require_repo from muse.core.paths import muse_dir # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_muse_dir(root: pathlib.Path, repo_id: str = "test-repo-id") -> pathlib.Path: muse = muse_dir(root) muse.mkdir(parents=True) (muse / "repo.json").write_text(json.dumps({"repo_id": repo_id})) return muse # --------------------------------------------------------------------------- # find_repo_root # --------------------------------------------------------------------------- class TestFindRepoRoot: def test_returns_none_outside_repo(self, tmp_path: pathlib.Path) -> None: assert find_repo_root(tmp_path) is None def test_finds_muse_in_cwd(self, tmp_path: pathlib.Path) -> None: _make_muse_dir(tmp_path) assert find_repo_root(tmp_path) == tmp_path def test_finds_muse_in_ancestor(self, tmp_path: pathlib.Path) -> None: _make_muse_dir(tmp_path) subdir = tmp_path / "src" / "pkg" subdir.mkdir(parents=True) assert find_repo_root(subdir) == tmp_path # --------------------------------------------------------------------------- # require_repo # --------------------------------------------------------------------------- class TestRequireRepo: def test_exits_outside_repo(self, tmp_path: pathlib.Path) -> None: with pytest.raises(SystemExit) as exc: require_repo(tmp_path) assert exc.value.code == ExitCode.REPO_NOT_FOUND def test_returns_root_inside_repo(self, tmp_path: pathlib.Path) -> None: _make_muse_dir(tmp_path) assert require_repo(tmp_path) == tmp_path # --------------------------------------------------------------------------- # read_repo_id — 4 belt-and-suspenders tests covering every exit path # --------------------------------------------------------------------------- class TestReadRepoId: def test_returns_repo_id_from_valid_file(self, tmp_path: pathlib.Path) -> None: """Happy path: valid repo.json returns the repo_id string.""" _make_muse_dir(tmp_path, repo_id="my-special-repo") assert read_repo_id(tmp_path) == "my-special-repo" def test_missing_repo_json_exits_repo_not_found( self, tmp_path: pathlib.Path ) -> None: """FileNotFoundError → SystemExit(REPO_NOT_FOUND).""" muse_dir(tmp_path).mkdir() # repo.json intentionally absent. with pytest.raises(SystemExit) as exc: read_repo_id(tmp_path) assert exc.value.code == ExitCode.REPO_NOT_FOUND def test_malformed_json_exits_internal_error( self, tmp_path: pathlib.Path ) -> None: """JSONDecodeError → SystemExit(INTERNAL_ERROR).""" muse = muse_dir(tmp_path) muse.mkdir() (muse / "repo.json").write_text("this is not json{{{") with pytest.raises(SystemExit) as exc: read_repo_id(tmp_path) assert exc.value.code == ExitCode.INTERNAL_ERROR def test_missing_key_exits_internal_error(self, tmp_path: pathlib.Path) -> None: """KeyError (no 'repo_id' key) → SystemExit(INTERNAL_ERROR).""" muse = muse_dir(tmp_path) muse.mkdir() (muse / "repo.json").write_text(json.dumps({"wrong_key": "value"})) with pytest.raises(SystemExit) as exc: read_repo_id(tmp_path) assert exc.value.code == ExitCode.INTERNAL_ERROR # --------------------------------------------------------------------------- # parse_date_arg — 6 belt-and-suspenders tests # --------------------------------------------------------------------------- class TestParseDateArg: """parse_date_arg handles YYYY-MM-DD and YYYY-MM-DDTHH:MM:SS; rejects bad input.""" def test_parses_date_only(self) -> None: from muse.core.repo import parse_date_arg import datetime as dt result = parse_date_arg("2026-03-25", "--since") assert result == dt.datetime(2026, 3, 25, tzinfo=dt.timezone.utc) def test_parses_full_datetime(self) -> None: from muse.core.repo import parse_date_arg import datetime as dt result = parse_date_arg("2026-03-25T14:30:00", "--until") assert result == dt.datetime(2026, 3, 25, 14, 30, 0, tzinfo=dt.timezone.utc) def test_invalid_value_exits_1(self) -> None: from muse.core.repo import parse_date_arg with pytest.raises(SystemExit) as exc: parse_date_arg("not-a-date", "--since") assert exc.value.code == 1 def test_since_flag_name_in_error_message( self, capsys: pytest.CaptureFixture[str] ) -> None: from muse.core.repo import parse_date_arg with pytest.raises(SystemExit): parse_date_arg("bad", "--since") err = capsys.readouterr().err assert "--since" in err def test_until_flag_name_in_error_message( self, capsys: pytest.CaptureFixture[str] ) -> None: from muse.core.repo import parse_date_arg with pytest.raises(SystemExit): parse_date_arg("bad", "--until") err = capsys.readouterr().err assert "--until" in err def test_result_is_utc_aware(self) -> None: from muse.core.repo import parse_date_arg import datetime as dt result = parse_date_arg("2026-01-01", "--since") assert result.tzinfo == dt.timezone.utc