cgcardona / muse public
test_repo.py python
170 lines 6.0 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """Tests for ``maestro.muse_cli._repo`` and public ``maestro.muse_cli.repo``.
2
3 Covers every acceptance criterion:
4
5 - Returns current dir if ``.muse/`` is present
6 - Traverses up and finds a parent ``.muse/``
7 - Returns ``None`` (never raises) when no ``.muse/`` ancestor exists
8 - ``MUSE_REPO_ROOT`` env-var takes precedence over traversal
9 - ``require_repo_root()`` exits 2 with the standardised git-style error
10 message when no repo is found
11 - Public ``repo.py`` module re-exports ``find_repo_root`` and
12 ``require_repo_root`` identically to the private ``_repo`` module
13 - ``MuseNotARepoError`` alias exists in ``errors.py``
14
15 All tests use ``tmp_path`` and ``monkeypatch`` for isolation.
16 """
17 from __future__ import annotations
18
19 import os
20 import pathlib
21
22 import pytest
23 from typer.testing import CliRunner
24
25 from maestro.muse_cli._repo import find_repo_root, require_repo, require_repo_root
26 from maestro.muse_cli.app import cli
27 from maestro.muse_cli.errors import ExitCode, MuseNotARepoError, RepoNotFoundError
28
29 runner = CliRunner()
30
31
32 # ---------------------------------------------------------------------------
33 # find_repo_root()
34 # ---------------------------------------------------------------------------
35
36
37 def test_find_repo_root_current_dir(tmp_path: pathlib.Path) -> None:
38 """Returns current directory when ``.muse/`` is present there."""
39 (tmp_path / ".muse").mkdir()
40 root = find_repo_root(tmp_path)
41 assert root == tmp_path
42
43
44 def test_find_repo_root_parent_dir(tmp_path: pathlib.Path) -> None:
45 """Traverses up and finds a ``.muse/`` in a parent directory."""
46 (tmp_path / ".muse").mkdir()
47 nested = tmp_path / "project" / "subdir"
48 nested.mkdir(parents=True)
49
50 root = find_repo_root(nested)
51 assert root == tmp_path
52
53
54 def test_find_repo_root_returns_none_outside_repo(tmp_path: pathlib.Path) -> None:
55 """Returns ``None`` (not an exception) when no ``.muse/`` ancestor exists."""
56 # tmp_path has no .muse/ and is an isolated temp dir.
57 root = find_repo_root(tmp_path)
58 assert root is None
59
60
61 def test_find_repo_root_uses_cwd_when_no_start(
62 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
63 ) -> None:
64 """With no ``start`` argument, uses ``Path.cwd()`` as the start."""
65 (tmp_path / ".muse").mkdir()
66 monkeypatch.chdir(tmp_path)
67 root = find_repo_root()
68 assert root == tmp_path
69
70
71 def test_find_repo_root_env_var_override(
72 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
73 ) -> None:
74 """``MUSE_REPO_ROOT`` env var takes precedence over directory traversal."""
75 override_dir = tmp_path / "override"
76 override_dir.mkdir()
77 (override_dir / ".muse").mkdir()
78
79 monkeypatch.setenv("MUSE_REPO_ROOT", str(override_dir))
80
81 # Even if there's a different .muse/ higher up, the override wins.
82 root = find_repo_root(tmp_path)
83 assert root == override_dir
84
85
86 def test_find_repo_root_env_var_override_invalid_returns_none(
87 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
88 ) -> None:
89 """``MUSE_REPO_ROOT`` pointing to a dir without ``.muse/`` returns ``None``."""
90 no_muse_dir = tmp_path / "no_muse"
91 no_muse_dir.mkdir()
92 monkeypatch.setenv("MUSE_REPO_ROOT", str(no_muse_dir))
93
94 root = find_repo_root()
95 assert root is None
96
97
98 def test_find_repo_root_stops_at_filesystem_root(tmp_path: pathlib.Path) -> None:
99 """Traversal stops at filesystem root and returns ``None``, never loops."""
100 # Use a temp dir that has no .muse/ in any ancestor up to its root.
101 deeply_nested = tmp_path / "a" / "b" / "c"
102 deeply_nested.mkdir(parents=True)
103 root = find_repo_root(deeply_nested)
104 assert root is None
105
106
107 # ---------------------------------------------------------------------------
108 # require_repo()
109 # ---------------------------------------------------------------------------
110
111
112 def test_require_repo_exits_2_when_no_muse(tmp_path: pathlib.Path) -> None:
113 """``require_repo()`` from the CLI exits 2 when outside a Muse repo."""
114 prev = os.getcwd()
115 try:
116 os.chdir(tmp_path)
117 result = runner.invoke(cli, ["status"])
118 assert result.exit_code == int(ExitCode.REPO_NOT_FOUND)
119 finally:
120 os.chdir(prev)
121
122
123 def test_require_repo_error_message_matches_standard(tmp_path: pathlib.Path) -> None:
124 """The error message matches the git-style format specified."""
125 prev = os.getcwd()
126 try:
127 os.chdir(tmp_path)
128 result = runner.invoke(cli, ["status"])
129 assert "fatal: not a muse repository" in result.output
130 assert "muse init" in result.output
131 finally:
132 os.chdir(prev)
133
134
135 def test_require_repo_returns_root_when_found(tmp_path: pathlib.Path) -> None:
136 """``require_repo()`` returns the resolved root path when ``.muse/`` exists."""
137 (tmp_path / ".muse").mkdir()
138 root = require_repo(tmp_path)
139 assert root == tmp_path
140
141
142 # ---------------------------------------------------------------------------
143 # Issue #46 additions — aliases and public module
144 # ---------------------------------------------------------------------------
145
146
147 def test_require_repo_root_alias_is_identical(tmp_path: pathlib.Path) -> None:
148 """``require_repo_root`` is the same callable as ``require_repo``."""
149 assert require_repo_root is require_repo
150
151
152 def test_muse_not_a_repo_error_alias(tmp_path: pathlib.Path) -> None:
153 """``MuseNotARepoError`` is the canonical alias for ``RepoNotFoundError``."""
154 assert MuseNotARepoError is RepoNotFoundError
155
156
157 def test_public_repo_module_exports_find_repo_root(tmp_path: pathlib.Path) -> None:
158 """Public ``maestro.muse_cli.repo`` re-exports ``find_repo_root``."""
159 from maestro.muse_cli.repo import find_repo_root as public_fn
160
161 (tmp_path / ".muse").mkdir()
162 assert public_fn(tmp_path) == tmp_path
163
164
165 def test_public_repo_module_exports_require_repo_root(tmp_path: pathlib.Path) -> None:
166 """Public ``maestro.muse_cli.repo`` re-exports ``require_repo_root``."""
167 from maestro.muse_cli.repo import require_repo_root as public_fn
168 from maestro.muse_cli._repo import require_repo_root as private_fn
169
170 assert public_fn is private_fn