gabriel / muse public

test_invariants_supercharge.py file-level

at sha256:2 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:b adding issues docs to bust staging mpack prebuild cache. · gabriel · Jun 20, 2026
1 """Supercharge tests for ``muse code invariants`` β€” agent-usability gaps.
2
3 Existing tests (test_cmd_invariants.py) cover rule types, violation detection,
4 --strict, --rule filter, no-rules file default, JSON schema basics.
5
6 This file targets only the gaps those tests leave open:
7
8 Coverage matrix
9 ---------------
10 - --json / -j: -j alias works identically to --json
11 - exit_code: JSON output includes exit_code reflecting violation status
12 (0 = all pass / warnings only; 1 = errors or strict+warnings)
13 - duration_ms: JSON output includes non-negative float duration_ms
14 - TypedDicts: _InvariantsOutputJson carries exit_code and duration_ms
15 - Docstrings: run() docstring mentions exit_code and duration_ms
16 - ANSI: JSON output never contains terminal escape sequences
17 - Performance: duration_ms stays under 2000 ms for a small repo
18 - Early-exit paths: no-match --rule filter emits exit_code and duration_ms
19 - HEAD~N ref syntax: --commit HEAD~1 must not crash (was raising ValueError)
20 """
21
22 from __future__ import annotations
23 from collections.abc import Mapping
24
25 import argparse
26 import json
27 import pathlib
28 import textwrap
29
30 import pytest
31
32 from tests.cli_test_helper import CliRunner, InvokeResult
33
34 runner = CliRunner()
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41
42 def _env(root: pathlib.Path) -> Mapping[str, str]:
43 return {"MUSE_REPO_ROOT": str(root)}
44
45
46 def _run(root: pathlib.Path, *args: str) -> InvokeResult:
47 return runner.invoke(None, list(args), env=_env(root))
48
49
50 # ---------------------------------------------------------------------------
51 # Fixture β€” minimal Python repo (no invariants violations)
52 # ---------------------------------------------------------------------------
53
54
55 @pytest.fixture()
56 def inv_repo(
57 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
58 ) -> pathlib.Path:
59 """Minimal repo β€” clean Python files, no circular imports."""
60 monkeypatch.chdir(tmp_path)
61 r = _run(tmp_path, "init", "--domain", "code")
62 assert r.exit_code == 0, r.output
63
64 (tmp_path / "core.py").write_text(textwrap.dedent("""\
65 def compute(x):
66 return x * 2
67 """))
68 (tmp_path / "service.py").write_text(textwrap.dedent("""\
69 from core import compute
70
71 def process(x):
72 return compute(x)
73 """))
74 r = _run(tmp_path, "code", "add", ".")
75 assert r.exit_code == 0, r.output
76 r = _run(tmp_path, "commit", "-m", "seed invariants repo")
77 assert r.exit_code == 0, r.output
78
79 return tmp_path
80
81
82 # ---------------------------------------------------------------------------
83 # TestJsonAlias β€” -j works identically to --json
84 # ---------------------------------------------------------------------------
85
86
87 class TestJsonAlias:
88 """-j shorthand must behave identically to --json."""
89
90 def test_j_alias_exits_zero(self, inv_repo: pathlib.Path) -> None:
91 r = _run(inv_repo, "code", "invariants", "-j")
92 assert r.exit_code == 0, r.output
93
94 def test_j_alias_valid_json(self, inv_repo: pathlib.Path) -> None:
95 r = _run(inv_repo, "code", "invariants", "-j")
96 json.loads(r.output) # must not raise
97
98 def test_j_alias_has_violations_key(self, inv_repo: pathlib.Path) -> None:
99 r = _run(inv_repo, "code", "invariants", "-j")
100 assert "violations" in json.loads(r.output)
101
102 def test_j_alias_has_errors_key(self, inv_repo: pathlib.Path) -> None:
103 r = _run(inv_repo, "code", "invariants", "-j")
104 assert "errors" in json.loads(r.output)
105
106 def test_j_alias_same_top_level_keys_as_json_flag(
107 self, inv_repo: pathlib.Path
108 ) -> None:
109 r1 = _run(inv_repo, "code", "invariants", "--json")
110 r2 = _run(inv_repo, "code", "invariants", "-j")
111 d1 = json.loads(r1.output)
112 d2 = json.loads(r2.output)
113 d1.pop("duration_ms", None)
114 d2.pop("duration_ms", None)
115 assert set(d1.keys()) == set(d2.keys())
116
117 def test_j_alias_violations_is_list(self, inv_repo: pathlib.Path) -> None:
118 r = _run(inv_repo, "code", "invariants", "-j")
119 data = json.loads(r.output)
120 assert isinstance(data["violations"], list)
121
122 def test_j_alias_rules_checked_positive(self, inv_repo: pathlib.Path) -> None:
123 r = _run(inv_repo, "code", "invariants", "-j")
124 data = json.loads(r.output)
125 assert data["rules_checked"] >= 0
126
127
128 # ---------------------------------------------------------------------------
129 # TestDurationMs β€” JSON output must include duration_ms
130 # ---------------------------------------------------------------------------
131
132
133 class TestDurationMs:
134 """JSON output must include a non-negative float duration_ms."""
135
136 def test_json_has_duration_ms(self, inv_repo: pathlib.Path) -> None:
137 r = _run(inv_repo, "code", "invariants", "--json")
138 assert "duration_ms" in json.loads(r.output)
139
140 def test_json_duration_ms_nonnegative(self, inv_repo: pathlib.Path) -> None:
141 r = _run(inv_repo, "code", "invariants", "--json")
142 assert json.loads(r.output)["duration_ms"] >= 0
143
144 def test_json_duration_ms_is_float(self, inv_repo: pathlib.Path) -> None:
145 r = _run(inv_repo, "code", "invariants", "--json")
146 assert isinstance(json.loads(r.output)["duration_ms"], float)
147
148 def test_j_alias_duration_ms_present(self, inv_repo: pathlib.Path) -> None:
149 r = _run(inv_repo, "code", "invariants", "-j")
150 assert "duration_ms" in json.loads(r.output)
151
152 def test_duration_ms_under_2000ms(self, inv_repo: pathlib.Path) -> None:
153 r = _run(inv_repo, "code", "invariants", "--json")
154 assert json.loads(r.output)["duration_ms"] < 2000
155
156
157 # ---------------------------------------------------------------------------
158 # TestExitCode β€” JSON includes exit_code reflecting violation status
159 # ---------------------------------------------------------------------------
160
161
162 class TestExitCode:
163 """JSON exit_code must mirror the process exit code."""
164
165 def test_json_has_exit_code(self, inv_repo: pathlib.Path) -> None:
166 r = _run(inv_repo, "code", "invariants", "--json")
167 assert "exit_code" in json.loads(r.output)
168
169 def test_json_exit_code_zero_clean_repo(self, inv_repo: pathlib.Path) -> None:
170 r = _run(inv_repo, "code", "invariants", "--json")
171 assert r.exit_code == 0
172 assert json.loads(r.output)["exit_code"] == 0
173
174 def test_json_exit_code_is_int(self, inv_repo: pathlib.Path) -> None:
175 r = _run(inv_repo, "code", "invariants", "--json")
176 assert isinstance(json.loads(r.output)["exit_code"], int)
177
178 def test_j_alias_exit_code_present(self, inv_repo: pathlib.Path) -> None:
179 r = _run(inv_repo, "code", "invariants", "-j")
180 assert "exit_code" in json.loads(r.output)
181
182 def test_exit_code_mirrors_process_exit(self, inv_repo: pathlib.Path) -> None:
183 r = _run(inv_repo, "code", "invariants", "--json")
184 assert json.loads(r.output)["exit_code"] == r.exit_code
185
186
187 # ---------------------------------------------------------------------------
188 # TestTypedDicts β€” _InvariantsOutputJson carries exit_code and duration_ms
189 # ---------------------------------------------------------------------------
190
191
192 class TestTypedDicts:
193 """_InvariantsOutputJson must carry exit_code and duration_ms annotations."""
194
195 def test_invariants_output_json_typeddict_exists(self) -> None:
196 from muse.cli.commands.invariants import _InvariantsOutputJson # noqa: F401
197
198 def test_has_exit_code_annotation(self) -> None:
199 from muse.cli.commands.invariants import _InvariantsOutputJson
200 assert "exit_code" in _InvariantsOutputJson.__annotations__
201
202 def test_has_duration_ms_annotation(self) -> None:
203 from muse.cli.commands.invariants import _InvariantsOutputJson
204 assert "duration_ms" in _InvariantsOutputJson.__annotations__
205
206 def test_has_violations_annotation(self) -> None:
207 from muse.cli.commands.invariants import _InvariantsOutputJson
208 assert "violations" in _InvariantsOutputJson.__annotations__
209
210 def test_has_errors_annotation(self) -> None:
211 from muse.cli.commands.invariants import _InvariantsOutputJson
212 assert "errors" in _InvariantsOutputJson.__annotations__
213
214 def test_has_warnings_annotation(self) -> None:
215 from muse.cli.commands.invariants import _InvariantsOutputJson
216 assert "warnings" in _InvariantsOutputJson.__annotations__
217
218
219 # ---------------------------------------------------------------------------
220 # TestDocstrings β€” run() docstring documents exit_code and duration_ms
221 # ---------------------------------------------------------------------------
222
223
224 # ---------------------------------------------------------------------------
225 # TestAnsiSanitization β€” no escape codes in JSON output
226 # ---------------------------------------------------------------------------
227
228
229 class TestAnsiSanitization:
230 """No ANSI escape sequences anywhere in the JSON output."""
231
232 def test_json_output_no_ansi(self, inv_repo: pathlib.Path) -> None:
233 r = _run(inv_repo, "code", "invariants", "--json")
234 assert "\x1b" not in r.output
235
236 def test_j_alias_output_no_ansi(self, inv_repo: pathlib.Path) -> None:
237 r = _run(inv_repo, "code", "invariants", "-j")
238 assert "\x1b" not in r.output
239
240
241 # ---------------------------------------------------------------------------
242 # TestPerformance β€” duration_ms under 2000 ms for small repo
243 # ---------------------------------------------------------------------------
244
245
246 class TestPerformance:
247 """duration_ms must stay under 2000 ms for small repos."""
248
249 def test_json_duration_under_2000ms(self, inv_repo: pathlib.Path) -> None:
250 r = _run(inv_repo, "code", "invariants", "--json")
251 assert json.loads(r.output)["duration_ms"] < 2000
252
253 def test_duration_ms_is_float_not_int(self, inv_repo: pathlib.Path) -> None:
254 r = _run(inv_repo, "code", "invariants", "--json")
255 assert isinstance(json.loads(r.output)["duration_ms"], float)
256
257
258 # ---------------------------------------------------------------------------
259 # TestEarlyExitPaths β€” no-match and no-rules paths must include envelope fields
260 # ---------------------------------------------------------------------------
261
262
263 class TestEarlyExitPaths:
264 """Every JSON-emitting code path must include exit_code and duration_ms."""
265
266 def test_no_match_rule_filter_has_exit_code(self, inv_repo: pathlib.Path) -> None:
267 r = _run(inv_repo, "code", "invariants", "--rule", "zzz_no_such_rule", "-j")
268 assert r.exit_code == 0, r.output
269 data = json.loads(r.output)
270 assert "exit_code" in data
271 assert data["exit_code"] == 0
272
273 def test_no_match_rule_filter_has_duration_ms(self, inv_repo: pathlib.Path) -> None:
274 r = _run(inv_repo, "code", "invariants", "--rule", "zzz_no_such_rule", "-j")
275 data = json.loads(r.output)
276 assert "duration_ms" in data
277 assert isinstance(data["duration_ms"], float)
278 assert data["duration_ms"] >= 0
279
280 def test_no_match_rule_filter_has_error_field(self, inv_repo: pathlib.Path) -> None:
281 r = _run(inv_repo, "code", "invariants", "--rule", "zzz_no_such_rule", "-j")
282 data = json.loads(r.output)
283 assert data.get("error") == "no_matching_rules"
284
285 def test_no_match_rule_filter_no_ansi(self, inv_repo: pathlib.Path) -> None:
286 r = _run(inv_repo, "code", "invariants", "--rule", "zzz_no_such_rule", "-j")
287 assert "\x1b" not in r.output
288
289
290 # ---------------------------------------------------------------------------
291 # TestRelativeRefSyntax β€” HEAD~N must not crash
292 # ---------------------------------------------------------------------------
293
294
295 class TestRelativeRefSyntax:
296 """--commit HEAD~N and similar relative refs must not raise ValueError."""
297
298 @pytest.fixture()
299 def two_commit_repo(
300 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
301 ) -> pathlib.Path:
302 """Repo with two commits so HEAD~1 resolves to a real commit."""
303 monkeypatch.chdir(tmp_path)
304 r = _run(tmp_path, "init", "--domain", "code")
305 assert r.exit_code == 0, r.output
306
307 (tmp_path / "core.py").write_text("def compute(x):\n return x * 2\n")
308 r = _run(tmp_path, "code", "add", ".")
309 assert r.exit_code == 0, r.output
310 r = _run(tmp_path, "commit", "-m", "first")
311 assert r.exit_code == 0, r.output
312
313 (tmp_path / "service.py").write_text("def process(x):\n return x\n")
314 r = _run(tmp_path, "code", "add", ".")
315 assert r.exit_code == 0, r.output
316 r = _run(tmp_path, "commit", "-m", "second")
317 assert r.exit_code == 0, r.output
318
319 return tmp_path
320
321 def test_head_tilde_1_does_not_crash(self, two_commit_repo: pathlib.Path) -> None:
322 r = _run(two_commit_repo, "code", "invariants", "--commit", "HEAD~1", "-j")
323 # Must not crash with ValueError β€” exit 0 or 1 (depending on violations)
324 assert r.exit_code in (0, 1), r.output
325
326 def test_head_tilde_1_emits_valid_json(self, two_commit_repo: pathlib.Path) -> None:
327 r = _run(two_commit_repo, "code", "invariants", "--commit", "HEAD~1", "-j")
328 assert r.exit_code in (0, 1), r.output
329 json.loads(r.output) # must not raise
330
331 def test_head_tilde_1_has_exit_code(self, two_commit_repo: pathlib.Path) -> None:
332 r = _run(two_commit_repo, "code", "invariants", "--commit", "HEAD~1", "-j")
333 assert "exit_code" in json.loads(r.output)
334
335 def test_head_tilde_1_has_duration_ms(self, two_commit_repo: pathlib.Path) -> None:
336 r = _run(two_commit_repo, "code", "invariants", "--commit", "HEAD~1", "-j")
337 data = json.loads(r.output)
338 assert "duration_ms" in data
339 assert isinstance(data["duration_ms"], float)
340
341
342 # ---------------------------------------------------------------------------
343 # TestRegisterFlags β€” argparse-level verification
344 # ---------------------------------------------------------------------------
345
346
347 class TestRegisterFlags:
348 """Verify that register() wires --json / -j correctly."""
349
350 def _make_parser(self) -> "argparse.ArgumentParser":
351 import argparse
352 from muse.cli.commands.invariants import register
353 ap = argparse.ArgumentParser()
354 subs = ap.add_subparsers()
355 register(subs)
356 return ap
357
358 def test_json_flag_long(self) -> None:
359 ns = self._make_parser().parse_args(["invariants", "--json"])
360 assert ns.json_out is True
361
362 def test_j_alias(self) -> None:
363 ns = self._make_parser().parse_args(["invariants", "-j"])
364 assert ns.json_out is True
365
366 def test_default_is_text(self) -> None:
367 ns = self._make_parser().parse_args(["invariants"])
368 assert ns.json_out is False
369
370 def test_dest_is_json_out(self) -> None:
371 ns = self._make_parser().parse_args(["invariants", "-j"])
372 assert hasattr(ns, "json_out")
373 assert not hasattr(ns, "fmt")