gabriel / muse public

test_breakage_supercharge.py file-level

at sha256:8 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:4 Merge branch 'dev' into main · gabriel · Jun 17, 2026
1 """Supercharge tests for ``muse code breakage`` β€” agent-usability gaps.
2
3 The existing test file (test_cmd_breakage.py, 855 lines) already covers
4 correctness, exit codes, regression, and stress. This file targets only the
5 gaps those tests leave open:
6
7 Coverage matrix
8 ---------------
9 - --json / -j: -j alias works identically to --json
10 - exit_code: JSON output includes exit_code mirroring process exit
11 - duration_ms: JSON output includes non-negative float duration_ms
12 - TypedDicts: _BreakageOutputJson gains exit_code/duration_ms annotations
13 - Docstrings: run() docstring mentions exit_code and duration_ms
14 - ANSI: JSON output never contains terminal escape sequences
15 - Performance: duration_ms stays under 2000 ms for a small repo
16
17 Critical distinction from other commands: exit_code in the JSON payload
18 mirrors the actual computed process exit code (may be 1 when issues are
19 found), NOT hardcoded to 0.
20 """
21
22 from __future__ import annotations
23 from collections.abc import Mapping
24
25 import json
26 import pathlib
27 import textwrap
28
29 import pytest
30
31 from tests.cli_test_helper import CliRunner
32
33 runner = CliRunner()
34
35
36 # ---------------------------------------------------------------------------
37 # Helpers
38 # ---------------------------------------------------------------------------
39
40
41 def _env(root: pathlib.Path) -> Mapping[str, str]:
42 return {"MUSE_REPO_ROOT": str(root)}
43
44
45 def _run(root: pathlib.Path, *args: str) -> "InvokeResult":
46 return runner.invoke(None, list(args), env=_env(root))
47
48
49 # ---------------------------------------------------------------------------
50 # Fixture β€” clean repo with no working-tree changes (no breakage)
51 # ---------------------------------------------------------------------------
52
53
54 @pytest.fixture()
55 def clean_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
56 """Code-domain repo whose working tree matches HEAD β€” zero breakage."""
57 monkeypatch.chdir(tmp_path)
58
59 r = _run(tmp_path, "init", "--domain", "code")
60 assert r.exit_code == 0, r.output
61
62 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
63 class Invoice:
64 def compute_total(self, items):
65 return sum(items)
66
67 def add_tax(self, rate):
68 return rate
69
70 def create_invoice(items):
71 return Invoice()
72 """))
73 r1 = _run(tmp_path, "code", "add", "billing.py")
74 assert r1.exit_code == 0, r1.output
75 r2 = _run(tmp_path, "commit", "-m", "initial billing")
76 assert r2.exit_code == 0, r2.output
77
78 return tmp_path
79
80
81 # ---------------------------------------------------------------------------
82 # Fixture β€” repo with a removed public method in the working tree (breakage)
83 # ---------------------------------------------------------------------------
84
85
86 @pytest.fixture()
87 def broken_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
88 """Repo where working tree removes a public method β†’ breakage detected.
89
90 Commit 1: Invoice has compute_total + add_tax
91 Working tree: Invoice has only compute_total (add_tax removed)
92 β†’ removed_public_method error β†’ exit_code == 1
93 """
94 monkeypatch.chdir(tmp_path)
95
96 r = _run(tmp_path, "init", "--domain", "code")
97 assert r.exit_code == 0, r.output
98
99 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
100 class Invoice:
101 def compute_total(self, items):
102 return sum(items)
103
104 def add_tax(self, rate):
105 return rate
106 """))
107 r1 = _run(tmp_path, "code", "add", "billing.py")
108 assert r1.exit_code == 0, r1.output
109 r2 = _run(tmp_path, "commit", "-m", "initial billing")
110 assert r2.exit_code == 0, r2.output
111
112 # Remove add_tax from working tree (not committed)
113 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
114 class Invoice:
115 def compute_total(self, items):
116 return sum(items)
117 """))
118
119 return tmp_path
120
121
122 # ---------------------------------------------------------------------------
123 # TestJsonAlias β€” -j works identically to --json
124 # ---------------------------------------------------------------------------
125
126
127 class TestJsonAlias:
128 """The -j shorthand must behave identically to --json."""
129
130 def test_j_alias_exits_zero_on_clean_repo(self, clean_repo: pathlib.Path) -> None:
131 r = _run(clean_repo, "code", "breakage", "-j")
132 assert r.exit_code == 0, r.output
133
134 def test_j_alias_valid_json(self, clean_repo: pathlib.Path) -> None:
135 r = _run(clean_repo, "code", "breakage", "-j")
136 json.loads(r.output) # must not raise
137
138 def test_j_alias_has_issues_key(self, clean_repo: pathlib.Path) -> None:
139 r = _run(clean_repo, "code", "breakage", "-j")
140 data = json.loads(r.output)
141 assert "issues" in data
142
143 def test_j_alias_has_errors_key(self, clean_repo: pathlib.Path) -> None:
144 r = _run(clean_repo, "code", "breakage", "-j")
145 data = json.loads(r.output)
146 assert "errors" in data
147
148 def test_j_alias_same_top_level_keys_as_json_flag(self, clean_repo: pathlib.Path) -> None:
149 r1 = _run(clean_repo, "code", "breakage", "--json")
150 r2 = _run(clean_repo, "code", "breakage", "-j")
151 d1 = json.loads(r1.output)
152 d2 = json.loads(r2.output)
153 d1.pop("duration_ms", None)
154 d2.pop("duration_ms", None)
155 assert set(d1.keys()) == set(d2.keys())
156
157 def test_j_alias_same_exit_code_as_json_flag(self, clean_repo: pathlib.Path) -> None:
158 r1 = _run(clean_repo, "code", "breakage", "--json")
159 r2 = _run(clean_repo, "code", "breakage", "-j")
160 assert r1.exit_code == r2.exit_code
161
162
163 # ---------------------------------------------------------------------------
164 # TestDurationMs β€” JSON output must include duration_ms
165 # ---------------------------------------------------------------------------
166
167
168 class TestDurationMs:
169 """JSON output must include a non-negative float duration_ms."""
170
171 def test_json_has_duration_ms(self, clean_repo: pathlib.Path) -> None:
172 r = _run(clean_repo, "code", "breakage", "--json")
173 data = json.loads(r.output)
174 assert "duration_ms" in data
175
176 def test_json_duration_ms_nonnegative(self, clean_repo: pathlib.Path) -> None:
177 r = _run(clean_repo, "code", "breakage", "--json")
178 data = json.loads(r.output)
179 assert data["duration_ms"] >= 0
180
181 def test_json_duration_ms_is_float(self, clean_repo: pathlib.Path) -> None:
182 r = _run(clean_repo, "code", "breakage", "--json")
183 data = json.loads(r.output)
184 assert isinstance(data["duration_ms"], float)
185
186 def test_j_alias_duration_ms_present(self, clean_repo: pathlib.Path) -> None:
187 r = _run(clean_repo, "code", "breakage", "-j")
188 data = json.loads(r.output)
189 assert "duration_ms" in data
190
191 def test_duration_ms_present_on_broken_repo(self, broken_repo: pathlib.Path) -> None:
192 r = _run(broken_repo, "code", "breakage", "--json")
193 data = json.loads(r.output)
194 assert "duration_ms" in data
195 assert data["duration_ms"] >= 0
196
197 def test_duration_ms_with_strict_flag(self, clean_repo: pathlib.Path) -> None:
198 r = _run(clean_repo, "code", "breakage", "--json", "--strict")
199 data = json.loads(r.output)
200 assert "duration_ms" in data
201 assert data["duration_ms"] >= 0
202
203
204 # ---------------------------------------------------------------------------
205 # TestExitCode β€” JSON output must include exit_code mirroring process exit
206 # ---------------------------------------------------------------------------
207
208
209 class TestExitCode:
210 """JSON output must include exit_code that mirrors the process exit code."""
211
212 def test_json_has_exit_code(self, clean_repo: pathlib.Path) -> None:
213 r = _run(clean_repo, "code", "breakage", "--json")
214 data = json.loads(r.output)
215 assert "exit_code" in data
216
217 def test_json_exit_code_zero_on_clean_repo(self, clean_repo: pathlib.Path) -> None:
218 r = _run(clean_repo, "code", "breakage", "--json")
219 assert r.exit_code == 0
220 data = json.loads(r.output)
221 assert data["exit_code"] == 0
222
223 def test_json_exit_code_is_int(self, clean_repo: pathlib.Path) -> None:
224 r = _run(clean_repo, "code", "breakage", "--json")
225 data = json.loads(r.output)
226 assert isinstance(data["exit_code"], int)
227
228 def test_j_alias_exit_code_present(self, clean_repo: pathlib.Path) -> None:
229 r = _run(clean_repo, "code", "breakage", "-j")
230 data = json.loads(r.output)
231 assert "exit_code" in data
232
233 def test_exit_code_mirrors_process_exit_on_clean(self, clean_repo: pathlib.Path) -> None:
234 r = _run(clean_repo, "code", "breakage", "--json")
235 data = json.loads(r.output)
236 assert data["exit_code"] == r.exit_code
237
238 def test_exit_code_one_on_broken_repo(self, broken_repo: pathlib.Path) -> None:
239 """Breakage with errors exits 1 and JSON exit_code == 1."""
240 r = _run(broken_repo, "code", "breakage", "--json")
241 assert r.exit_code == 1
242 data = json.loads(r.output)
243 assert data["exit_code"] == 1
244
245 def test_exit_code_mirrors_process_exit_on_broken(self, broken_repo: pathlib.Path) -> None:
246 r = _run(broken_repo, "code", "breakage", "--json")
247 data = json.loads(r.output)
248 assert data["exit_code"] == r.exit_code
249
250 def test_exit_code_in_json_is_not_hardcoded_zero(self, broken_repo: pathlib.Path) -> None:
251 """Verify exit_code reflects real exit, not a hardcoded 0."""
252 r = _run(broken_repo, "code", "breakage", "--json")
253 data = json.loads(r.output)
254 # exit_code must equal 1 (errors found), proving it's not hardcoded
255 assert data["exit_code"] != 0
256
257
258 # ---------------------------------------------------------------------------
259 # TestTypedDicts β€” _BreakageOutputJson carries the new fields
260 # ---------------------------------------------------------------------------
261
262
263 class TestTypedDicts:
264 """_BreakageOutputJson must carry exit_code/duration_ms annotations."""
265
266 def test_breakage_output_json_exists(self) -> None:
267 from muse.cli.commands.breakage import _BreakageOutputJson # noqa: F401
268
269 def test_breakage_output_json_has_exit_code_annotation(self) -> None:
270 from muse.cli.commands.breakage import _BreakageOutputJson
271 assert "exit_code" in _BreakageOutputJson.__annotations__
272
273 def test_breakage_output_json_has_duration_ms_annotation(self) -> None:
274 from muse.cli.commands.breakage import _BreakageOutputJson
275 assert "duration_ms" in _BreakageOutputJson.__annotations__
276
277 def test_breakage_output_json_retains_issues_annotation(self) -> None:
278 from muse.cli.commands.breakage import _BreakageOutputJson
279 assert "issues" in _BreakageOutputJson.__annotations__
280
281 def test_breakage_output_json_retains_errors_annotation(self) -> None:
282 from muse.cli.commands.breakage import _BreakageOutputJson
283 assert "errors" in _BreakageOutputJson.__annotations__
284
285 def test_breakage_output_json_retains_warnings_annotation(self) -> None:
286 from muse.cli.commands.breakage import _BreakageOutputJson
287 assert "warnings" in _BreakageOutputJson.__annotations__
288
289 def test_breakage_issue_exists(self) -> None:
290 from muse.cli.commands.breakage import _BreakageIssue # noqa: F401
291
292 def test_breakage_issue_has_severity(self) -> None:
293 from muse.cli.commands.breakage import _BreakageIssue
294 assert "severity" in _BreakageIssue.__annotations__
295
296
297 # ---------------------------------------------------------------------------
298 # TestDocstrings β€” run() docstring documents new fields
299 # ---------------------------------------------------------------------------
300
301
302 class TestDocstrings:
303 """run() must document exit_code."""
304
305 def test_run_docstring_documents_fields(self) -> None:
306 from muse.cli.commands.breakage import run
307 assert "exit_code" in run.__doc__
308
309
310 # ---------------------------------------------------------------------------
311 # TestAnsiSanitization β€” no escape codes in JSON output
312 # ---------------------------------------------------------------------------
313
314
315 class TestAnsiSanitization:
316 """No ANSI escape sequences anywhere in the JSON output."""
317
318 def test_json_output_no_ansi(self, clean_repo: pathlib.Path) -> None:
319 r = _run(clean_repo, "code", "breakage", "--json")
320 assert "\x1b" not in r.output
321
322 def test_j_alias_output_no_ansi(self, clean_repo: pathlib.Path) -> None:
323 r = _run(clean_repo, "code", "breakage", "-j")
324 assert "\x1b" not in r.output
325
326 def test_broken_repo_json_output_no_ansi(self, broken_repo: pathlib.Path) -> None:
327 r = _run(broken_repo, "code", "breakage", "--json")
328 assert "\x1b" not in r.output
329
330
331 # ---------------------------------------------------------------------------
332 # TestPerformance β€” duration_ms under 2000 ms for a small repo
333 # ---------------------------------------------------------------------------
334
335
336 class TestPerformance:
337 """duration_ms must be non-negative and under 2000 ms for small repos."""
338
339 def test_json_duration_under_2000ms(self, clean_repo: pathlib.Path) -> None:
340 r = _run(clean_repo, "code", "breakage", "--json")
341 data = json.loads(r.output)
342 assert data["duration_ms"] < 2000
343
344 def test_j_alias_duration_under_2000ms(self, clean_repo: pathlib.Path) -> None:
345 r = _run(clean_repo, "code", "breakage", "-j")
346 data = json.loads(r.output)
347 assert data["duration_ms"] < 2000
348
349 def test_broken_repo_duration_under_2000ms(self, broken_repo: pathlib.Path) -> None:
350 r = _run(broken_repo, "code", "breakage", "--json")
351 data = json.loads(r.output)
352 assert data["duration_ms"] < 2000
353
354 def test_duration_ms_is_float_not_int(self, clean_repo: pathlib.Path) -> None:
355 r = _run(clean_repo, "code", "breakage", "--json")
356 data = json.loads(r.output)
357 assert isinstance(data["duration_ms"], float)