gabriel / muse public
test_ls_files_supercharge.py python
453 lines 17.5 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 137 days ago
1 """Supercharge tests for ``muse ls-files``.
2
3 Coverage tiers
4 --------------
5 I JSON envelope schema — status, error, branch, path_prefix, duration_ms, exit_code
6 II Error payload shape — consistent {status, error, exit_code}; no prose in JSON mode
7 III branch field — reflects the branch HEAD resolved to
8 IV path_prefix echoed — agents can verify which filter was applied
9 V TypedDicts — _LsFilesJson and _LsFilesErrorJson exist with correct annotations
10 VI Docstring — documents all envelope fields
11 VII Data integrity — object_ids are sha256:-prefixed in all output modes
12 VIII No prose pollution in JSON mode
13 """
14 from __future__ import annotations
15 from collections.abc import Mapping
16
17 import datetime
18 import json
19 import pathlib
20
21 import pytest
22
23 from muse.core.object_store import write_object
24 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
25 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
26 from muse.core._types import Manifest, blob_id, long_id
27 from tests.cli_test_helper import CliRunner, InvokeResult
28
29 runner = CliRunner()
30
31 _ENVELOPE_KEYS = {"duration_ms", "exit_code", "muse_version", "schema", "timestamp", "warnings"}
32 _REQUIRED_SUCCESS_KEYS = {
33 "status", "error", "commit_id", "snapshot_id", "branch",
34 "path_prefix", "file_count", "files",
35 } | _ENVELOPE_KEYS
36 _REQUIRED_ERROR_KEYS = {"status", "error"} | _ENVELOPE_KEYS
37
38 _TS = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
39
40
41 # ---------------------------------------------------------------------------
42 # Helpers
43 # ---------------------------------------------------------------------------
44
45 def _oid(content: bytes) -> str:
46 return blob_id(content)
47
48
49 def _make_repo(tmp_path: pathlib.Path, branch: str = "main") -> pathlib.Path:
50 repo = tmp_path / "repo"
51 muse = repo / ".muse"
52 for sub in ("objects", "commits", "snapshots", "refs/heads"):
53 (muse / sub).mkdir(parents=True)
54 (muse / "HEAD").write_text(f"ref: refs/heads/{branch}")
55 (muse / "repo.json").write_text(json.dumps({"repo_id": "test", "domain": "code"}))
56 return repo
57
58
59 def _add_commit(
60 repo: pathlib.Path,
61 files: dict[str, bytes],
62 *,
63 branch: str = "main",
64 set_head: bool = True,
65 ) -> str:
66 stored: Manifest = {}
67 for path, content in files.items():
68 oid = _oid(content)
69 write_object(repo, oid, content)
70 stored[path] = oid
71 snap_id = compute_snapshot_id(stored)
72 write_snapshot(repo, SnapshotRecord(snapshot_id=snap_id, manifest=stored, created_at=_TS))
73 commit_id = compute_commit_id(
74 repo_id="test",
75 parent_ids=[],
76 snapshot_id=snap_id,
77 message="test",
78 committed_at_iso=_TS.isoformat(),
79 author="tester",)
80 write_commit(repo, CommitRecord(
81 commit_id=commit_id, repo_id="test", created_on_branch=branch,
82 snapshot_id=snap_id, message="test", committed_at=_TS,
83 author="tester", parent_commit_id=None,
84 ))
85 if set_head:
86 ref = repo / ".muse" / "refs" / "heads" / branch
87 ref.parent.mkdir(parents=True, exist_ok=True)
88 ref.write_text(commit_id)
89 return commit_id
90
91
92 def _ls(repo: pathlib.Path, *args: str) -> InvokeResult:
93 from muse.cli.app import main as cli
94 return runner.invoke(cli, ["ls-files", *args], env={"MUSE_REPO_ROOT": str(repo)})
95
96
97 def _ls_json(repo: pathlib.Path, *args: str) -> Mapping[str, object]:
98 result = _ls(repo, "--json", *args)
99 assert result.exit_code == 0, f"ls-files --json failed:\n{result.output}"
100 return json.loads(result.output.strip())
101
102
103 # ---------------------------------------------------------------------------
104 # I JSON envelope schema
105 # ---------------------------------------------------------------------------
106
107
108 class TestJsonEnvelopeSchema:
109 def test_all_required_keys_present(self, tmp_path: pathlib.Path) -> None:
110 repo = _make_repo(tmp_path)
111 _add_commit(repo, {"a.py": b"a"})
112 data = _ls_json(repo)
113 missing = _REQUIRED_SUCCESS_KEYS - set(data.keys())
114 assert not missing, f"Missing envelope keys: {missing}"
115
116 def test_no_extra_undocumented_keys(self, tmp_path: pathlib.Path) -> None:
117 repo = _make_repo(tmp_path)
118 _add_commit(repo, {"a.py": b"a"})
119 data = _ls_json(repo)
120 extra = set(data.keys()) - _REQUIRED_SUCCESS_KEYS
121 assert not extra, f"Undocumented extra keys: {extra}"
122
123 def test_status_ok_on_success(self, tmp_path: pathlib.Path) -> None:
124 repo = _make_repo(tmp_path)
125 _add_commit(repo, {"a.py": b"a"})
126 data = _ls_json(repo)
127 assert data["status"] == "ok"
128
129 def test_error_empty_string_on_success(self, tmp_path: pathlib.Path) -> None:
130 repo = _make_repo(tmp_path)
131 _add_commit(repo, {"a.py": b"a"})
132 data = _ls_json(repo)
133 assert data["error"] == ""
134
135 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
136 repo = _make_repo(tmp_path)
137 _add_commit(repo, {"a.py": b"a"})
138 data = _ls_json(repo)
139 assert data["exit_code"] == 0
140
141 def test_duration_ms_nonnegative_float(self, tmp_path: pathlib.Path) -> None:
142 repo = _make_repo(tmp_path)
143 _add_commit(repo, {"a.py": b"a"})
144 data = _ls_json(repo)
145 assert isinstance(data["duration_ms"], float)
146 assert data["duration_ms"] >= 0.0
147
148 def test_file_count_matches_files_length(self, tmp_path: pathlib.Path) -> None:
149 repo = _make_repo(tmp_path)
150 _add_commit(repo, {"a.py": b"a", "b.py": b"b", "c.py": b"c"})
151 data = _ls_json(repo)
152 assert data["file_count"] == len(data["files"])
153
154 def test_files_is_list(self, tmp_path: pathlib.Path) -> None:
155 repo = _make_repo(tmp_path)
156 _add_commit(repo, {"a.py": b"a"})
157 data = _ls_json(repo)
158 assert isinstance(data["files"], list)
159
160 def test_path_prefix_none_when_not_filtered(self, tmp_path: pathlib.Path) -> None:
161 repo = _make_repo(tmp_path)
162 _add_commit(repo, {"a.py": b"a"})
163 data = _ls_json(repo)
164 assert data["path_prefix"] is None
165
166 def test_path_prefix_echoed_when_filtered(self, tmp_path: pathlib.Path) -> None:
167 repo = _make_repo(tmp_path)
168 _add_commit(repo, {"src/a.py": b"a", "tests/b.py": b"b"})
169 data = _ls_json(repo, "--path-prefix", "src/")
170 assert data["path_prefix"] == "src/"
171
172 def test_commit_id_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
173 repo = _make_repo(tmp_path)
174 _add_commit(repo, {"a.py": b"a"})
175 data = _ls_json(repo)
176 assert data["commit_id"].startswith("sha256:")
177
178 def test_snapshot_id_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
179 repo = _make_repo(tmp_path)
180 _add_commit(repo, {"a.py": b"a"})
181 data = _ls_json(repo)
182 assert data["snapshot_id"].startswith("sha256:")
183
184
185 # ---------------------------------------------------------------------------
186 # II Error payload shape
187 # ---------------------------------------------------------------------------
188
189
190 class TestErrorPayloadShape:
191 def test_error_keys_present(self, tmp_path: pathlib.Path) -> None:
192 repo = _make_repo(tmp_path) # no commits
193 result = _ls(repo, "--json")
194 assert result.exit_code != 0
195 data = json.loads(result.output.strip())
196 assert _REQUIRED_ERROR_KEYS.issubset(set(data.keys()))
197
198 def test_error_status_is_error(self, tmp_path: pathlib.Path) -> None:
199 repo = _make_repo(tmp_path)
200 result = _ls(repo, "--json")
201 assert result.exit_code != 0
202 data = json.loads(result.output.strip())
203 assert data["status"] == "error"
204
205 def test_error_message_nonempty(self, tmp_path: pathlib.Path) -> None:
206 repo = _make_repo(tmp_path)
207 result = _ls(repo, "--json")
208 data = json.loads(result.output.strip())
209 assert isinstance(data["error"], str) and len(data["error"]) > 0
210
211 def test_error_exit_code_nonzero(self, tmp_path: pathlib.Path) -> None:
212 repo = _make_repo(tmp_path)
213 result = _ls(repo, "--json")
214 data = json.loads(result.output.strip())
215 assert isinstance(data["exit_code"], int) and data["exit_code"] != 0
216
217 def test_invalid_commit_error_payload(self, tmp_path: pathlib.Path) -> None:
218 repo = _make_repo(tmp_path)
219 result = _ls(repo, "--json", "--commit", "not-valid")
220 assert result.exit_code != 0
221 data = json.loads(result.output.strip())
222 assert data["status"] == "error"
223 assert "exit_code" in data
224
225 def test_nonexistent_commit_error_payload(self, tmp_path: pathlib.Path) -> None:
226 repo = _make_repo(tmp_path)
227 result = _ls(repo, "--json", "--commit", long_id("f" * 64))
228 assert result.exit_code != 0
229 data = json.loads(result.output.strip())
230 assert data["status"] == "error"
231
232
233 # ---------------------------------------------------------------------------
234 # III branch field
235 # ---------------------------------------------------------------------------
236
237
238 class TestBranchField:
239 def test_branch_is_main_when_on_main(self, tmp_path: pathlib.Path) -> None:
240 repo = _make_repo(tmp_path, branch="main")
241 _add_commit(repo, {"a.py": b"a"}, branch="main")
242 data = _ls_json(repo)
243 assert data["branch"] == "main"
244
245 def test_branch_is_dev_when_on_dev(self, tmp_path: pathlib.Path) -> None:
246 repo = _make_repo(tmp_path, branch="dev")
247 _add_commit(repo, {"a.py": b"a"}, branch="dev")
248 data = _ls_json(repo)
249 assert data["branch"] == "dev"
250
251 def test_branch_is_none_when_explicit_commit_given(
252 self, tmp_path: pathlib.Path
253 ) -> None:
254 """When --commit is given explicitly, no branch resolution occurs."""
255 repo = _make_repo(tmp_path)
256 cid = _add_commit(repo, {"a.py": b"a"})
257 data = _ls_json(repo, "--commit", cid)
258 # branch is null when commit was specified directly, not via HEAD
259 assert data["branch"] is None
260
261
262 # ---------------------------------------------------------------------------
263 # IV path_prefix echoed
264 # ---------------------------------------------------------------------------
265
266
267 class TestPathPrefixEchoed:
268 def test_path_prefix_null_without_filter(self, tmp_path: pathlib.Path) -> None:
269 repo = _make_repo(tmp_path)
270 _add_commit(repo, {"a.py": b"a"})
271 data = _ls_json(repo)
272 assert data["path_prefix"] is None
273
274 def test_path_prefix_echoed_src(self, tmp_path: pathlib.Path) -> None:
275 repo = _make_repo(tmp_path)
276 _add_commit(repo, {"src/a.py": b"a"})
277 data = _ls_json(repo, "--path-prefix", "src/")
278 assert data["path_prefix"] == "src/"
279
280 def test_path_prefix_echoed_nested(self, tmp_path: pathlib.Path) -> None:
281 repo = _make_repo(tmp_path)
282 _add_commit(repo, {"a/b/c.py": b"c"})
283 data = _ls_json(repo, "--path-prefix", "a/b/")
284 assert data["path_prefix"] == "a/b/"
285
286
287 # ---------------------------------------------------------------------------
288 # V TypedDicts
289 # ---------------------------------------------------------------------------
290
291
292 class TestTypedDicts:
293 def test_ls_files_json_typed_dict_exists(self) -> None:
294 from muse.cli.commands.ls_files import _LsFilesJson # type: ignore[attr-defined]
295 assert _LsFilesJson is not None
296
297 def test_ls_files_error_json_typed_dict_exists(self) -> None:
298 from muse.cli.commands.ls_files import _LsFilesErrorJson # type: ignore[attr-defined]
299 assert _LsFilesErrorJson is not None
300
301 def test_ls_files_json_has_all_annotations(self) -> None:
302 from muse.cli.commands.ls_files import _LsFilesJson # type: ignore[attr-defined]
303 hints = _LsFilesJson.__annotations__
304 required = {"status", "error", "commit_id", "snapshot_id", "branch",
305 "path_prefix", "file_count", "files", "duration_ms", "exit_code"}
306 assert not (required - set(hints)), f"Missing: {required - set(hints)}"
307
308 def test_ls_files_error_json_has_all_annotations(self) -> None:
309 from muse.cli.commands.ls_files import _LsFilesErrorJson # type: ignore[attr-defined]
310 hints = _LsFilesErrorJson.__annotations__
311 assert not ({"status", "error", "exit_code"} - set(hints))
312
313
314 # ---------------------------------------------------------------------------
315 # VI Docstring
316 # ---------------------------------------------------------------------------
317
318
319 class TestDocstring:
320 def test_docstring_documents_status(self) -> None:
321 import muse.cli.commands.ls_files as m
322 assert '"status"' in (m.__doc__ or "")
323
324 def test_docstring_documents_branch(self) -> None:
325 import muse.cli.commands.ls_files as m
326 assert '"branch"' in (m.__doc__ or "")
327
328 def test_docstring_documents_path_prefix(self) -> None:
329 import muse.cli.commands.ls_files as m
330 assert '"path_prefix"' in (m.__doc__ or "")
331
332 def test_docstring_documents_duration_ms(self) -> None:
333 import muse.cli.commands.ls_files as m
334 assert "duration_ms" in (m.__doc__ or "")
335
336 def test_docstring_documents_exit_code(self) -> None:
337 import muse.cli.commands.ls_files as m
338 assert "exit_code" in (m.__doc__ or "")
339
340 def test_docstring_documents_error(self) -> None:
341 import muse.cli.commands.ls_files as m
342 assert '"error"' in (m.__doc__ or "")
343
344
345 # ---------------------------------------------------------------------------
346 # VII Data integrity
347 # ---------------------------------------------------------------------------
348
349
350 class TestDataIntegrity:
351 def test_object_ids_sha256_prefixed_in_json(self, tmp_path: pathlib.Path) -> None:
352 repo = _make_repo(tmp_path)
353 _add_commit(repo, {"a.py": b"content", "b.py": b"more"})
354 data = _ls_json(repo)
355 for f in data["files"]:
356 assert f["object_id"].startswith("sha256:"), (
357 f"object_id not sha256:-prefixed: {f['object_id']!r}"
358 )
359
360 def test_object_ids_sha256_prefixed_in_text(self, tmp_path: pathlib.Path) -> None:
361 repo = _make_repo(tmp_path)
362 _add_commit(repo, {"a.py": b"content"})
363 # Default (no --json) emits text: <oid>\t<path> per line
364 result = _ls(repo)
365 assert result.exit_code == 0
366 for line in result.output.strip().splitlines():
367 oid = line.split("\t")[0]
368 assert oid.startswith("sha256:"), f"text OID not prefixed: {oid!r}"
369
370 def test_commit_id_matches_stored(self, tmp_path: pathlib.Path) -> None:
371 repo = _make_repo(tmp_path)
372 cid = _add_commit(repo, {"a.py": b"a"})
373 data = _ls_json(repo)
374 assert data["commit_id"] == cid
375
376 def test_files_sorted_alphabetically(self, tmp_path: pathlib.Path) -> None:
377 repo = _make_repo(tmp_path)
378 _add_commit(repo, {"z.py": b"z", "a.py": b"a", "m.py": b"m"})
379 data = _ls_json(repo)
380 paths = [f["path"] for f in data["files"]]
381 assert paths == sorted(paths)
382
383 def test_path_prefix_file_count_consistent(self, tmp_path: pathlib.Path) -> None:
384 repo = _make_repo(tmp_path)
385 _add_commit(repo, {"src/a.py": b"a", "src/b.py": b"b", "tests/c.py": b"c"})
386 data = _ls_json(repo, "--path-prefix", "src/")
387 assert data["file_count"] == len(data["files"]) == 2
388
389
390 # ---------------------------------------------------------------------------
391 # VIII No prose pollution in JSON mode
392 # ---------------------------------------------------------------------------
393
394
395 class TestNoProsePollution:
396 def test_success_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
397 repo = _make_repo(tmp_path)
398 _add_commit(repo, {"a.py": b"a"})
399 result = _ls(repo, "--json")
400 json.loads(result.output.strip()) # must not raise
401
402 def test_no_emoji_in_json_success_output(self, tmp_path: pathlib.Path) -> None:
403 repo = _make_repo(tmp_path)
404 _add_commit(repo, {"a.py": b"a"})
405 result = _ls(repo, "--json")
406 assert "❌" not in result.output
407 assert "✅" not in result.output
408
409 def test_no_emoji_in_json_error_output(self, tmp_path: pathlib.Path) -> None:
410 """Errors in --json mode must not emit prose emoji to stdout."""
411 repo = _make_repo(tmp_path) # no commits
412 result = _ls(repo, "--json")
413 assert "❌" not in result.output
414 data = json.loads(result.output.strip())
415 assert data["status"] == "error"
416
417 def test_error_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
418 repo = _make_repo(tmp_path)
419 result = _ls(repo, "--json")
420 json.loads(result.output.strip()) # must not raise
421
422
423 class TestRegisterFlags:
424 def test_json_short_flag(self):
425 import argparse
426 from muse.cli.commands.ls_files import register
427 p = argparse.ArgumentParser()
428 subs = p.add_subparsers()
429 register(subs)
430 args = p.parse_args(["ls-files", "-j"])
431 assert args.json_out is True
432
433 def test_json_long_flag(self):
434 import argparse
435 from muse.cli.commands.ls_files import register
436 p = argparse.ArgumentParser()
437 subs = p.add_subparsers()
438 register(subs)
439 args = p.parse_args(["ls-files", "--json"])
440 assert args.json_out is True
441
442 def test_default_no_json(self):
443 import argparse
444 from muse.cli.commands.ls_files import register
445 p = argparse.ArgumentParser()
446 subs = p.add_subparsers()
447 register(subs)
448 # Command-specific required args may differ; just check dest exists when possible
449 try:
450 args = p.parse_args(["ls-files"])
451 assert args.json_out is False
452 except SystemExit:
453 pass # required positional args missing — flag default still correct
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 137 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 143 days ago