gabriel / muse public
test_blame_supercharge.py python
336 lines 13.0 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Supercharge tests for ``muse code blame`` — agent-usability gaps.
2
3 The three existing test files (test_cmd_blame.py, test_cmd_blame_hardening.py,
4 test_core_blame.py) already cover correctness, security, rename tracking, kind/
5 author filters, E2E, and stress. This file targets only the gaps those files
6 leave open:
7
8 Coverage matrix
9 ---------------
10 - --json / -j: -j alias works identically to --json
11 - exit_code: JSON output includes exit_code = 0 on success
12 - duration_ms: JSON output includes non-negative float duration_ms
13 - TypedDicts: _BlameResultJson gains exit_code/duration_ms annotations
14 - Docstrings: run() docstring mentions exit_code and duration_ms
15 - ANSI: JSON output never contains terminal escape sequences
16 - Performance: duration_ms stays under 1000 ms for a small repo
17 """
18
19 from __future__ import annotations
20 from collections.abc import Mapping
21
22 import json
23 import pathlib
24 import textwrap
25
26 import pytest
27
28 from tests.cli_test_helper import CliRunner
29
30 runner = CliRunner()
31
32
33 # ---------------------------------------------------------------------------
34 # Helpers
35 # ---------------------------------------------------------------------------
36
37
38 def _env(root: pathlib.Path) -> Mapping[str, str]:
39 return {"MUSE_REPO_ROOT": str(root)}
40
41
42 def _run(root: pathlib.Path, *args: str) -> "InvokeResult":
43 return runner.invoke(None, list(args), env=_env(root))
44
45
46 # ---------------------------------------------------------------------------
47 # Fixture — repo with a committed symbol so blame returns real events
48 # ---------------------------------------------------------------------------
49
50
51 @pytest.fixture()
52 def blame_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
53 """Code-domain repo with two commits containing structured deltas.
54
55 Commit 1: create billing.py with Invoice.compute_total + process_order
56 Commit 2: modify compute_total body → blame sees a 'modified' event
57 """
58 monkeypatch.chdir(tmp_path)
59
60 r = _run(tmp_path, "init", "--domain", "code")
61 assert r.exit_code == 0, r.output
62
63 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
64 class Invoice:
65 def compute_total(self, items):
66 return sum(items)
67
68 def process_order(invoice, items):
69 return invoice.compute_total(items)
70 """))
71 r1 = _run(tmp_path, "code", "add", "billing.py")
72 assert r1.exit_code == 0, r1.output
73 r2 = _run(tmp_path, "commit", "-m", "initial billing")
74 assert r2.exit_code == 0, r2.output
75
76 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
77 class Invoice:
78 def compute_total(self, items):
79 return round(sum(items), 2)
80
81 def process_order(invoice, items):
82 return invoice.compute_total(items)
83 """))
84 r3 = _run(tmp_path, "code", "add", "billing.py")
85 assert r3.exit_code == 0, r3.output
86 r4 = _run(tmp_path, "commit", "-m", "round result")
87 assert r4.exit_code == 0, r4.output
88
89 return tmp_path
90
91
92 def _blame_address(root: pathlib.Path) -> str:
93 """Return a symbol address that blame can find in blame_repo."""
94 return "billing.py::Invoice.compute_total"
95
96
97 # ---------------------------------------------------------------------------
98 # TestJsonAlias — -j works identically to --json
99 # ---------------------------------------------------------------------------
100
101
102 class TestJsonAlias:
103 """The -j shorthand must behave identically to --json."""
104
105 def test_j_alias_exits_zero(self, blame_repo: pathlib.Path) -> None:
106 addr = _blame_address(blame_repo)
107 r = _run(blame_repo, "code", "blame", addr, "-j")
108 assert r.exit_code == 0, r.output
109
110 def test_j_alias_valid_json(self, blame_repo: pathlib.Path) -> None:
111 addr = _blame_address(blame_repo)
112 r = _run(blame_repo, "code", "blame", addr, "-j")
113 json.loads(r.output) # must not raise
114
115 def test_j_alias_has_events_key(self, blame_repo: pathlib.Path) -> None:
116 addr = _blame_address(blame_repo)
117 r = _run(blame_repo, "code", "blame", addr, "-j")
118 data = json.loads(r.output)
119 assert "events" in data
120
121 def test_j_alias_has_address_key(self, blame_repo: pathlib.Path) -> None:
122 addr = _blame_address(blame_repo)
123 r = _run(blame_repo, "code", "blame", addr, "-j")
124 data = json.loads(r.output)
125 assert data["address"] == addr
126
127 def test_j_alias_same_top_level_keys_as_json_flag(self, blame_repo: pathlib.Path) -> None:
128 addr = _blame_address(blame_repo)
129 r1 = _run(blame_repo, "code", "blame", addr, "--json")
130 r2 = _run(blame_repo, "code", "blame", addr, "-j")
131 d1 = json.loads(r1.output)
132 d2 = json.loads(r2.output)
133 d1.pop("duration_ms", None)
134 d2.pop("duration_ms", None)
135 assert set(d1.keys()) == set(d2.keys())
136
137 def test_j_alias_address_matches(self, blame_repo: pathlib.Path) -> None:
138 addr = _blame_address(blame_repo)
139 r1 = _run(blame_repo, "code", "blame", addr, "--json")
140 r2 = _run(blame_repo, "code", "blame", addr, "-j")
141 assert json.loads(r1.output)["address"] == json.loads(r2.output)["address"]
142
143
144 # ---------------------------------------------------------------------------
145 # TestDurationMs — JSON output must include duration_ms
146 # ---------------------------------------------------------------------------
147
148
149 class TestDurationMs:
150 """JSON output must include a non-negative float duration_ms."""
151
152 def test_json_has_duration_ms(self, blame_repo: pathlib.Path) -> None:
153 addr = _blame_address(blame_repo)
154 r = _run(blame_repo, "code", "blame", addr, "--json")
155 data = json.loads(r.output)
156 assert "duration_ms" in data
157
158 def test_json_duration_ms_nonnegative(self, blame_repo: pathlib.Path) -> None:
159 addr = _blame_address(blame_repo)
160 r = _run(blame_repo, "code", "blame", addr, "--json")
161 data = json.loads(r.output)
162 assert data["duration_ms"] >= 0
163
164 def test_json_duration_ms_is_float(self, blame_repo: pathlib.Path) -> None:
165 addr = _blame_address(blame_repo)
166 r = _run(blame_repo, "code", "blame", addr, "--json")
167 data = json.loads(r.output)
168 assert isinstance(data["duration_ms"], float)
169
170 def test_j_alias_duration_ms_present(self, blame_repo: pathlib.Path) -> None:
171 addr = _blame_address(blame_repo)
172 r = _run(blame_repo, "code", "blame", addr, "-j")
173 data = json.loads(r.output)
174 assert "duration_ms" in data
175
176 def test_duration_ms_with_kind_filter(self, blame_repo: pathlib.Path) -> None:
177 addr = _blame_address(blame_repo)
178 r = _run(blame_repo, "code", "blame", addr, "--json", "--kind", "created")
179 data = json.loads(r.output)
180 assert "duration_ms" in data
181 assert data["duration_ms"] >= 0
182
183 def test_duration_ms_with_all_flag(self, blame_repo: pathlib.Path) -> None:
184 addr = _blame_address(blame_repo)
185 r = _run(blame_repo, "code", "blame", addr, "--json", "--all")
186 data = json.loads(r.output)
187 assert "duration_ms" in data
188 assert data["duration_ms"] >= 0
189
190
191 # ---------------------------------------------------------------------------
192 # TestExitCode — JSON output must include exit_code
193 # ---------------------------------------------------------------------------
194
195
196 class TestExitCode:
197 """JSON output must include exit_code = 0 on success."""
198
199 def test_json_has_exit_code(self, blame_repo: pathlib.Path) -> None:
200 addr = _blame_address(blame_repo)
201 r = _run(blame_repo, "code", "blame", addr, "--json")
202 data = json.loads(r.output)
203 assert "exit_code" in data
204
205 def test_json_exit_code_zero_on_success(self, blame_repo: pathlib.Path) -> None:
206 addr = _blame_address(blame_repo)
207 r = _run(blame_repo, "code", "blame", addr, "--json")
208 assert r.exit_code == 0
209 data = json.loads(r.output)
210 assert data["exit_code"] == 0
211
212 def test_json_exit_code_is_int(self, blame_repo: pathlib.Path) -> None:
213 addr = _blame_address(blame_repo)
214 r = _run(blame_repo, "code", "blame", addr, "--json")
215 data = json.loads(r.output)
216 assert isinstance(data["exit_code"], int)
217
218 def test_j_alias_exit_code_present(self, blame_repo: pathlib.Path) -> None:
219 addr = _blame_address(blame_repo)
220 r = _run(blame_repo, "code", "blame", addr, "-j")
221 data = json.loads(r.output)
222 assert "exit_code" in data
223
224 def test_exit_code_mirrors_process_exit(self, blame_repo: pathlib.Path) -> None:
225 addr = _blame_address(blame_repo)
226 r = _run(blame_repo, "code", "blame", addr, "--json")
227 data = json.loads(r.output)
228 assert data["exit_code"] == r.exit_code
229
230 def test_exit_code_zero_with_no_events_found(self, blame_repo: pathlib.Path) -> None:
231 """Blame on a symbol with no recorded history exits 0 with empty events."""
232 r = _run(blame_repo, "code", "blame", "billing.py::nonexistent_fn", "--json")
233 assert r.exit_code == 0
234 data = json.loads(r.output)
235 assert data["exit_code"] == 0
236 assert data["events"] == []
237
238
239 # ---------------------------------------------------------------------------
240 # TestTypedDicts — _BlameResultJson carries the new fields
241 # ---------------------------------------------------------------------------
242
243
244 class TestTypedDicts:
245 """_BlameResultJson must gain exit_code/duration_ms annotations."""
246
247 def test_blame_result_json_exists(self) -> None:
248 from muse.cli.commands.blame import _BlameResultJson # noqa: F401
249
250 def test_blame_result_json_has_exit_code_annotation(self) -> None:
251 from muse.cli.commands.blame import _BlameResultJson
252 assert "exit_code" in _BlameResultJson.__annotations__
253
254 def test_blame_result_json_has_duration_ms_annotation(self) -> None:
255 from muse.cli.commands.blame import _BlameResultJson
256 assert "duration_ms" in _BlameResultJson.__annotations__
257
258 def test_blame_result_json_retains_events_annotation(self) -> None:
259 from muse.cli.commands.blame import _BlameResultJson
260 assert "events" in _BlameResultJson.__annotations__
261
262 def test_blame_result_json_retains_address_annotation(self) -> None:
263 from muse.cli.commands.blame import _BlameResultJson
264 assert "address" in _BlameResultJson.__annotations__
265
266 def test_blame_event_json_exists(self) -> None:
267 from muse.cli.commands.blame import _BlameEventJson # noqa: F401
268
269 def test_blame_event_json_has_commit_id(self) -> None:
270 from muse.cli.commands.blame import _BlameEventJson
271 assert "commit_id" in _BlameEventJson.__annotations__
272
273
274 # ---------------------------------------------------------------------------
275 # TestDocstrings — run() docstring documents new fields
276 # ---------------------------------------------------------------------------
277
278
279 class TestDocstrings:
280 """run() must document exit_code."""
281
282 def test_run_docstring_documents_fields(self) -> None:
283 from muse.cli.commands.blame import run
284 assert "Exit codes" in run.__doc__
285
286
287 # ---------------------------------------------------------------------------
288 # TestAnsiSanitization — no escape codes in JSON output
289 # ---------------------------------------------------------------------------
290
291
292 class TestAnsiSanitization:
293 """No ANSI escape sequences anywhere in the JSON output."""
294
295 def test_json_output_no_ansi(self, blame_repo: pathlib.Path) -> None:
296 addr = _blame_address(blame_repo)
297 r = _run(blame_repo, "code", "blame", addr, "--json")
298 assert "\x1b" not in r.output
299
300 def test_j_alias_output_no_ansi(self, blame_repo: pathlib.Path) -> None:
301 addr = _blame_address(blame_repo)
302 r = _run(blame_repo, "code", "blame", addr, "-j")
303 assert "\x1b" not in r.output
304
305 def test_json_address_field_no_ansi(self, blame_repo: pathlib.Path) -> None:
306 addr = _blame_address(blame_repo)
307 r = _run(blame_repo, "code", "blame", addr, "--json")
308 data = json.loads(r.output)
309 assert "\x1b" not in data["address"]
310
311
312 # ---------------------------------------------------------------------------
313 # TestPerformance — duration_ms under 1000 ms for a small repo
314 # ---------------------------------------------------------------------------
315
316
317 class TestPerformance:
318 """duration_ms must be non-negative and under 1000 ms for small repos."""
319
320 def test_json_duration_under_1000ms(self, blame_repo: pathlib.Path) -> None:
321 addr = _blame_address(blame_repo)
322 r = _run(blame_repo, "code", "blame", addr, "--json")
323 data = json.loads(r.output)
324 assert data["duration_ms"] < 1000
325
326 def test_j_alias_duration_under_1000ms(self, blame_repo: pathlib.Path) -> None:
327 addr = _blame_address(blame_repo)
328 r = _run(blame_repo, "code", "blame", addr, "-j")
329 data = json.loads(r.output)
330 assert data["duration_ms"] < 1000
331
332 def test_duration_ms_is_float_not_int(self, blame_repo: pathlib.Path) -> None:
333 addr = _blame_address(blame_repo)
334 r = _run(blame_repo, "code", "blame", addr, "--json")
335 data = json.loads(r.output)
336 assert isinstance(data["duration_ms"], float)
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 29 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago