gabriel / muse public
test_query_history_supercharge.py python
666 lines 28.3 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
1 """TDD supercharge tests for ``muse code query-history``.
2
3 Gaps being closed
4 -----------------
5 - ``-j`` alias for ``--json``
6 - ``exit_code`` and ``duration_ms`` in all three JSON envelopes
7 - ``truncated`` in introduced-only and removed-only JSON
8 - ``_QueryHistoryJson``, ``_IntroducedJson``, ``_RemovedJson`` TypedDicts
9 - Sanitize predicate values in human-readable output
10 - Unit tests for ``_SymbolHistory``, ``_sort_key_fn``, ``_collect_addresses``,
11 ``_RemovedSymbol``, ``_IntroducedSymbol``
12 - ``--sort last`` coverage
13 - ``stable=True`` correctness
14 - ``register()`` and ``run()`` docstring completeness
15
16 Test classes
17 ------------
18 TestJsonAlias -j alias works identically to --json
19 TestDefaultModeJson exit_code, duration_ms, schema in default mode
20 TestIntroducedJson exit_code, duration_ms, truncated in introduced-only
21 TestRemovedJson exit_code, duration_ms, truncated in removed-only
22 TestTypedDicts _QueryHistoryJson, _IntroducedJson, _RemovedJson
23 TestUnitSymbolHistory _SymbolHistory methods
24 TestUnitSortKeyFn _sort_key_fn behaviour
25 TestUnitDiffSymbols _RemovedSymbol, _IntroducedSymbol to_dict
26 TestCLIFilters --sort last, stable flag, --min-changes+--changed-only
27 TestCLISecurity null bytes / ANSI in predicates
28 TestDocstrings run(), register() doc completeness
29 """
30
31 from __future__ import annotations
32
33 import json
34 import pathlib
35 import textwrap
36 import typing
37
38 import pytest
39
40 from tests.cli_test_helper import CliRunner
41
42 cli = None
43 runner = CliRunner()
44
45
46 # ---------------------------------------------------------------------------
47 # Helpers
48 # ---------------------------------------------------------------------------
49
50
51 def _run(root: pathlib.Path, *args: str):
52 return runner.invoke(cli, list(args), env={"MUSE_REPO_ROOT": str(root)})
53
54
55 def _commit(root: pathlib.Path, msg: str = "commit") -> None:
56 r = _run(root, "code", "add", ".")
57 assert r.exit_code == 0, r.output
58 r2 = _run(root, "commit", "-m", msg)
59 assert r2.exit_code == 0, r2.output
60
61
62 # ---------------------------------------------------------------------------
63 # Fixture — repo with two commits so history is meaningful
64 # ---------------------------------------------------------------------------
65
66
67 @pytest.fixture
68 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
69 """Code-domain repo with two commits containing different function versions."""
70 monkeypatch.chdir(tmp_path)
71 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
72 r = _run(tmp_path, "init", "--domain", "code")
73 assert r.exit_code == 0, r.output
74
75 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
76 def compute_total(items: list[int]) -> int:
77 return sum(items)
78
79 def validate_amount(amount: float) -> bool:
80 return amount > 0
81 """))
82 _commit(tmp_path, "initial billing")
83
84 # Second commit: change compute_total, add new function, keep validate_amount
85 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
86 def compute_total(items: list[int]) -> int:
87 return sum(items) * 2 # changed
88
89 def validate_amount(amount: float) -> bool:
90 return amount > 0
91
92 def format_total(total: int) -> str:
93 return f"${total}"
94 """))
95 _commit(tmp_path, "update billing")
96 return tmp_path
97
98
99 # ---------------------------------------------------------------------------
100 # 1. -j alias
101 # ---------------------------------------------------------------------------
102
103
104 class TestJsonAlias:
105 def test_j_alias_exits_zero(self, repo: pathlib.Path) -> None:
106 r = _run(repo, "code", "query-history", "kind=function", "-j")
107 assert r.exit_code == 0, r.output
108
109 def test_j_alias_emits_valid_json(self, repo: pathlib.Path) -> None:
110 r = _run(repo, "code", "query-history", "kind=function", "-j")
111 assert r.exit_code == 0, r.output
112 data = json.loads(r.output.strip())
113 assert isinstance(data, dict)
114
115 def test_j_alias_has_results(self, repo: pathlib.Path) -> None:
116 r = _run(repo, "code", "query-history", "kind=function", "-j")
117 data = json.loads(r.output)
118 assert "results" in data
119
120 def test_j_alias_same_keys_as_json_flag(self, repo: pathlib.Path) -> None:
121 r1 = _run(repo, "code", "query-history", "kind=function", "--json")
122 r2 = _run(repo, "code", "query-history", "kind=function", "-j")
123 d1 = json.loads(r1.output)
124 d2 = json.loads(r2.output)
125 d1.pop("duration_ms", None)
126 d2.pop("duration_ms", None)
127 assert set(d1.keys()) == set(d2.keys())
128
129 def test_j_alias_result_count_matches(self, repo: pathlib.Path) -> None:
130 r1 = _run(repo, "code", "query-history", "kind=function", "--json")
131 r2 = _run(repo, "code", "query-history", "kind=function", "-j")
132 assert len(json.loads(r1.output)["results"]) == len(json.loads(r2.output)["results"])
133
134 def test_j_alias_introduced_mode(self, repo: pathlib.Path) -> None:
135 r = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "-j")
136 assert r.exit_code == 0, r.output
137 data = json.loads(r.output)
138 assert data["mode"] == "introduced-only"
139
140 def test_j_alias_removed_mode(self, repo: pathlib.Path) -> None:
141 r = _run(repo, "code", "query-history", "kind=function", "--removed-only", "-j")
142 assert r.exit_code == 0, r.output
143 data = json.loads(r.output)
144 assert data["mode"] == "removed-only"
145
146
147 # ---------------------------------------------------------------------------
148 # 2. Default-mode JSON schema: exit_code + duration_ms
149 # ---------------------------------------------------------------------------
150
151
152 class TestDefaultModeJson:
153 def test_has_exit_code(self, repo: pathlib.Path) -> None:
154 r = _run(repo, "code", "query-history", "kind=function", "--json")
155 data = json.loads(r.output)
156 assert "exit_code" in data
157
158 def test_exit_code_is_zero(self, repo: pathlib.Path) -> None:
159 r = _run(repo, "code", "query-history", "kind=function", "--json")
160 data = json.loads(r.output)
161 assert data["exit_code"] == 0
162
163 def test_has_duration_ms(self, repo: pathlib.Path) -> None:
164 r = _run(repo, "code", "query-history", "kind=function", "--json")
165 data = json.loads(r.output)
166 assert "duration_ms" in data
167
168 def test_duration_ms_positive(self, repo: pathlib.Path) -> None:
169 r = _run(repo, "code", "query-history", "kind=function", "--json")
170 data = json.loads(r.output)
171 assert isinstance(data["duration_ms"], float)
172 assert data["duration_ms"] > 0
173
174 def test_has_schema_version(self, repo: pathlib.Path) -> None:
175 r = _run(repo, "code", "query-history", "kind=function", "--json")
176 data = json.loads(r.output)
177 assert "schema_version" in data
178
179 def test_has_commits_scanned(self, repo: pathlib.Path) -> None:
180 r = _run(repo, "code", "query-history", "kind=function", "--json")
181 data = json.loads(r.output)
182 assert "commits_scanned" in data
183 assert isinstance(data["commits_scanned"], int)
184
185 def test_has_truncated(self, repo: pathlib.Path) -> None:
186 r = _run(repo, "code", "query-history", "kind=function", "--json")
187 data = json.loads(r.output)
188 assert "truncated" in data
189
190 def test_result_record_schema(self, repo: pathlib.Path) -> None:
191 r = _run(repo, "code", "query-history", "kind=function", "--json")
192 data = json.loads(r.output)
193 assert data["results"]
194 rec = data["results"][0]
195 required = {
196 "address", "kind", "language", "commit_count", "change_count",
197 "first_commit_id", "first_commit_id_short", "first_committed_at",
198 "last_commit_id", "last_commit_id_short", "last_committed_at", "stable",
199 }
200 assert required <= set(rec.keys())
201
202 def test_no_match_exit_code_zero(self, repo: pathlib.Path) -> None:
203 r = _run(repo, "code", "query-history", "name=zzz_nonexistent", "--json")
204 assert r.exit_code == 0
205 data = json.loads(r.output)
206 assert data["exit_code"] == 0
207 assert data["results"] == []
208
209 def test_no_match_has_duration_ms(self, repo: pathlib.Path) -> None:
210 r = _run(repo, "code", "query-history", "name=zzz_nonexistent", "--json")
211 data = json.loads(r.output)
212 assert "duration_ms" in data
213
214
215 # ---------------------------------------------------------------------------
216 # 3. introduced-only JSON: exit_code + duration_ms + truncated
217 # ---------------------------------------------------------------------------
218
219
220 class TestIntroducedJson:
221 def test_has_exit_code(self, repo: pathlib.Path) -> None:
222 r = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "--json")
223 data = json.loads(r.output)
224 assert "exit_code" in data
225
226 def test_exit_code_is_zero(self, repo: pathlib.Path) -> None:
227 r = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "--json")
228 data = json.loads(r.output)
229 assert data["exit_code"] == 0
230
231 def test_has_duration_ms(self, repo: pathlib.Path) -> None:
232 r = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "--json")
233 data = json.loads(r.output)
234 assert "duration_ms" in data
235
236 def test_duration_ms_positive(self, repo: pathlib.Path) -> None:
237 r = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "--json")
238 data = json.loads(r.output)
239 assert isinstance(data["duration_ms"], float)
240 assert data["duration_ms"] > 0
241
242 def test_has_truncated(self, repo: pathlib.Path) -> None:
243 r = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "--json")
244 data = json.loads(r.output)
245 assert "truncated" in data
246
247 def test_truncated_false_without_limit(self, repo: pathlib.Path) -> None:
248 r = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "--json")
249 data = json.loads(r.output)
250 assert data["truncated"] is False
251
252 def test_truncated_true_when_limited(self, repo: pathlib.Path) -> None:
253 # format_total was introduced in commit 2; limit=0 still gets it
254 # Check we have at least 1 introduced symbol first
255 r_all = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "--json")
256 total = len(json.loads(r_all.output)["results"])
257 if total <= 1:
258 pytest.skip("need >1 introduced symbols to test truncation")
259 r = _run(repo, "code", "query-history", "kind=function",
260 "--introduced-only", "--json", "--limit", "1")
261 data = json.loads(r.output)
262 assert data["truncated"] is True
263
264 def test_finds_format_total(self, repo: pathlib.Path) -> None:
265 r = _run(repo, "code", "query-history", "kind=function", "--introduced-only", "--json")
266 data = json.loads(r.output)
267 addrs = [res["address"] for res in data["results"]]
268 # format_total was added in the second commit — it's net-new
269 assert any("format_total" in a for a in addrs)
270
271
272 # ---------------------------------------------------------------------------
273 # 4. removed-only JSON: exit_code + duration_ms + truncated
274 # ---------------------------------------------------------------------------
275
276
277 class TestRemovedJson:
278 def test_has_exit_code(self, repo: pathlib.Path) -> None:
279 r = _run(repo, "code", "query-history", "kind=function", "--removed-only", "--json")
280 data = json.loads(r.output)
281 assert "exit_code" in data
282
283 def test_exit_code_is_zero(self, repo: pathlib.Path) -> None:
284 r = _run(repo, "code", "query-history", "kind=function", "--removed-only", "--json")
285 data = json.loads(r.output)
286 assert data["exit_code"] == 0
287
288 def test_has_duration_ms(self, repo: pathlib.Path) -> None:
289 r = _run(repo, "code", "query-history", "kind=function", "--removed-only", "--json")
290 data = json.loads(r.output)
291 assert "duration_ms" in data
292
293 def test_duration_ms_positive(self, repo: pathlib.Path) -> None:
294 r = _run(repo, "code", "query-history", "kind=function", "--removed-only", "--json")
295 data = json.loads(r.output)
296 assert isinstance(data["duration_ms"], float)
297 assert data["duration_ms"] > 0
298
299 def test_has_truncated(self, repo: pathlib.Path) -> None:
300 r = _run(repo, "code", "query-history", "kind=function", "--removed-only", "--json")
301 data = json.loads(r.output)
302 assert "truncated" in data
303
304 def test_truncated_false_without_limit(self, repo: pathlib.Path) -> None:
305 r = _run(repo, "code", "query-history", "kind=function", "--removed-only", "--json")
306 data = json.loads(r.output)
307 assert data["truncated"] is False
308
309
310 # ---------------------------------------------------------------------------
311 # 5. TypedDicts
312 # ---------------------------------------------------------------------------
313
314
315 class TestTypedDicts:
316 def test_query_history_json_importable(self) -> None:
317 from muse.cli.commands.query_history import _QueryHistoryJson
318 assert _QueryHistoryJson is not None
319
320 def test_query_history_json_has_exit_code(self) -> None:
321 from muse.cli.commands.query_history import _QueryHistoryJson
322 hints = typing.get_type_hints(_QueryHistoryJson)
323 assert "exit_code" in hints
324
325 def test_query_history_json_has_duration_ms(self) -> None:
326 from muse.cli.commands.query_history import _QueryHistoryJson
327 hints = typing.get_type_hints(_QueryHistoryJson)
328 assert "duration_ms" in hints
329
330 def test_query_history_json_has_schema_version(self) -> None:
331 from muse.cli.commands.query_history import _QueryHistoryJson
332 hints = typing.get_type_hints(_QueryHistoryJson)
333 assert "schema_version" in hints
334
335 def test_query_history_json_has_truncated(self) -> None:
336 from muse.cli.commands.query_history import _QueryHistoryJson
337 hints = typing.get_type_hints(_QueryHistoryJson)
338 assert "truncated" in hints
339
340 def test_query_history_json_has_results(self) -> None:
341 from muse.cli.commands.query_history import _QueryHistoryJson
342 hints = typing.get_type_hints(_QueryHistoryJson)
343 assert "results" in hints
344
345 def test_introduced_json_importable(self) -> None:
346 from muse.cli.commands.query_history import _IntroducedJson
347 assert _IntroducedJson is not None
348
349 def test_introduced_json_has_exit_code(self) -> None:
350 from muse.cli.commands.query_history import _IntroducedJson
351 hints = typing.get_type_hints(_IntroducedJson)
352 assert "exit_code" in hints
353
354 def test_introduced_json_has_truncated(self) -> None:
355 from muse.cli.commands.query_history import _IntroducedJson
356 hints = typing.get_type_hints(_IntroducedJson)
357 assert "truncated" in hints
358
359 def test_removed_json_importable(self) -> None:
360 from muse.cli.commands.query_history import _RemovedJson
361 assert _RemovedJson is not None
362
363 def test_removed_json_has_exit_code(self) -> None:
364 from muse.cli.commands.query_history import _RemovedJson
365 hints = typing.get_type_hints(_RemovedJson)
366 assert "exit_code" in hints
367
368 def test_removed_json_has_truncated(self) -> None:
369 from muse.cli.commands.query_history import _RemovedJson
370 hints = typing.get_type_hints(_RemovedJson)
371 assert "truncated" in hints
372
373
374 # ---------------------------------------------------------------------------
375 # 6. Unit — _SymbolHistory
376 # ---------------------------------------------------------------------------
377
378
379 class TestUnitSymbolHistory:
380 def test_initial_state(self) -> None:
381 from muse.cli.commands.query_history import _SymbolHistory
382 h = _SymbolHistory("billing.py::foo", "function", "Python")
383 assert h.commit_count == 0
384 assert h.change_count == 0
385 assert h.address == "billing.py::foo"
386
387 def test_record_increments_commit_count(self) -> None:
388 from muse.cli.commands.query_history import _SymbolHistory
389 h = _SymbolHistory("billing.py::foo", "function", "Python")
390 h.record("sha256:aaa", "2026-01-01T00:00:00+00:00", "cid1")
391 assert h.commit_count == 1
392
393 def test_record_tracks_first_and_last(self) -> None:
394 from muse.cli.commands.query_history import _SymbolHistory
395 h = _SymbolHistory("billing.py::foo", "function", "Python")
396 h.record("sha256:aaa", "2026-01-01T00:00:00+00:00", "cid1")
397 h.record("sha256:bbb", "2026-02-01T00:00:00+00:00", "cid2")
398 assert h.first_commit_id == "sha256:aaa"
399 assert h.last_commit_id == "sha256:bbb"
400
401 def test_change_count_same_body(self) -> None:
402 from muse.cli.commands.query_history import _SymbolHistory
403 h = _SymbolHistory("billing.py::foo", "function", "Python")
404 h.record("sha256:aaa", "2026-01-01T00:00:00+00:00", "cid1")
405 h.record("sha256:bbb", "2026-02-01T00:00:00+00:00", "cid1") # same cid
406 assert h.change_count == 1
407 assert h.commit_count == 2
408
409 def test_change_count_different_bodies(self) -> None:
410 from muse.cli.commands.query_history import _SymbolHistory
411 h = _SymbolHistory("billing.py::foo", "function", "Python")
412 h.record("sha256:aaa", "2026-01-01T00:00:00+00:00", "cid1")
413 h.record("sha256:bbb", "2026-02-01T00:00:00+00:00", "cid2")
414 assert h.change_count == 2
415
416 def test_stable_true_when_one_version(self) -> None:
417 from muse.cli.commands.query_history import _SymbolHistory
418 h = _SymbolHistory("billing.py::foo", "function", "Python")
419 h.record("sha256:aaa", "2026-01-01T00:00:00+00:00", "cid1")
420 d = h.to_dict()
421 assert d["stable"] is True
422
423 def test_stable_false_when_multiple_versions(self) -> None:
424 from muse.cli.commands.query_history import _SymbolHistory
425 h = _SymbolHistory("billing.py::foo", "function", "Python")
426 h.record("sha256:aaa", "2026-01-01T00:00:00+00:00", "cid1")
427 h.record("sha256:bbb", "2026-02-01T00:00:00+00:00", "cid2")
428 d = h.to_dict()
429 assert d["stable"] is False
430
431 def test_to_dict_schema(self) -> None:
432 from muse.cli.commands.query_history import _SymbolHistory
433 h = _SymbolHistory("billing.py::foo", "function", "Python")
434 h.record("sha256:aaa", "2026-01-01T00:00:00+00:00", "cid1")
435 d = h.to_dict()
436 required = {
437 "address", "kind", "language", "commit_count", "change_count",
438 "first_commit_id", "first_commit_id_short", "first_committed_at",
439 "last_commit_id", "last_commit_id_short", "last_committed_at", "stable",
440 }
441 assert required <= set(d.keys())
442
443 def test_first_committed_at_truncated_to_date(self) -> None:
444 from muse.cli.commands.query_history import _SymbolHistory
445 h = _SymbolHistory("billing.py::foo", "function", "Python")
446 h.record("sha256:aaa", "2026-04-18T12:34:56+00:00", "cid1")
447 d = h.to_dict()
448 assert d["first_committed_at"] == "2026-04-18"
449
450
451 # ---------------------------------------------------------------------------
452 # 7. Unit — _sort_key_fn
453 # ---------------------------------------------------------------------------
454
455
456 class TestUnitSortKeyFn:
457 def _make_history(self, address: str, commits: int, changes: int,
458 first: str, last: str) -> object:
459 from muse.cli.commands.query_history import _SymbolHistory
460 h = _SymbolHistory(address, "function", "Python")
461 # Simulate commit_count and change_count without real records
462 h.commit_count = commits
463 for i in range(changes):
464 h.content_ids.add(f"cid{i}")
465 h.first_committed_at = first
466 h.last_committed_at = last
467 h.first_commit_id = "sha256:aaa"
468 h.last_commit_id = "sha256:bbb"
469 return h
470
471 def test_sort_by_address(self) -> None:
472 from muse.cli.commands.query_history import _sort_key_fn
473 fn = _sort_key_fn("address")
474 h1 = self._make_history("z.py::foo", 1, 1, "2026-01-01", "2026-01-01")
475 h2 = self._make_history("a.py::bar", 1, 1, "2026-01-01", "2026-01-01")
476 assert fn(h2) < fn(h1) # type: ignore[arg-type]
477
478 def test_sort_by_commits_descending(self) -> None:
479 from muse.cli.commands.query_history import _sort_key_fn
480 fn = _sort_key_fn("commits")
481 h1 = self._make_history("a.py::foo", 10, 1, "2026-01-01", "2026-01-01")
482 h2 = self._make_history("b.py::bar", 1, 1, "2026-01-01", "2026-01-01")
483 assert fn(h1) < fn(h2) # more commits sorts first # type: ignore[arg-type]
484
485 def test_sort_by_changes_descending(self) -> None:
486 from muse.cli.commands.query_history import _sort_key_fn
487 fn = _sort_key_fn("changes")
488 h1 = self._make_history("a.py::foo", 1, 5, "2026-01-01", "2026-01-01")
489 h2 = self._make_history("b.py::bar", 1, 1, "2026-01-01", "2026-01-01")
490 assert fn(h1) < fn(h2) # more changes sorts first # type: ignore[arg-type]
491
492 def test_sort_by_first(self) -> None:
493 from muse.cli.commands.query_history import _sort_key_fn
494 fn = _sort_key_fn("first")
495 h1 = self._make_history("a.py::foo", 1, 1, "2026-01-01", "2026-06-01")
496 h2 = self._make_history("b.py::bar", 1, 1, "2026-03-01", "2026-06-01")
497 assert fn(h1) < fn(h2) # earlier first_committed_at sorts first # type: ignore[arg-type]
498
499 def test_sort_by_last(self) -> None:
500 from muse.cli.commands.query_history import _sort_key_fn
501 fn = _sort_key_fn("last")
502 h1 = self._make_history("a.py::foo", 1, 1, "2026-01-01", "2026-01-01")
503 h2 = self._make_history("b.py::bar", 1, 1, "2026-01-01", "2026-06-01")
504 assert fn(h1) < fn(h2) # earlier last_committed_at sorts first # type: ignore[arg-type]
505
506 def test_unknown_sort_falls_back_to_address(self) -> None:
507 from muse.cli.commands.query_history import _sort_key_fn
508 fn = _sort_key_fn("zzz_unknown")
509 h = self._make_history("billing.py::foo", 1, 1, "2026-01-01", "2026-01-01")
510 assert fn(h) == ("billing.py::foo",) # type: ignore[arg-type]
511
512
513 # ---------------------------------------------------------------------------
514 # 8. Unit — _RemovedSymbol / _IntroducedSymbol
515 # ---------------------------------------------------------------------------
516
517
518 class TestUnitDiffSymbols:
519 def _make_rec(self) -> dict:
520 return {
521 "kind": "function",
522 "name": "foo",
523 "qualified_name": "foo",
524 "lineno": 1,
525 "end_lineno": 3,
526 "content_id": "sha256:abc",
527 "body_hash": "sha256:abc",
528 "signature_id": "sha256:def",
529 }
530
531 def test_removed_to_dict_schema(self) -> None:
532 from muse.cli.commands.query_history import _RemovedSymbol
533 sym = _RemovedSymbol("billing.py::foo", self._make_rec(), "Python") # type: ignore[arg-type]
534 d = sym.to_dict()
535 assert d["address"] == "billing.py::foo"
536 assert d["status"] == "removed"
537 assert d["kind"] == "function"
538 assert d["language"] == "Python"
539
540 def test_introduced_to_dict_schema(self) -> None:
541 from muse.cli.commands.query_history import _IntroducedSymbol
542 sym = _IntroducedSymbol("billing.py::bar", self._make_rec(), "Python") # type: ignore[arg-type]
543 d = sym.to_dict()
544 assert d["address"] == "billing.py::bar"
545 assert d["status"] == "introduced"
546 assert d["kind"] == "function"
547 assert d["language"] == "Python"
548
549
550 # ---------------------------------------------------------------------------
551 # 9. CLI filters — stable, --sort last, interaction tests
552 # ---------------------------------------------------------------------------
553
554
555 class TestCLIFilters:
556 def test_validate_amount_is_stable(self, repo: pathlib.Path) -> None:
557 # validate_amount never changed — should be stable=True
558 r = _run(repo, "code", "query-history", "name=validate_amount", "--json")
559 data = json.loads(r.output)
560 assert data["results"]
561 rec = data["results"][0]
562 assert rec["stable"] is True
563 assert rec["change_count"] == 1
564
565 def test_compute_total_is_not_stable(self, repo: pathlib.Path) -> None:
566 # compute_total changed between commits — stable=False
567 r = _run(repo, "code", "query-history", "name=compute_total", "--json")
568 data = json.loads(r.output)
569 assert data["results"]
570 rec = data["results"][0]
571 assert rec["stable"] is False
572 assert rec["change_count"] >= 2
573
574 def test_sort_last(self, repo: pathlib.Path) -> None:
575 r = _run(repo, "code", "query-history", "kind=function", "--sort", "last", "--json")
576 assert r.exit_code == 0, r.output
577 data = json.loads(r.output)
578 dates = [rec["last_committed_at"] for rec in data["results"]]
579 assert dates == sorted(dates)
580
581 def test_changed_only_plus_min_changes_interaction(self, repo: pathlib.Path) -> None:
582 # --changed-only (>1) with --min-changes 2 should be consistent
583 r = _run(repo, "code", "query-history", "kind=function",
584 "--changed-only", "--min-changes", "2", "--json")
585 assert r.exit_code == 0, r.output
586 data = json.loads(r.output)
587 for rec in data["results"]:
588 assert rec["change_count"] >= 2
589
590 def test_introduced_finds_format_total(self, repo: pathlib.Path) -> None:
591 r = _run(repo, "code", "query-history", "name=format_total",
592 "--introduced-only", "--json")
593 data = json.loads(r.output)
594 assert any("format_total" in res["address"] for res in data["results"])
595
596 def test_removed_empty_when_nothing_removed(self, repo: pathlib.Path) -> None:
597 r = _run(repo, "code", "query-history", "kind=function",
598 "--removed-only", "--json")
599 data = json.loads(r.output)
600 # validate_amount and compute_total both present in both commits — no removals
601 assert data["exit_code"] == 0
602 removed_addrs = [res["address"] for res in data["results"]]
603 assert not any("validate_amount" in a for a in removed_addrs)
604
605 def test_limit_applied_to_introduced(self, repo: pathlib.Path) -> None:
606 r = _run(repo, "code", "query-history", "kind=function",
607 "--introduced-only", "--json", "--limit", "1")
608 data = json.loads(r.output)
609 assert len(data["results"]) <= 1
610
611
612 # ---------------------------------------------------------------------------
613 # 10. Security
614 # ---------------------------------------------------------------------------
615
616
617 class TestCLISecurity:
618 def test_null_byte_in_predicate_not_in_output(self, repo: pathlib.Path) -> None:
619 r = _run(repo, "code", "query-history", "name=\x00evil")
620 assert "\x00" not in r.output
621
622 def test_ansi_not_in_json_output(self, repo: pathlib.Path) -> None:
623 r = _run(repo, "code", "query-history", "kind=function", "--json")
624 assert "\x1b" not in r.output
625
626 def test_ansi_not_in_introduced_json(self, repo: pathlib.Path) -> None:
627 r = _run(repo, "code", "query-history", "kind=function",
628 "--introduced-only", "--json")
629 assert "\x1b" not in r.output
630
631
632 # ---------------------------------------------------------------------------
633 # 11. Docstrings
634 # ---------------------------------------------------------------------------
635
636
637 class TestDocstrings:
638 def test_run_docstring_exists(self) -> None:
639 from muse.cli.commands.query_history import run
640 assert run.__doc__ is not None
641 assert len(run.__doc__) > 50
642
643 def test_run_docstring_mentions_json(self) -> None:
644 from muse.cli.commands.query_history import run
645 assert "json" in (run.__doc__ or "").lower()
646
647 def test_run_docstring_mentions_exit_code(self) -> None:
648 from muse.cli.commands.query_history import run
649 assert "exit_code" in (run.__doc__ or "")
650
651 def test_run_docstring_mentions_duration_ms(self) -> None:
652 from muse.cli.commands.query_history import run
653 assert "duration_ms" in (run.__doc__ or "")
654
655 def test_register_docstring_exists(self) -> None:
656 from muse.cli.commands.query_history import register
657 assert register.__doc__ is not None
658 assert len(register.__doc__) > 50
659
660 def test_symbol_history_docstring_exists(self) -> None:
661 from muse.cli.commands.query_history import _SymbolHistory
662 assert _SymbolHistory.__doc__ is not None
663
664 def test_sort_key_fn_docstring_exists(self) -> None:
665 from muse.cli.commands.query_history import _sort_key_fn
666 assert _sort_key_fn.__doc__ is not None
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago