gabriel / muse public
test_plumbing_check_attr.py python
182 lines 7.4 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Tests for ``muse plumbing check-attr``.
2
3 Verifies first-match-wins strategy resolution, priority ordering, dimension
4 filtering, ``--all-rules`` enumeration, text-format output, and fallback to
5 ``"auto"`` when no rule matches.
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_attrs(repo: pathlib.Path, content: str) -> None:
45 (repo / ".museattributes").write_text(content, encoding="utf-8")
46
47
48 # ---------------------------------------------------------------------------
49 # Tests
50 # ---------------------------------------------------------------------------
51
52
53 class TestCheckAttr:
54 def test_matching_rule_strategy_returned(self, tmp_path: pathlib.Path) -> None:
55 repo = _init_repo(tmp_path)
56 _write_attrs(repo, '[[rules]]\npath = "drums/*"\ndimension = "*"\nstrategy = "ours"\n')
57 result = runner.invoke(
58 cli, ["plumbing", "check-attr", "drums/kit.mid"], env=_env(repo)
59 )
60 assert result.exit_code == 0, result.output
61 data = json.loads(result.stdout)
62 assert data["results"][0]["strategy"] == "ours"
63 assert data["results"][0]["rule"]["path_pattern"] == "drums/*"
64
65 def test_no_match_returns_auto(self, tmp_path: pathlib.Path) -> None:
66 repo = _init_repo(tmp_path)
67 _write_attrs(repo, '[[rules]]\npath = "drums/*"\ndimension = "*"\nstrategy = "ours"\n')
68 result = runner.invoke(
69 cli, ["plumbing", "check-attr", "keys/melody.mid"], env=_env(repo)
70 )
71 assert result.exit_code == 0, result.output
72 r = json.loads(result.stdout)["results"][0]
73 assert r["strategy"] == "auto"
74 assert r["rule"] is None
75
76 def test_no_museattributes_returns_auto_with_zero_rules(self, tmp_path: pathlib.Path) -> None:
77 repo = _init_repo(tmp_path)
78 result = runner.invoke(
79 cli, ["plumbing", "check-attr", "any/path.mid"], env=_env(repo)
80 )
81 assert result.exit_code == 0, result.output
82 data = json.loads(result.stdout)
83 assert data["results"][0]["strategy"] == "auto"
84 assert data["rules_loaded"] == 0
85
86 def test_multiple_paths_resolved_independently(self, tmp_path: pathlib.Path) -> None:
87 repo = _init_repo(tmp_path)
88 _write_attrs(
89 repo,
90 '[[rules]]\npath = "drums/*"\ndimension = "*"\nstrategy = "ours"\n'
91 '[[rules]]\npath = "keys/*"\ndimension = "*"\nstrategy = "theirs"\n',
92 )
93 result = runner.invoke(
94 cli, ["plumbing", "check-attr", "drums/kit.mid", "keys/piano.mid", "misc/x.mid"],
95 env=_env(repo),
96 )
97 assert result.exit_code == 0, result.output
98 strategies = {r["path"]: r["strategy"] for r in json.loads(result.stdout)["results"]}
99 assert strategies["drums/kit.mid"] == "ours"
100 assert strategies["keys/piano.mid"] == "theirs"
101 assert strategies["misc/x.mid"] == "auto"
102
103 def test_priority_higher_wins_over_lower(self, tmp_path: pathlib.Path) -> None:
104 repo = _init_repo(tmp_path)
105 _write_attrs(
106 repo,
107 '[[rules]]\npath = "*"\ndimension = "*"\nstrategy = "auto"\npriority = 0\n'
108 '[[rules]]\npath = "drums/*"\ndimension = "*"\nstrategy = "ours"\npriority = 10\n',
109 )
110 result = runner.invoke(
111 cli, ["plumbing", "check-attr", "drums/kit.mid"], env=_env(repo)
112 )
113 assert result.exit_code == 0, result.output
114 assert json.loads(result.stdout)["results"][0]["strategy"] == "ours"
115
116 def test_dimension_flag_filters_to_specific_axis(self, tmp_path: pathlib.Path) -> None:
117 repo = _init_repo(tmp_path)
118 _write_attrs(
119 repo,
120 '[[rules]]\npath = "*"\ndimension = "notes"\nstrategy = "union"\n'
121 '[[rules]]\npath = "*"\ndimension = "tempo"\nstrategy = "ours"\n',
122 )
123 result = runner.invoke(
124 cli, ["plumbing", "check-attr", "--dimension", "tempo", "track.mid"],
125 env=_env(repo),
126 )
127 assert result.exit_code == 0, result.output
128 assert json.loads(result.stdout)["results"][0]["strategy"] == "ours"
129
130 def test_all_rules_flag_returns_every_match(self, tmp_path: pathlib.Path) -> None:
131 repo = _init_repo(tmp_path)
132 _write_attrs(
133 repo,
134 '[[rules]]\npath = "*"\ndimension = "*"\nstrategy = "auto"\n'
135 '[[rules]]\npath = "drums/*"\ndimension = "*"\nstrategy = "ours"\n',
136 )
137 result = runner.invoke(
138 cli, ["plumbing", "check-attr", "--all-rules", "drums/kit.mid"], env=_env(repo)
139 )
140 assert result.exit_code == 0, result.output
141 data = json.loads(result.stdout)
142 assert len(data["results"][0]["matching_rules"]) == 2
143
144 def test_all_rules_no_match_returns_empty_list(self, tmp_path: pathlib.Path) -> None:
145 repo = _init_repo(tmp_path)
146 _write_attrs(repo, '[[rules]]\npath = "drums/*"\ndimension = "*"\nstrategy = "ours"\n')
147 result = runner.invoke(
148 cli, ["plumbing", "check-attr", "--all-rules", "keys/x.mid"], env=_env(repo)
149 )
150 assert result.exit_code == 0, result.output
151 data = json.loads(result.stdout)
152 assert data["results"][0]["matching_rules"] == []
153
154 def test_text_format_output(self, tmp_path: pathlib.Path) -> None:
155 repo = _init_repo(tmp_path)
156 _write_attrs(repo, '[[rules]]\npath = "drums/*"\ndimension = "*"\nstrategy = "ours"\n')
157 result = runner.invoke(
158 cli, ["plumbing", "check-attr", "--format", "text", "drums/kit.mid"], env=_env(repo)
159 )
160 assert result.exit_code == 0, result.output
161 assert "strategy=ours" in result.stdout
162
163 def test_source_index_matches_rule_position(self, tmp_path: pathlib.Path) -> None:
164 repo = _init_repo(tmp_path)
165 _write_attrs(
166 repo,
167 '[[rules]]\npath = "a/*"\ndimension = "*"\nstrategy = "ours"\n'
168 '[[rules]]\npath = "b/*"\ndimension = "*"\nstrategy = "theirs"\n',
169 )
170 result = runner.invoke(
171 cli, ["plumbing", "check-attr", "b/x.mid"], env=_env(repo)
172 )
173 assert result.exit_code == 0, result.output
174 r = json.loads(result.stdout)["results"][0]
175 assert r["rule"]["source_index"] == 1
176
177 def test_bad_format_exits_user_error(self, tmp_path: pathlib.Path) -> None:
178 repo = _init_repo(tmp_path)
179 result = runner.invoke(
180 cli, ["plumbing", "check-attr", "--format", "xml", "any.mid"], env=_env(repo)
181 )
182 assert result.exit_code == ExitCode.USER_ERROR