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