gabriel / muse public
test_index_supercharge.py python
362 lines 14.6 KB
Raw
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402 Merge branch 'dev' into main Human 20 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 from collections.abc import Mapping
22
23 import json
24 import pathlib
25 import textwrap
26
27 import pytest
28
29 from tests.cli_test_helper import CliRunner, InvokeResult
30
31 runner = CliRunner()
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38
39 def _env(root: pathlib.Path) -> Mapping[str, str]:
40 return {"MUSE_REPO_ROOT": str(root)}
41
42
43 def _run(root: pathlib.Path, *args: str) -> InvokeResult:
44 return runner.invoke(None, list(args), env=_env(root))
45
46
47 # ---------------------------------------------------------------------------
48 # Fixture — minimal repo
49 # ---------------------------------------------------------------------------
50
51
52 @pytest.fixture()
53 def index_repo(
54 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
55 ) -> pathlib.Path:
56 """Minimal repo with one Python file — enough to build real indexes."""
57 monkeypatch.chdir(tmp_path)
58 r = _run(tmp_path, "init", "--domain", "code")
59 assert r.exit_code == 0, r.output
60
61 (tmp_path / "core.py").write_text(textwrap.dedent("""\
62 def compute(x):
63 return x * 2
64
65 def validate(x):
66 return x > 0
67 """))
68 r = _run(tmp_path, "code", "add", ".")
69 assert r.exit_code == 0, r.output
70 r = _run(tmp_path, "commit", "-m", "seed index repo")
71 assert r.exit_code == 0, r.output
72
73 return tmp_path
74
75
76 # ---------------------------------------------------------------------------
77 # TestStatusJson — status --json envelope shape
78 # ---------------------------------------------------------------------------
79
80
81 class TestStatusJson:
82 """status --json must emit a wrapper object with indexes, exit_code, duration_ms."""
83
84 def test_status_j_exits_zero(self, index_repo: pathlib.Path) -> None:
85 r = _run(index_repo, "code", "index", "status", "-j")
86 assert r.exit_code == 0, r.output
87
88 def test_status_j_valid_json(self, index_repo: pathlib.Path) -> None:
89 r = _run(index_repo, "code", "index", "status", "-j")
90 json.loads(r.output) # must not raise
91
92 def test_status_j_has_indexes_key(self, index_repo: pathlib.Path) -> None:
93 r = _run(index_repo, "code", "index", "status", "-j")
94 assert "indexes" in json.loads(r.output)
95
96 def test_status_j_indexes_is_list(self, index_repo: pathlib.Path) -> None:
97 r = _run(index_repo, "code", "index", "status", "-j")
98 assert isinstance(json.loads(r.output)["indexes"], list)
99
100 def test_status_j_has_exit_code(self, index_repo: pathlib.Path) -> None:
101 r = _run(index_repo, "code", "index", "status", "-j")
102 assert "exit_code" in json.loads(r.output)
103
104 def test_status_j_exit_code_zero(self, index_repo: pathlib.Path) -> None:
105 r = _run(index_repo, "code", "index", "status", "-j")
106 assert json.loads(r.output)["exit_code"] == 0
107
108 def test_status_j_has_duration_ms(self, index_repo: pathlib.Path) -> None:
109 r = _run(index_repo, "code", "index", "status", "-j")
110 assert "duration_ms" in json.loads(r.output)
111
112 def test_status_j_duration_ms_is_float(self, index_repo: pathlib.Path) -> None:
113 r = _run(index_repo, "code", "index", "status", "-j")
114 assert isinstance(json.loads(r.output)["duration_ms"], float)
115
116 def test_status_j_duration_ms_nonnegative(self, index_repo: pathlib.Path) -> None:
117 r = _run(index_repo, "code", "index", "status", "-j")
118 assert json.loads(r.output)["duration_ms"] >= 0
119
120 def test_status_j_index_entries_have_name(self, index_repo: pathlib.Path) -> None:
121 r = _run(index_repo, "code", "index", "status", "-j")
122 for entry in json.loads(r.output)["indexes"]:
123 assert "name" in entry
124
125 def test_status_j_index_entries_have_status(self, index_repo: pathlib.Path) -> None:
126 r = _run(index_repo, "code", "index", "status", "-j")
127 for entry in json.loads(r.output)["indexes"]:
128 assert "status" in entry
129
130 def test_status_j_no_ansi(self, index_repo: pathlib.Path) -> None:
131 r = _run(index_repo, "code", "index", "status", "-j")
132 assert "\x1b" not in r.output
133
134 def test_status_json_flag_same_as_j(self, index_repo: pathlib.Path) -> None:
135 r1 = _run(index_repo, "code", "index", "status", "--json")
136 r2 = _run(index_repo, "code", "index", "status", "-j")
137 d1 = json.loads(r1.output)
138 d2 = json.loads(r2.output)
139 d1.pop("duration_ms", None)
140 d2.pop("duration_ms", None)
141 d1.pop("timestamp", None)
142 d2.pop("timestamp", None)
143 assert d1 == d2
144
145
146 # ---------------------------------------------------------------------------
147 # TestRebuildJson — rebuild --json envelope
148 # ---------------------------------------------------------------------------
149
150
151 class TestRebuildJson:
152 """rebuild --json must include exit_code and duration_ms."""
153
154 def test_rebuild_j_exits_zero(self, index_repo: pathlib.Path) -> None:
155 r = _run(index_repo, "code", "index", "rebuild", "-j")
156 assert r.exit_code == 0, r.output
157
158 def test_rebuild_j_valid_json(self, index_repo: pathlib.Path) -> None:
159 r = _run(index_repo, "code", "index", "rebuild", "-j")
160 json.loads(r.output) # must not raise
161
162 def test_rebuild_j_has_exit_code(self, index_repo: pathlib.Path) -> None:
163 r = _run(index_repo, "code", "index", "rebuild", "-j")
164 assert "exit_code" in json.loads(r.output)
165
166 def test_rebuild_j_exit_code_zero(self, index_repo: pathlib.Path) -> None:
167 r = _run(index_repo, "code", "index", "rebuild", "-j")
168 assert json.loads(r.output)["exit_code"] == 0
169
170 def test_rebuild_j_has_duration_ms(self, index_repo: pathlib.Path) -> None:
171 r = _run(index_repo, "code", "index", "rebuild", "-j")
172 assert "duration_ms" in json.loads(r.output)
173
174 def test_rebuild_j_duration_ms_is_float(self, index_repo: pathlib.Path) -> None:
175 r = _run(index_repo, "code", "index", "rebuild", "-j")
176 assert isinstance(json.loads(r.output)["duration_ms"], float)
177
178 def test_rebuild_j_has_rebuilt_key(self, index_repo: pathlib.Path) -> None:
179 r = _run(index_repo, "code", "index", "rebuild", "-j")
180 assert "rebuilt" in json.loads(r.output)
181
182 def test_rebuild_dry_run_j_exit_code_zero(self, index_repo: pathlib.Path) -> None:
183 r = _run(index_repo, "code", "index", "rebuild", "--dry-run", "-j")
184 assert r.exit_code == 0, r.output
185 assert json.loads(r.output)["exit_code"] == 0
186
187 def test_rebuild_dry_run_j_duration_ms(self, index_repo: pathlib.Path) -> None:
188 r = _run(index_repo, "code", "index", "rebuild", "--dry-run", "-j")
189 data = json.loads(r.output)
190 assert "duration_ms" in data
191 assert isinstance(data["duration_ms"], float)
192
193 def test_rebuild_j_no_ansi(self, index_repo: pathlib.Path) -> None:
194 r = _run(index_repo, "code", "index", "rebuild", "-j")
195 assert "\x1b" not in r.output
196
197
198 # ---------------------------------------------------------------------------
199 # TestPurgeJson — purge --json envelope
200 # ---------------------------------------------------------------------------
201
202
203 class TestPurgeJson:
204 """purge --json must include exit_code and duration_ms."""
205
206 def test_purge_j_exits_zero(self, index_repo: pathlib.Path) -> None:
207 r = _run(index_repo, "code", "index", "purge", "-j")
208 assert r.exit_code == 0, r.output
209
210 def test_purge_j_valid_json(self, index_repo: pathlib.Path) -> None:
211 r = _run(index_repo, "code", "index", "purge", "-j")
212 json.loads(r.output) # must not raise
213
214 def test_purge_j_has_exit_code(self, index_repo: pathlib.Path) -> None:
215 r = _run(index_repo, "code", "index", "purge", "-j")
216 assert "exit_code" in json.loads(r.output)
217
218 def test_purge_j_exit_code_zero(self, index_repo: pathlib.Path) -> None:
219 r = _run(index_repo, "code", "index", "purge", "-j")
220 assert json.loads(r.output)["exit_code"] == 0
221
222 def test_purge_j_has_duration_ms(self, index_repo: pathlib.Path) -> None:
223 r = _run(index_repo, "code", "index", "purge", "-j")
224 assert "duration_ms" in json.loads(r.output)
225
226 def test_purge_j_duration_ms_is_float(self, index_repo: pathlib.Path) -> None:
227 r = _run(index_repo, "code", "index", "purge", "-j")
228 assert isinstance(json.loads(r.output)["duration_ms"], float)
229
230 def test_purge_j_has_purged_key(self, index_repo: pathlib.Path) -> None:
231 r = _run(index_repo, "code", "index", "purge", "-j")
232 assert "purged" in json.loads(r.output)
233
234 def test_purge_j_has_skipped_key(self, index_repo: pathlib.Path) -> None:
235 r = _run(index_repo, "code", "index", "purge", "-j")
236 assert "skipped" in json.loads(r.output)
237
238 def test_purge_j_no_ansi(self, index_repo: pathlib.Path) -> None:
239 r = _run(index_repo, "code", "index", "purge", "-j")
240 assert "\x1b" not in r.output
241
242 def test_purge_absent_indexes_skipped_not_error(self, index_repo: pathlib.Path) -> None:
243 """Purging an already-absent index exits 0 and lists it as skipped."""
244 # Purge twice — second time all indexes are absent
245 _run(index_repo, "code", "index", "purge", "-j")
246 r = _run(index_repo, "code", "index", "purge", "-j")
247 assert r.exit_code == 0
248 data = json.loads(r.output)
249 assert data["exit_code"] == 0
250 assert data["purged"] == []
251 assert len(data["skipped"]) > 0
252
253
254 # ---------------------------------------------------------------------------
255 # TestTypedDicts — TypedDicts carry exit_code and duration_ms
256 # ---------------------------------------------------------------------------
257
258
259 class TestTypedDicts:
260 """All three TypedDicts must carry exit_code and duration_ms annotations."""
261
262 def test_rebuild_result_typeddict_exists(self) -> None:
263 from muse.cli.commands.index_rebuild import _RebuildResult # noqa: F401
264
265 def test_rebuild_result_has_exit_code(self) -> None:
266 from muse.cli.commands.index_rebuild import _RebuildResult
267 assert "exit_code" in _RebuildResult.__annotations__
268
269 def test_rebuild_result_has_duration_ms(self) -> None:
270 from muse.cli.commands.index_rebuild import _RebuildResult
271 assert "duration_ms" in _RebuildResult.__annotations__
272
273 def test_status_result_typeddict_exists(self) -> None:
274 from muse.cli.commands.index_rebuild import _StatusResult # noqa: F401
275
276 def test_status_result_has_indexes(self) -> None:
277 from muse.cli.commands.index_rebuild import _StatusResult
278 assert "indexes" in _StatusResult.__annotations__
279
280 def test_status_result_has_exit_code(self) -> None:
281 from muse.cli.commands.index_rebuild import _StatusResult
282 assert "exit_code" in _StatusResult.__annotations__
283
284 def test_status_result_has_duration_ms(self) -> None:
285 from muse.cli.commands.index_rebuild import _StatusResult
286 assert "duration_ms" in _StatusResult.__annotations__
287
288 def test_purge_result_typeddict_exists(self) -> None:
289 from muse.cli.commands.index_rebuild import _PurgeResult # noqa: F401
290
291 def test_purge_result_has_exit_code(self) -> None:
292 from muse.cli.commands.index_rebuild import _PurgeResult
293 assert "exit_code" in _PurgeResult.__annotations__
294
295 def test_purge_result_has_duration_ms(self) -> None:
296 from muse.cli.commands.index_rebuild import _PurgeResult
297 assert "duration_ms" in _PurgeResult.__annotations__
298
299
300 # ---------------------------------------------------------------------------
301 # TestDocstrings — run_* functions document exit_code and duration_ms
302 # ---------------------------------------------------------------------------
303
304
305 class TestDocstrings:
306 """All three run_* functions must mention exit_code and duration_ms."""
307
308 def test_run_status_mentions_exit_code(self) -> None:
309 from muse.cli.commands.index_rebuild import run_status
310 assert run_status.__doc__ is not None
311 assert "exit_code" in run_status.__doc__
312
313 def test_run_status_mentions_duration_ms(self) -> None:
314 from muse.cli.commands.index_rebuild import run_status
315 assert "duration_ms" in run_status.__doc__
316
317 def test_run_rebuild_mentions_exit_code(self) -> None:
318 from muse.cli.commands.index_rebuild import run_rebuild
319 assert run_rebuild.__doc__ is not None
320 assert "exit_code" in run_rebuild.__doc__
321
322 def test_run_rebuild_mentions_duration_ms(self) -> None:
323 from muse.cli.commands.index_rebuild import run_rebuild
324 assert "duration_ms" in run_rebuild.__doc__
325
326 def test_run_purge_mentions_exit_code(self) -> None:
327 from muse.cli.commands.index_rebuild import run_purge
328 assert run_purge.__doc__ is not None
329 assert "exit_code" in run_purge.__doc__
330
331 def test_run_purge_mentions_duration_ms(self) -> None:
332 from muse.cli.commands.index_rebuild import run_purge
333 assert "duration_ms" in run_purge.__doc__
334
335
336 class TestRegisterFlags:
337 def test_json_short_flag(self) -> None:
338 import argparse
339 from muse.cli.commands.index_rebuild import register
340 p = argparse.ArgumentParser()
341 subs = p.add_subparsers()
342 register(subs)
343 args = p.parse_args(["index", "rebuild", "-j"])
344 assert args.json_out is True
345
346 def test_json_long_flag(self) -> None:
347 import argparse
348 from muse.cli.commands.index_rebuild import register
349 p = argparse.ArgumentParser()
350 subs = p.add_subparsers()
351 register(subs)
352 args = p.parse_args(["index", "rebuild", "--json"])
353 assert args.json_out is True
354
355 def test_default_no_json(self) -> None:
356 import argparse
357 from muse.cli.commands.index_rebuild import register
358 p = argparse.ArgumentParser()
359 subs = p.add_subparsers()
360 register(subs)
361 args = p.parse_args(["index", "rebuild"])
362 assert args.json_out is False
File History 1 commit
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402 Merge branch 'dev' into main Human 20 days ago