cgcardona / muse public
test_muse_symbolic_ref.py python
230 lines 8.2 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """Tests for ``muse symbolic-ref`` — read or write a symbolic ref.
2
3 Verifies:
4 - read_symbolic_ref: returns SymbolicRefResult for a valid symbolic ref.
5 - read_symbolic_ref: returns None for a non-existent file.
6 - read_symbolic_ref: returns None when content is a bare SHA (not symbolic).
7 - read_symbolic_ref.short: last path component only.
8 - write_symbolic_ref: creates the file with the given target.
9 - write_symbolic_ref: raises ValueError for a non-refs/ target.
10 - write_symbolic_ref: creates intermediate directories for nested refs.
11 - delete_symbolic_ref: removes the file; returns True.
12 - delete_symbolic_ref: returns False for a non-existent file.
13 - CLI read path: ``muse symbolic-ref HEAD`` prints full ref.
14 - CLI --short: prints just the branch name.
15 - CLI write path: ``muse symbolic-ref HEAD refs/heads/x`` updates the file.
16 - CLI write rejects non-refs/ target.
17 - CLI --delete: removes the file; exits 0.
18 - CLI --delete absent file: exits USER_ERROR.
19 - CLI -q suppresses error output when ref is not symbolic.
20 - Boundary seal (AST): ``from __future__ import annotations`` present.
21 """
22 from __future__ import annotations
23
24 import ast
25 import pathlib
26
27 import pytest
28 from typer.testing import CliRunner
29
30 from maestro.muse_cli.app import cli
31 from maestro.muse_cli.commands.symbolic_ref import (
32 SymbolicRefResult,
33 delete_symbolic_ref,
34 read_symbolic_ref,
35 write_symbolic_ref,
36 )
37 from maestro.muse_cli.errors import ExitCode
38
39 runner = CliRunner()
40
41
42 # ---------------------------------------------------------------------------
43 # Fixtures
44 # ---------------------------------------------------------------------------
45
46
47 @pytest.fixture
48 def muse_dir(tmp_path: pathlib.Path) -> pathlib.Path:
49 """Create a minimal .muse directory."""
50 d = tmp_path / ".muse"
51 d.mkdir()
52 return d
53
54
55 @pytest.fixture
56 def repo_root_with_head(tmp_path: pathlib.Path) -> pathlib.Path:
57 """Full repo directory with .muse/HEAD pointing at refs/heads/main."""
58 muse = tmp_path / ".muse"
59 muse.mkdir()
60 (muse / "HEAD").write_text("refs/heads/main\n")
61 refs = muse / "refs" / "heads"
62 refs.mkdir(parents=True)
63 return tmp_path
64
65
66 # ---------------------------------------------------------------------------
67 # Unit tests — pure logic
68 # ---------------------------------------------------------------------------
69
70
71 def test_read_symbolic_ref_returns_result(muse_dir: pathlib.Path) -> None:
72 (muse_dir / "HEAD").write_text("refs/heads/main\n")
73 result = read_symbolic_ref(muse_dir, "HEAD")
74 assert result is not None
75 assert result.ref == "refs/heads/main"
76 assert result.name == "HEAD"
77 assert result.short == "main"
78
79
80 def test_read_symbolic_ref_missing_file_returns_none(muse_dir: pathlib.Path) -> None:
81 result = read_symbolic_ref(muse_dir, "HEAD", quiet=True)
82 assert result is None
83
84
85 def test_read_symbolic_ref_bare_sha_returns_none(muse_dir: pathlib.Path) -> None:
86 """Detached HEAD contains a bare SHA — not a symbolic ref."""
87 (muse_dir / "HEAD").write_text("a" * 64 + "\n")
88 result = read_symbolic_ref(muse_dir, "HEAD", quiet=True)
89 assert result is None
90
91
92 def test_symbolic_ref_result_short_no_slash() -> None:
93 """A ref without slashes uses the whole string as short form."""
94 result = SymbolicRefResult(name="HEAD", ref="main")
95 assert result.short == "main"
96
97
98 def test_write_symbolic_ref_creates_file(muse_dir: pathlib.Path) -> None:
99 write_symbolic_ref(muse_dir, "HEAD", "refs/heads/feature/x")
100 content = (muse_dir / "HEAD").read_text().strip()
101 assert content == "refs/heads/feature/x"
102
103
104 def test_write_symbolic_ref_raises_for_non_refs_target(muse_dir: pathlib.Path) -> None:
105 with pytest.raises(ValueError, match="must start with 'refs/'"):
106 write_symbolic_ref(muse_dir, "HEAD", "main")
107
108
109 def test_write_symbolic_ref_creates_intermediate_dirs(muse_dir: pathlib.Path) -> None:
110 write_symbolic_ref(muse_dir, "refs/heads/feature/guitar", "refs/heads/main")
111 assert (muse_dir / "refs" / "heads" / "feature" / "guitar").exists()
112
113
114 def test_delete_symbolic_ref_removes_file(muse_dir: pathlib.Path) -> None:
115 (muse_dir / "HEAD").write_text("refs/heads/main\n")
116 deleted = delete_symbolic_ref(muse_dir, "HEAD")
117 assert deleted is True
118 assert not (muse_dir / "HEAD").exists()
119
120
121 def test_delete_symbolic_ref_absent_returns_false(muse_dir: pathlib.Path) -> None:
122 deleted = delete_symbolic_ref(muse_dir, "HEAD", quiet=True)
123 assert deleted is False
124
125
126 # ---------------------------------------------------------------------------
127 # CLI integration tests
128 # ---------------------------------------------------------------------------
129
130
131 def test_cli_read_prints_full_ref(repo_root_with_head: pathlib.Path) -> None:
132 result = runner.invoke(cli, ["symbolic-ref", "HEAD"], env={"MUSE_REPO_ROOT": str(repo_root_with_head)})
133 assert result.exit_code == ExitCode.SUCCESS
134 assert "refs/heads/main" in result.output
135
136
137 def test_cli_read_short_prints_branch_name(repo_root_with_head: pathlib.Path) -> None:
138 result = runner.invoke(
139 cli,
140 ["symbolic-ref", "--short", "HEAD"],
141 env={"MUSE_REPO_ROOT": str(repo_root_with_head)},
142 )
143 assert result.exit_code == ExitCode.SUCCESS
144 assert result.output.strip() == "main"
145 assert "refs/heads/main" not in result.output
146
147
148 def test_cli_write_updates_file(repo_root_with_head: pathlib.Path) -> None:
149 result = runner.invoke(
150 cli,
151 ["symbolic-ref", "HEAD", "refs/heads/feature/guitar"],
152 env={"MUSE_REPO_ROOT": str(repo_root_with_head)},
153 )
154 assert result.exit_code == ExitCode.SUCCESS
155 content = (repo_root_with_head / ".muse" / "HEAD").read_text().strip()
156 assert content == "refs/heads/feature/guitar"
157
158
159 def test_cli_write_rejects_non_refs_target(repo_root_with_head: pathlib.Path) -> None:
160 result = runner.invoke(
161 cli,
162 ["symbolic-ref", "HEAD", "main"],
163 env={"MUSE_REPO_ROOT": str(repo_root_with_head)},
164 )
165 assert result.exit_code == ExitCode.USER_ERROR
166 assert "refs/" in result.output
167
168
169 def test_cli_delete_removes_file(repo_root_with_head: pathlib.Path) -> None:
170 result = runner.invoke(
171 cli,
172 ["symbolic-ref", "--delete", "HEAD"],
173 env={"MUSE_REPO_ROOT": str(repo_root_with_head)},
174 )
175 assert result.exit_code == ExitCode.SUCCESS
176 assert not (repo_root_with_head / ".muse" / "HEAD").exists()
177
178
179 def test_cli_delete_absent_file_exits_user_error(repo_root_with_head: pathlib.Path) -> None:
180 muse_dir = repo_root_with_head / ".muse"
181 (muse_dir / "HEAD").unlink()
182 result = runner.invoke(
183 cli,
184 ["symbolic-ref", "--delete", "HEAD"],
185 env={"MUSE_REPO_ROOT": str(repo_root_with_head)},
186 )
187 assert result.exit_code == ExitCode.USER_ERROR
188
189
190 def test_cli_read_non_symbolic_exits_user_error(repo_root_with_head: pathlib.Path) -> None:
191 """Detached HEAD (bare SHA) should exit USER_ERROR on read."""
192 (repo_root_with_head / ".muse" / "HEAD").write_text("a" * 64 + "\n")
193 result = runner.invoke(
194 cli,
195 ["symbolic-ref", "HEAD"],
196 env={"MUSE_REPO_ROOT": str(repo_root_with_head)},
197 )
198 assert result.exit_code == ExitCode.USER_ERROR
199
200
201 def test_cli_quiet_suppresses_error_output(repo_root_with_head: pathlib.Path) -> None:
202 """With -q, reading a non-symbolic ref should produce no user-facing text."""
203 (repo_root_with_head / ".muse" / "HEAD").write_text("a" * 64 + "\n")
204 result = runner.invoke(
205 cli,
206 ["symbolic-ref", "-q", "HEAD"],
207 env={"MUSE_REPO_ROOT": str(repo_root_with_head)},
208 )
209 assert result.exit_code == ExitCode.USER_ERROR
210 assert result.output.strip() == ""
211
212
213 # ---------------------------------------------------------------------------
214 # Boundary seal
215 # ---------------------------------------------------------------------------
216
217
218 def test_boundary_seal_future_annotations() -> None:
219 """Verify ``from __future__ import annotations`` is the first import."""
220 import maestro.muse_cli.commands.symbolic_ref as mod
221
222 source = pathlib.Path(mod.__file__).read_text()
223 tree = ast.parse(source)
224 for node in ast.walk(tree):
225 if isinstance(node, ast.ImportFrom):
226 if node.module == "__future__" and any(
227 alias.name == "annotations" for alias in node.names
228 ):
229 return
230 pytest.fail("from __future__ import annotations not found in symbolic_ref.py")