gabriel / muse public
test_plumbing_check_ignore.py python
190 lines 7.9 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Tests for ``muse plumbing check-ignore``.
2
3 Verifies pattern evaluation (global + domain sections), last-match-wins
4 semantics, negation rules, quiet mode exit codes, matching-pattern reporting,
5 and text-format output.
6 """
7
8 from __future__ import annotations
9
10 import json
11 import pathlib
12
13 from tests.cli_test_helper import CliRunner
14
15 cli = None # argparse migration — CliRunner ignores this arg
16 from muse.core.errors import ExitCode
17
18 runner = CliRunner()
19
20
21 # ---------------------------------------------------------------------------
22 # Helpers
23 # ---------------------------------------------------------------------------
24
25
26 def _init_repo(path: pathlib.Path, domain: str = "midi") -> pathlib.Path:
27 muse = path / ".muse"
28 muse.mkdir(parents=True, exist_ok=True)
29 (muse / "commits").mkdir(exist_ok=True)
30 (muse / "snapshots").mkdir(exist_ok=True)
31 (muse / "objects").mkdir(exist_ok=True)
32 (muse / "refs" / "heads").mkdir(parents=True, exist_ok=True)
33 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
34 (muse / "repo.json").write_text(
35 json.dumps({"repo_id": "test-repo", "domain": domain}), encoding="utf-8"
36 )
37 return path
38
39
40 def _env(repo: pathlib.Path) -> dict[str, str]:
41 return {"MUSE_REPO_ROOT": str(repo)}
42
43
44 def _write_ignore(repo: pathlib.Path, content: str) -> None:
45 (repo / ".museignore").write_text(content, encoding="utf-8")
46
47
48 # ---------------------------------------------------------------------------
49 # Tests
50 # ---------------------------------------------------------------------------
51
52
53 class TestCheckIgnore:
54 def test_ignored_path_reports_true(self, tmp_path: pathlib.Path) -> None:
55 repo = _init_repo(tmp_path)
56 _write_ignore(repo, '[global]\npatterns = ["build/"]\n')
57 result = runner.invoke(
58 cli, ["plumbing", "check-ignore", "build/output.bin"], env=_env(repo)
59 )
60 assert result.exit_code == 0, result.output
61 data = json.loads(result.stdout)
62 assert data["results"][0]["ignored"] is True
63
64 def test_non_ignored_path_reports_false(self, tmp_path: pathlib.Path) -> None:
65 repo = _init_repo(tmp_path)
66 _write_ignore(repo, '[global]\npatterns = ["build/"]\n')
67 result = runner.invoke(
68 cli, ["plumbing", "check-ignore", "tracks/drums.mid"], env=_env(repo)
69 )
70 assert result.exit_code == 0, result.output
71 data = json.loads(result.stdout)
72 assert data["results"][0]["ignored"] is False
73
74 def test_multiple_paths_evaluated_independently(self, tmp_path: pathlib.Path) -> None:
75 repo = _init_repo(tmp_path)
76 _write_ignore(repo, '[global]\npatterns = ["*.bin"]\n')
77 result = runner.invoke(
78 cli, ["plumbing", "check-ignore", "a.bin", "b.mid", "c.bin"], env=_env(repo)
79 )
80 assert result.exit_code == 0, result.output
81 results = {r["path"]: r["ignored"] for r in json.loads(result.stdout)["results"]}
82 assert results["a.bin"] is True
83 assert results["b.mid"] is False
84 assert results["c.bin"] is True
85
86 def test_no_museignore_means_nothing_is_ignored(self, tmp_path: pathlib.Path) -> None:
87 repo = _init_repo(tmp_path)
88 result = runner.invoke(
89 cli, ["plumbing", "check-ignore", "tracks/drums.mid"], env=_env(repo)
90 )
91 assert result.exit_code == 0, result.output
92 data = json.loads(result.stdout)
93 assert data["results"][0]["ignored"] is False
94 assert data["patterns_loaded"] == 0
95
96 def test_negation_rule_unignores_previously_matched_path(self, tmp_path: pathlib.Path) -> None:
97 repo = _init_repo(tmp_path)
98 _write_ignore(repo, '[global]\npatterns = ["*.bin", "!important.bin"]\n')
99 result = runner.invoke(
100 cli, ["plumbing", "check-ignore", "other.bin", "important.bin"], env=_env(repo)
101 )
102 assert result.exit_code == 0, result.output
103 results = {r["path"]: r["ignored"] for r in json.loads(result.stdout)["results"]}
104 assert results["other.bin"] is True
105 assert results["important.bin"] is False
106
107 def test_matching_pattern_reported_in_result(self, tmp_path: pathlib.Path) -> None:
108 repo = _init_repo(tmp_path)
109 _write_ignore(repo, '[global]\npatterns = ["build/"]\n')
110 result = runner.invoke(
111 cli, ["plumbing", "check-ignore", "build/artifact.bin"], env=_env(repo)
112 )
113 assert result.exit_code == 0, result.output
114 r = json.loads(result.stdout)["results"][0]
115 assert r["matching_pattern"] == "build/"
116
117 def test_non_matching_path_has_null_pattern(self, tmp_path: pathlib.Path) -> None:
118 repo = _init_repo(tmp_path)
119 _write_ignore(repo, '[global]\npatterns = ["build/"]\n')
120 result = runner.invoke(
121 cli, ["plumbing", "check-ignore", "tracks/drums.mid"], env=_env(repo)
122 )
123 assert result.exit_code == 0, result.output
124 r = json.loads(result.stdout)["results"][0]
125 assert r["matching_pattern"] is None
126
127 def test_domain_specific_patterns_applied(self, tmp_path: pathlib.Path) -> None:
128 repo = _init_repo(tmp_path, domain="midi")
129 _write_ignore(repo, '[global]\npatterns = []\n[domain.midi]\npatterns = ["*.mid"]\n')
130 result = runner.invoke(
131 cli, ["plumbing", "check-ignore", "track.mid"], env=_env(repo)
132 )
133 assert result.exit_code == 0, result.output
134 assert json.loads(result.stdout)["results"][0]["ignored"] is True
135
136 def test_domain_patterns_not_applied_to_other_domains(self, tmp_path: pathlib.Path) -> None:
137 repo = _init_repo(tmp_path, domain="code")
138 _write_ignore(repo, '[domain.midi]\npatterns = ["*.mid"]\n')
139 result = runner.invoke(
140 cli, ["plumbing", "check-ignore", "track.mid"], env=_env(repo)
141 )
142 assert result.exit_code == 0, result.output
143 assert json.loads(result.stdout)["results"][0]["ignored"] is False
144
145 def test_quiet_all_ignored_exits_zero(self, tmp_path: pathlib.Path) -> None:
146 repo = _init_repo(tmp_path)
147 _write_ignore(repo, '[global]\npatterns = ["*.bin"]\n')
148 result = runner.invoke(
149 cli, ["plumbing", "check-ignore", "--quiet", "a.bin", "b.bin"], env=_env(repo)
150 )
151 assert result.exit_code == 0
152 assert result.stdout.strip() == ""
153
154 def test_quiet_any_not_ignored_exits_user_error(self, tmp_path: pathlib.Path) -> None:
155 repo = _init_repo(tmp_path)
156 _write_ignore(repo, '[global]\npatterns = ["*.bin"]\n')
157 result = runner.invoke(
158 cli, ["plumbing", "check-ignore", "--quiet", "a.bin", "keep.mid"], env=_env(repo)
159 )
160 assert result.exit_code == ExitCode.USER_ERROR
161
162 def test_text_format_output(self, tmp_path: pathlib.Path) -> None:
163 repo = _init_repo(tmp_path)
164 _write_ignore(repo, '[global]\npatterns = ["*.bin"]\n')
165 result = runner.invoke(
166 cli, ["plumbing", "check-ignore", "--format", "text", "a.bin", "b.mid"], env=_env(repo)
167 )
168 assert result.exit_code == 0, result.output
169 assert "ignored" in result.stdout
170 assert "ok" in result.stdout
171
172 def test_verbose_text_shows_matching_pattern(self, tmp_path: pathlib.Path) -> None:
173 repo = _init_repo(tmp_path)
174 _write_ignore(repo, '[global]\npatterns = ["build/"]\n')
175 result = runner.invoke(
176 cli,
177 ["plumbing", "check-ignore", "--format", "text", "--verbose", "build/x.bin"],
178 env=_env(repo),
179 )
180 assert result.exit_code == 0, result.output
181 assert "build/" in result.stdout
182
183 def test_patterns_loaded_count_correct(self, tmp_path: pathlib.Path) -> None:
184 repo = _init_repo(tmp_path)
185 _write_ignore(repo, '[global]\npatterns = ["*.bin", "build/"]\n')
186 result = runner.invoke(
187 cli, ["plumbing", "check-ignore", "x.bin"], env=_env(repo)
188 )
189 assert result.exit_code == 0, result.output
190 assert json.loads(result.stdout)["patterns_loaded"] == 2