gabriel / muse public
test_index_supercharge.py python
330 lines 13.5 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
1 """Supercharge tests for ``muse code index`` — agent-usability gaps.
2
3 No prior supercharge tests existed for ``muse code index``. This file covers
4 all three sub-commands (status, rebuild, purge).
5
6 Coverage matrix
7 ---------------
8 - -j alias: already present on all three sub-commands (no change needed)
9 - exit_code: JSON output includes exit_code = 0 on success (all three)
10 - duration_ms: JSON output includes non-negative float duration_ms (all three)
11 - status shape: status --json now wraps indexes in an object (schema change)
12 - TypedDicts: _RebuildResult, _StatusResult, _PurgeResult all carry
13 exit_code and duration_ms annotations
14 - Docstrings: run_status, run_rebuild, run_purge docstrings mention
15 exit_code and duration_ms
16 - ANSI: JSON output never contains terminal escape sequences
17 - Performance: duration_ms present and is a float
18 """
19
20 from __future__ import annotations
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) -> dict[str, str]:
39 return {"MUSE_REPO_ROOT": str(root)}
40
41
42 def _run(root: pathlib.Path, *args: str):
43 return runner.invoke(None, list(args), env=_env(root))
44
45
46 # ---------------------------------------------------------------------------
47 # Fixture — minimal repo
48 # ---------------------------------------------------------------------------
49
50
51 @pytest.fixture()
52 def index_repo(
53 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
54 ) -> pathlib.Path:
55 """Minimal repo with one Python file — enough to build real indexes."""
56 monkeypatch.chdir(tmp_path)
57 r = _run(tmp_path, "init", "--domain", "code")
58 assert r.exit_code == 0, r.output
59
60 (tmp_path / "core.py").write_text(textwrap.dedent("""\
61 def compute(x):
62 return x * 2
63
64 def validate(x):
65 return x > 0
66 """))
67 r = _run(tmp_path, "code", "add", ".")
68 assert r.exit_code == 0, r.output
69 r = _run(tmp_path, "commit", "-m", "seed index repo")
70 assert r.exit_code == 0, r.output
71
72 return tmp_path
73
74
75 # ---------------------------------------------------------------------------
76 # TestStatusJson — status --json envelope shape
77 # ---------------------------------------------------------------------------
78
79
80 class TestStatusJson:
81 """status --json must emit a wrapper object with indexes, exit_code, duration_ms."""
82
83 def test_status_j_exits_zero(self, index_repo: pathlib.Path) -> None:
84 r = _run(index_repo, "code", "index", "status", "-j")
85 assert r.exit_code == 0, r.output
86
87 def test_status_j_valid_json(self, index_repo: pathlib.Path) -> None:
88 r = _run(index_repo, "code", "index", "status", "-j")
89 json.loads(r.output) # must not raise
90
91 def test_status_j_has_indexes_key(self, index_repo: pathlib.Path) -> None:
92 r = _run(index_repo, "code", "index", "status", "-j")
93 assert "indexes" in json.loads(r.output)
94
95 def test_status_j_indexes_is_list(self, index_repo: pathlib.Path) -> None:
96 r = _run(index_repo, "code", "index", "status", "-j")
97 assert isinstance(json.loads(r.output)["indexes"], list)
98
99 def test_status_j_has_exit_code(self, index_repo: pathlib.Path) -> None:
100 r = _run(index_repo, "code", "index", "status", "-j")
101 assert "exit_code" in json.loads(r.output)
102
103 def test_status_j_exit_code_zero(self, index_repo: pathlib.Path) -> None:
104 r = _run(index_repo, "code", "index", "status", "-j")
105 assert json.loads(r.output)["exit_code"] == 0
106
107 def test_status_j_has_duration_ms(self, index_repo: pathlib.Path) -> None:
108 r = _run(index_repo, "code", "index", "status", "-j")
109 assert "duration_ms" in json.loads(r.output)
110
111 def test_status_j_duration_ms_is_float(self, index_repo: pathlib.Path) -> None:
112 r = _run(index_repo, "code", "index", "status", "-j")
113 assert isinstance(json.loads(r.output)["duration_ms"], float)
114
115 def test_status_j_duration_ms_nonnegative(self, index_repo: pathlib.Path) -> None:
116 r = _run(index_repo, "code", "index", "status", "-j")
117 assert json.loads(r.output)["duration_ms"] >= 0
118
119 def test_status_j_index_entries_have_name(self, index_repo: pathlib.Path) -> None:
120 r = _run(index_repo, "code", "index", "status", "-j")
121 for entry in json.loads(r.output)["indexes"]:
122 assert "name" in entry
123
124 def test_status_j_index_entries_have_status(self, index_repo: pathlib.Path) -> None:
125 r = _run(index_repo, "code", "index", "status", "-j")
126 for entry in json.loads(r.output)["indexes"]:
127 assert "status" in entry
128
129 def test_status_j_no_ansi(self, index_repo: pathlib.Path) -> None:
130 r = _run(index_repo, "code", "index", "status", "-j")
131 assert "\x1b" not in r.output
132
133 def test_status_json_flag_same_as_j(self, index_repo: pathlib.Path) -> None:
134 r1 = _run(index_repo, "code", "index", "status", "--json")
135 r2 = _run(index_repo, "code", "index", "status", "-j")
136 d1 = json.loads(r1.output)
137 d2 = json.loads(r2.output)
138 d1.pop("duration_ms", None)
139 d2.pop("duration_ms", None)
140 assert d1 == d2
141
142
143 # ---------------------------------------------------------------------------
144 # TestRebuildJson — rebuild --json envelope
145 # ---------------------------------------------------------------------------
146
147
148 class TestRebuildJson:
149 """rebuild --json must include exit_code and duration_ms."""
150
151 def test_rebuild_j_exits_zero(self, index_repo: pathlib.Path) -> None:
152 r = _run(index_repo, "code", "index", "rebuild", "-j")
153 assert r.exit_code == 0, r.output
154
155 def test_rebuild_j_valid_json(self, index_repo: pathlib.Path) -> None:
156 r = _run(index_repo, "code", "index", "rebuild", "-j")
157 json.loads(r.output) # must not raise
158
159 def test_rebuild_j_has_exit_code(self, index_repo: pathlib.Path) -> None:
160 r = _run(index_repo, "code", "index", "rebuild", "-j")
161 assert "exit_code" in json.loads(r.output)
162
163 def test_rebuild_j_exit_code_zero(self, index_repo: pathlib.Path) -> None:
164 r = _run(index_repo, "code", "index", "rebuild", "-j")
165 assert json.loads(r.output)["exit_code"] == 0
166
167 def test_rebuild_j_has_duration_ms(self, index_repo: pathlib.Path) -> None:
168 r = _run(index_repo, "code", "index", "rebuild", "-j")
169 assert "duration_ms" in json.loads(r.output)
170
171 def test_rebuild_j_duration_ms_is_float(self, index_repo: pathlib.Path) -> None:
172 r = _run(index_repo, "code", "index", "rebuild", "-j")
173 assert isinstance(json.loads(r.output)["duration_ms"], float)
174
175 def test_rebuild_j_has_rebuilt_key(self, index_repo: pathlib.Path) -> None:
176 r = _run(index_repo, "code", "index", "rebuild", "-j")
177 assert "rebuilt" in json.loads(r.output)
178
179 def test_rebuild_dry_run_j_exit_code_zero(self, index_repo: pathlib.Path) -> None:
180 r = _run(index_repo, "code", "index", "rebuild", "--dry-run", "-j")
181 assert r.exit_code == 0, r.output
182 assert json.loads(r.output)["exit_code"] == 0
183
184 def test_rebuild_dry_run_j_duration_ms(self, index_repo: pathlib.Path) -> None:
185 r = _run(index_repo, "code", "index", "rebuild", "--dry-run", "-j")
186 data = json.loads(r.output)
187 assert "duration_ms" in data
188 assert isinstance(data["duration_ms"], float)
189
190 def test_rebuild_j_no_ansi(self, index_repo: pathlib.Path) -> None:
191 r = _run(index_repo, "code", "index", "rebuild", "-j")
192 assert "\x1b" not in r.output
193
194
195 # ---------------------------------------------------------------------------
196 # TestPurgeJson — purge --json envelope
197 # ---------------------------------------------------------------------------
198
199
200 class TestPurgeJson:
201 """purge --json must include exit_code and duration_ms."""
202
203 def test_purge_j_exits_zero(self, index_repo: pathlib.Path) -> None:
204 r = _run(index_repo, "code", "index", "purge", "-j")
205 assert r.exit_code == 0, r.output
206
207 def test_purge_j_valid_json(self, index_repo: pathlib.Path) -> None:
208 r = _run(index_repo, "code", "index", "purge", "-j")
209 json.loads(r.output) # must not raise
210
211 def test_purge_j_has_exit_code(self, index_repo: pathlib.Path) -> None:
212 r = _run(index_repo, "code", "index", "purge", "-j")
213 assert "exit_code" in json.loads(r.output)
214
215 def test_purge_j_exit_code_zero(self, index_repo: pathlib.Path) -> None:
216 r = _run(index_repo, "code", "index", "purge", "-j")
217 assert json.loads(r.output)["exit_code"] == 0
218
219 def test_purge_j_has_duration_ms(self, index_repo: pathlib.Path) -> None:
220 r = _run(index_repo, "code", "index", "purge", "-j")
221 assert "duration_ms" in json.loads(r.output)
222
223 def test_purge_j_duration_ms_is_float(self, index_repo: pathlib.Path) -> None:
224 r = _run(index_repo, "code", "index", "purge", "-j")
225 assert isinstance(json.loads(r.output)["duration_ms"], float)
226
227 def test_purge_j_has_purged_key(self, index_repo: pathlib.Path) -> None:
228 r = _run(index_repo, "code", "index", "purge", "-j")
229 assert "purged" in json.loads(r.output)
230
231 def test_purge_j_has_skipped_key(self, index_repo: pathlib.Path) -> None:
232 r = _run(index_repo, "code", "index", "purge", "-j")
233 assert "skipped" in json.loads(r.output)
234
235 def test_purge_j_no_ansi(self, index_repo: pathlib.Path) -> None:
236 r = _run(index_repo, "code", "index", "purge", "-j")
237 assert "\x1b" not in r.output
238
239 def test_purge_absent_indexes_skipped_not_error(self, index_repo: pathlib.Path) -> None:
240 """Purging an already-absent index exits 0 and lists it as skipped."""
241 # Purge twice — second time all indexes are absent
242 _run(index_repo, "code", "index", "purge", "-j")
243 r = _run(index_repo, "code", "index", "purge", "-j")
244 assert r.exit_code == 0
245 data = json.loads(r.output)
246 assert data["exit_code"] == 0
247 assert data["purged"] == []
248 assert len(data["skipped"]) > 0
249
250
251 # ---------------------------------------------------------------------------
252 # TestTypedDicts — TypedDicts carry exit_code and duration_ms
253 # ---------------------------------------------------------------------------
254
255
256 class TestTypedDicts:
257 """All three TypedDicts must carry exit_code and duration_ms annotations."""
258
259 def test_rebuild_result_typeddict_exists(self) -> None:
260 from muse.cli.commands.index_rebuild import _RebuildResult # noqa: F401
261
262 def test_rebuild_result_has_exit_code(self) -> None:
263 from muse.cli.commands.index_rebuild import _RebuildResult
264 assert "exit_code" in _RebuildResult.__annotations__
265
266 def test_rebuild_result_has_duration_ms(self) -> None:
267 from muse.cli.commands.index_rebuild import _RebuildResult
268 assert "duration_ms" in _RebuildResult.__annotations__
269
270 def test_status_result_typeddict_exists(self) -> None:
271 from muse.cli.commands.index_rebuild import _StatusResult # noqa: F401
272
273 def test_status_result_has_indexes(self) -> None:
274 from muse.cli.commands.index_rebuild import _StatusResult
275 assert "indexes" in _StatusResult.__annotations__
276
277 def test_status_result_has_exit_code(self) -> None:
278 from muse.cli.commands.index_rebuild import _StatusResult
279 assert "exit_code" in _StatusResult.__annotations__
280
281 def test_status_result_has_duration_ms(self) -> None:
282 from muse.cli.commands.index_rebuild import _StatusResult
283 assert "duration_ms" in _StatusResult.__annotations__
284
285 def test_purge_result_typeddict_exists(self) -> None:
286 from muse.cli.commands.index_rebuild import _PurgeResult # noqa: F401
287
288 def test_purge_result_has_exit_code(self) -> None:
289 from muse.cli.commands.index_rebuild import _PurgeResult
290 assert "exit_code" in _PurgeResult.__annotations__
291
292 def test_purge_result_has_duration_ms(self) -> None:
293 from muse.cli.commands.index_rebuild import _PurgeResult
294 assert "duration_ms" in _PurgeResult.__annotations__
295
296
297 # ---------------------------------------------------------------------------
298 # TestDocstrings — run_* functions document exit_code and duration_ms
299 # ---------------------------------------------------------------------------
300
301
302 class TestDocstrings:
303 """All three run_* functions must mention exit_code and duration_ms."""
304
305 def test_run_status_mentions_exit_code(self) -> None:
306 from muse.cli.commands.index_rebuild import run_status
307 assert run_status.__doc__ is not None
308 assert "exit_code" in run_status.__doc__
309
310 def test_run_status_mentions_duration_ms(self) -> None:
311 from muse.cli.commands.index_rebuild import run_status
312 assert "duration_ms" in run_status.__doc__
313
314 def test_run_rebuild_mentions_exit_code(self) -> None:
315 from muse.cli.commands.index_rebuild import run_rebuild
316 assert run_rebuild.__doc__ is not None
317 assert "exit_code" in run_rebuild.__doc__
318
319 def test_run_rebuild_mentions_duration_ms(self) -> None:
320 from muse.cli.commands.index_rebuild import run_rebuild
321 assert "duration_ms" in run_rebuild.__doc__
322
323 def test_run_purge_mentions_exit_code(self) -> None:
324 from muse.cli.commands.index_rebuild import run_purge
325 assert run_purge.__doc__ is not None
326 assert "exit_code" in run_purge.__doc__
327
328 def test_run_purge_mentions_duration_ms(self) -> None:
329 from muse.cli.commands.index_rebuild import run_purge
330 assert "duration_ms" in run_purge.__doc__
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago