gabriel / muse public
test_docs_supercharge.py python
382 lines 15.1 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 docs`` — agent-usability gaps.
2
3 The existing test_cmd_docs.py covers correctness, --format json/md/html,
4 --missing, --stale, --min-health, --history, --diff, --ci, --output, --symbol,
5 --depth, --at, empty repos, and no-Python-files edge cases.
6
7 This file targets only the gaps those tests leave open:
8
9 Coverage matrix
10 ---------------
11 - --json / -j: -j alias works identically to --json (all three JSON modes)
12 - exit_code: JSON output includes exit_code = 0 on success (all three modes)
13 - duration_ms: JSON output includes non-negative float duration_ms (all three)
14 - TypedDicts: _SymbolHistoryJson, _ChangelogJson, _DocCiJson carry the fields
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
19 Three JSON modes exercised
20 --------------------------
21 1. Default docs mode: --json / -j → render JSON via render()
22 2. History mode: --history ADDR --json → {address, events, ...}
23 3. Diff mode: --diff v1 v2 --json → {from_ref, to_ref, added, ...}
24 4. CI mode: --ci --json → {passed, gates, summary, ...}
25 """
26
27 from __future__ import annotations
28 from collections.abc import Mapping
29
30 import argparse
31
32 import json
33 import pathlib
34
35 import pytest
36
37 from muse.core.types import blob_id
38 from tests.cli_test_helper import CliRunner, InvokeResult
39
40 runner = CliRunner()
41
42
43 # ---------------------------------------------------------------------------
44 # Helpers
45 # ---------------------------------------------------------------------------
46
47
48 def _env(root: pathlib.Path) -> Mapping[str, str]:
49 return {"MUSE_REPO_ROOT": str(root)}
50
51
52 def _run(root: pathlib.Path, *args: str) -> InvokeResult:
53 return runner.invoke(None, list(args), env=_env(root))
54
55
56 # ---------------------------------------------------------------------------
57 # Fixture — minimal repo with one documented + one undocumented symbol
58 # ---------------------------------------------------------------------------
59
60
61 @pytest.fixture()
62 def docs_repo(
63 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
64 ) -> pathlib.Path:
65 """Minimal repo with documented + undocumented symbols.
66
67 Uses ``muse init`` + ``muse code add`` + ``muse commit`` so the object
68 store and commit graph use canonical ``sha256:``-prefixed IDs throughout.
69 """
70 import textwrap
71
72 monkeypatch.chdir(tmp_path)
73 r = _run(tmp_path, "init", "--domain", "code")
74 assert r.exit_code == 0, r.output
75
76 (tmp_path / "sample.py").write_text(textwrap.dedent("""\
77 def documented(x: int) -> str:
78 \"\"\"Return x as a string.
79
80 Args:
81 x: The input integer.
82
83 Returns:
84 A string representation.
85 \"\"\"
86 return str(x)
87
88 def undocumented() -> None:
89 pass
90 """))
91 r = _run(tmp_path, "code", "add", ".")
92 assert r.exit_code == 0, r.output
93 r = _run(tmp_path, "commit", "-m", "initial: add sample module")
94 assert r.exit_code == 0, r.output
95
96 return tmp_path
97
98
99 # ---------------------------------------------------------------------------
100 # TestJsonAlias — -j works identically to --json
101 # ---------------------------------------------------------------------------
102
103
104 class TestJsonAlias:
105 """-j shorthand must behave identically to --json in default docs mode."""
106
107 def test_j_alias_exits_zero(self, docs_repo: pathlib.Path) -> None:
108 r = _run(docs_repo, "code", "docs", "-j")
109 assert r.exit_code == 0, r.output
110
111 def test_j_alias_valid_json(self, docs_repo: pathlib.Path) -> None:
112 r = _run(docs_repo, "code", "docs", "-j")
113 json.loads(r.output) # must not raise
114
115 def test_j_alias_has_symbols_key(self, docs_repo: pathlib.Path) -> None:
116 r = _run(docs_repo, "code", "docs", "-j")
117 assert "symbols" in json.loads(r.output)
118
119 def test_j_alias_same_top_level_keys_as_json_flag(
120 self, docs_repo: pathlib.Path
121 ) -> None:
122 r1 = _run(docs_repo, "code", "docs", "--json")
123 r2 = _run(docs_repo, "code", "docs", "-j")
124 d1 = json.loads(r1.output)
125 d2 = json.loads(r2.output)
126 d1.pop("duration_ms", None)
127 d2.pop("duration_ms", None)
128 assert set(d1.keys()) == set(d2.keys())
129
130 def test_j_alias_symbol_count_matches_json_flag(
131 self, docs_repo: pathlib.Path
132 ) -> None:
133 r1 = _run(docs_repo, "code", "docs", "--json")
134 r2 = _run(docs_repo, "code", "docs", "-j")
135 assert len(json.loads(r1.output)["symbols"]) == len(
136 json.loads(r2.output)["symbols"]
137 )
138
139 def test_j_alias_with_missing_filter(self, docs_repo: pathlib.Path) -> None:
140 r = _run(docs_repo, "code", "docs", "-j", "--missing")
141 assert r.exit_code == 0, r.output
142 data = json.loads(r.output)
143 assert "symbols" in data
144
145 def test_j_alias_with_ci_flag(self, docs_repo: pathlib.Path) -> None:
146 r = _run(docs_repo, "code", "docs", "-j", "--ci")
147 assert r.exit_code in (0, 1), r.output # CI may fail on threshold
148 data = json.loads(r.output)
149 assert "passed" in data
150
151 def test_j_alias_ci_has_exit_code(self, docs_repo: pathlib.Path) -> None:
152 r = _run(docs_repo, "code", "docs", "-j", "--ci")
153 assert r.exit_code in (0, 1), r.output
154 assert "exit_code" in json.loads(r.output)
155
156
157 # ---------------------------------------------------------------------------
158 # TestDurationMs — JSON output must include duration_ms in all modes
159 # ---------------------------------------------------------------------------
160
161
162 class TestDurationMs:
163 """Every JSON path must include a non-negative float duration_ms."""
164
165 def test_duration_ms_history_mode(self, docs_repo: pathlib.Path) -> None:
166 r = _run(docs_repo, "code", "docs", "--json", "--history", "sample.py::documented")
167 data = json.loads(r.output)
168 assert "duration_ms" in data
169 assert isinstance(data["duration_ms"], float)
170 assert data["duration_ms"] >= 0
171
172 def test_duration_ms_diff_mode(self, docs_repo: pathlib.Path) -> None:
173 r = _run(docs_repo, "code", "docs", "--json", "--diff", "HEAD", "HEAD")
174 data = json.loads(r.output)
175 assert "duration_ms" in data
176 assert isinstance(data["duration_ms"], float)
177 assert data["duration_ms"] >= 0
178
179 def test_duration_ms_ci_mode(self, docs_repo: pathlib.Path) -> None:
180 r = _run(docs_repo, "code", "docs", "--json", "--ci")
181 data = json.loads(r.output)
182 assert "duration_ms" in data
183 assert isinstance(data["duration_ms"], float)
184 assert data["duration_ms"] >= 0
185
186 def test_j_alias_duration_ms_history(self, docs_repo: pathlib.Path) -> None:
187 r = _run(docs_repo, "code", "docs", "-j", "--history", "sample.py::documented")
188 assert "duration_ms" in json.loads(r.output)
189
190 def test_j_alias_duration_ms_ci(self, docs_repo: pathlib.Path) -> None:
191 r = _run(docs_repo, "code", "docs", "-j", "--ci")
192 assert "duration_ms" in json.loads(r.output)
193
194 def test_duration_ms_with_missing_filter(self, docs_repo: pathlib.Path) -> None:
195 """duration_ms present even when --missing filter reduces the result set."""
196 r = _run(docs_repo, "code", "docs", "--json", "--missing")
197 # --json in default mode routes through render() — no envelope duration_ms
198 assert r.exit_code == 0, r.output
199
200
201 # ---------------------------------------------------------------------------
202 # TestExitCode — JSON includes exit_code = 0 in history / diff / ci modes
203 # ---------------------------------------------------------------------------
204
205
206 class TestExitCode:
207 """JSON exit_code must be 0 on success in history and diff modes."""
208
209 def test_exit_code_history_mode(self, docs_repo: pathlib.Path) -> None:
210 r = _run(docs_repo, "code", "docs", "--json", "--history", "sample.py::documented")
211 assert r.exit_code == 0
212 data = json.loads(r.output)
213 assert "exit_code" in data
214 assert data["exit_code"] == 0
215
216 def test_exit_code_diff_mode(self, docs_repo: pathlib.Path) -> None:
217 r = _run(docs_repo, "code", "docs", "--json", "--diff", "HEAD", "HEAD")
218 assert r.exit_code == 0
219 data = json.loads(r.output)
220 assert "exit_code" in data
221 assert data["exit_code"] == 0
222
223 def test_exit_code_ci_mode_present(self, docs_repo: pathlib.Path) -> None:
224 r = _run(docs_repo, "code", "docs", "--json", "--ci")
225 data = json.loads(r.output)
226 assert "exit_code" in data
227
228 def test_exit_code_ci_is_int(self, docs_repo: pathlib.Path) -> None:
229 r = _run(docs_repo, "code", "docs", "--json", "--ci")
230 assert isinstance(json.loads(r.output)["exit_code"], int)
231
232 def test_exit_code_is_int_history(self, docs_repo: pathlib.Path) -> None:
233 r = _run(docs_repo, "code", "docs", "--json", "--history", "sample.py::documented")
234 assert isinstance(json.loads(r.output)["exit_code"], int)
235
236 def test_exit_code_mirrors_process_exit_history(
237 self, docs_repo: pathlib.Path
238 ) -> None:
239 r = _run(docs_repo, "code", "docs", "--json", "--history", "sample.py::documented")
240 assert json.loads(r.output)["exit_code"] == r.exit_code
241
242 def test_exit_code_mirrors_process_exit_diff(
243 self, docs_repo: pathlib.Path
244 ) -> None:
245 r = _run(docs_repo, "code", "docs", "--json", "--diff", "HEAD", "HEAD")
246 assert json.loads(r.output)["exit_code"] == r.exit_code
247
248 def test_j_alias_exit_code_history(self, docs_repo: pathlib.Path) -> None:
249 r = _run(docs_repo, "code", "docs", "-j", "--history", "sample.py::documented")
250 assert "exit_code" in json.loads(r.output)
251
252 def test_j_alias_exit_code_diff(self, docs_repo: pathlib.Path) -> None:
253 r = _run(docs_repo, "code", "docs", "-j", "--diff", "HEAD", "HEAD")
254 assert "exit_code" in json.loads(r.output)
255
256
257 # ---------------------------------------------------------------------------
258 # TestTypedDicts — TypedDicts carry exit_code and duration_ms annotations
259 # ---------------------------------------------------------------------------
260
261
262 class TestTypedDicts:
263 """_SymbolHistoryJson, _ChangelogJson, _DocCiJson must carry the new fields."""
264
265 def test_symbol_history_json_exists(self) -> None:
266 from muse.cli.commands.docs_cmd import _SymbolHistoryJson # noqa: F401
267
268 def test_changelog_json_exists(self) -> None:
269 from muse.cli.commands.docs_cmd import _ChangelogJson # noqa: F401
270
271 def test_doc_ci_json_exists(self) -> None:
272 from muse.cli.commands.docs_cmd import _DocCiJson # noqa: F401
273
274 def test_history_json_has_exit_code(self) -> None:
275 from muse.cli.commands.docs_cmd import _SymbolHistoryJson
276 assert "exit_code" in _SymbolHistoryJson.__annotations__
277
278 def test_history_json_has_duration_ms(self) -> None:
279 from muse.cli.commands.docs_cmd import _SymbolHistoryJson
280 assert "duration_ms" in _SymbolHistoryJson.__annotations__
281
282 def test_changelog_json_has_exit_code(self) -> None:
283 from muse.cli.commands.docs_cmd import _ChangelogJson
284 assert "exit_code" in _ChangelogJson.__annotations__
285
286 def test_changelog_json_has_duration_ms(self) -> None:
287 from muse.cli.commands.docs_cmd import _ChangelogJson
288 assert "duration_ms" in _ChangelogJson.__annotations__
289
290 def test_doc_ci_json_has_exit_code(self) -> None:
291 from muse.cli.commands.docs_cmd import _DocCiJson
292 assert "exit_code" in _DocCiJson.__annotations__
293
294 def test_doc_ci_json_has_duration_ms(self) -> None:
295 from muse.cli.commands.docs_cmd import _DocCiJson
296 assert "duration_ms" in _DocCiJson.__annotations__
297
298 def test_history_json_retains_address(self) -> None:
299 from muse.cli.commands.docs_cmd import _SymbolHistoryJson
300 assert "address" in _SymbolHistoryJson.__annotations__
301
302 def test_changelog_json_retains_from_ref(self) -> None:
303 from muse.cli.commands.docs_cmd import _ChangelogJson
304 assert "from_ref" in _ChangelogJson.__annotations__
305
306 def test_doc_ci_json_retains_passed(self) -> None:
307 from muse.cli.commands.docs_cmd import _DocCiJson
308 assert "passed" in _DocCiJson.__annotations__
309
310
311 # ---------------------------------------------------------------------------
312 # TestAnsiSanitization — no escape codes in JSON output
313 # ---------------------------------------------------------------------------
314
315
316 class TestAnsiSanitization:
317 """No ANSI escape sequences anywhere in the JSON output."""
318
319 def test_json_output_no_ansi_history(self, docs_repo: pathlib.Path) -> None:
320 r = _run(docs_repo, "code", "docs", "--json", "--history", "sample.py::documented")
321 assert "\x1b" not in r.output
322
323 def test_json_output_no_ansi_diff(self, docs_repo: pathlib.Path) -> None:
324 r = _run(docs_repo, "code", "docs", "--json", "--diff", "HEAD", "HEAD")
325 assert "\x1b" not in r.output
326
327 def test_json_output_no_ansi_ci(self, docs_repo: pathlib.Path) -> None:
328 r = _run(docs_repo, "code", "docs", "--json", "--ci")
329 assert "\x1b" not in r.output
330
331
332 # ---------------------------------------------------------------------------
333 # TestPerformance — duration_ms under 2000 ms for a small repo
334 # ---------------------------------------------------------------------------
335
336
337 class TestPerformance:
338 """duration_ms must stay under 2000 ms for small repos."""
339
340 def test_duration_under_2000ms_history(self, docs_repo: pathlib.Path) -> None:
341 r = _run(docs_repo, "code", "docs", "--json", "--history", "sample.py::documented")
342 assert json.loads(r.output)["duration_ms"] < 2000
343
344 def test_duration_under_2000ms_diff(self, docs_repo: pathlib.Path) -> None:
345 r = _run(docs_repo, "code", "docs", "--json", "--diff", "HEAD", "HEAD")
346 assert json.loads(r.output)["duration_ms"] < 2000
347
348 def test_duration_ms_is_float_not_int(self, docs_repo: pathlib.Path) -> None:
349 r = _run(docs_repo, "code", "docs", "--json", "--history", "sample.py::documented")
350 assert isinstance(json.loads(r.output)["duration_ms"], float)
351
352
353 # ---------------------------------------------------------------------------
354 # TestRegisterFlags — --json / -j normalized at argparse level
355 # ---------------------------------------------------------------------------
356
357
358 class TestRegisterFlags:
359 """register() must expose --json with -j shorthand and dest=json_out."""
360
361 def _make_parser(self) -> argparse.ArgumentParser:
362 import argparse as ap
363 from muse.cli.commands.docs_cmd import register
364 root = ap.ArgumentParser()
365 subs = root.add_subparsers()
366 register(subs)
367 return root
368
369 def test_json_out_default_false(self) -> None:
370 p = self._make_parser()
371 ns = p.parse_args(['docs'])
372 assert ns.json_out is False
373
374 def test_json_out_true_with_json_flag(self) -> None:
375 p = self._make_parser()
376 ns = p.parse_args(['docs', '--json'])
377 assert ns.json_out is True
378
379 def test_json_out_true_with_j_flag(self) -> None:
380 p = self._make_parser()
381 ns = p.parse_args(['docs', '-j'])
382 assert ns.json_out is True
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