gabriel / muse public
test_cmd_core_cat.py python
536 lines 19.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Tests for ``muse cat`` — file-level, domain-agnostic content reader.
2
3 ``muse cat`` is the core-VCS primitive: give me the raw bytes of a tracked
4 file at HEAD or any ref. It does not parse symbols — that is ``muse code cat``.
5 Mirrors the relationship between ``muse blame`` (line-level) and
6 ``muse code blame`` (symbol-level).
7
8 7-tier coverage
9 ---------------
10 Unit argument parsing, address validation (:: rejected)
11 Integration single file, multi-file, --at ref, --json schema
12 E2E historical ref shows old content; working-tree shows new
13 Security symlink rejected, path traversal rejected, ANSI in path
14 Stress large file (1 MiB) completes fast
15 Data integrity JSON content == disk bytes; --at content != HEAD content
16 Performance single file < 0.3s; multi-file 10 files < 1s
17 """
18
19 from __future__ import annotations
20 from collections.abc import Mapping
21
22 import json
23 import pathlib
24 import textwrap
25 import time
26 import hashlib
27 import datetime
28
29 import pytest
30
31 from tests.cli_test_helper import CliRunner
32 from muse.core.object_store import write_object
33 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
34 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
35 from muse.core._types import long_id, blob_id
36
37 cli = None
38 runner = CliRunner()
39
40 _REPO_ID = "core-cat-test"
41 _counter = 0
42
43
44 # ---------------------------------------------------------------------------
45 # Helpers
46 # ---------------------------------------------------------------------------
47
48
49 def _sha(data: bytes) -> str:
50 return blob_id(data)
51
52
53 def _init_repo(path: pathlib.Path, repo_id: str = _REPO_ID) -> pathlib.Path:
54 muse = path / ".muse"
55 for d in ("commits", "snapshots", "objects", "refs/heads"):
56 (muse / d).mkdir(parents=True, exist_ok=True)
57 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
58 (muse / "repo.json").write_text(
59 json.dumps({"repo_id": repo_id, "domain": "code"}), encoding="utf-8"
60 )
61 return path
62
63
64 def _env(repo: pathlib.Path) -> Mapping[str, str]:
65 return {"MUSE_REPO_ROOT": str(repo)}
66
67
68 def _add_file(repo: pathlib.Path, rel_path: str, content: bytes) -> str:
69 obj_id = long_id(_sha(content))
70 write_object(repo, obj_id, content)
71 full = repo / rel_path
72 full.parent.mkdir(parents=True, exist_ok=True)
73 full.write_bytes(content)
74 return obj_id
75
76
77 def _commit(
78 repo: pathlib.Path,
79 files: Mapping[str, bytes],
80 message: str = "c",
81 parent_id: str | None = None,
82 branch: str = "main",
83 ) -> str:
84 global _counter
85 _counter += 1
86 manifest = {p: _add_file(repo, p, c) for p, c in files.items()}
87 snap_id = compute_snapshot_id(manifest)
88 write_snapshot(repo, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
89 now = datetime.datetime.now(datetime.timezone.utc)
90 cid = compute_commit_id(
91 repo_id=_REPO_ID,
92 parent_ids=[parent_id] if parent_id else [],
93 snapshot_id=snap_id,
94 message=message,
95 committed_at_iso=now.isoformat(),
96 )
97 write_commit(repo, CommitRecord(
98 commit_id=cid, repo_id=_REPO_ID, created_on_branch=branch,
99 snapshot_id=snap_id, message=message, committed_at=now,
100 parent_commit_id=parent_id,
101 ))
102 (repo / ".muse" / "refs" / "heads" / branch).write_text(cid, encoding="utf-8")
103 return cid
104
105
106 # ---------------------------------------------------------------------------
107 # Fixtures
108 # ---------------------------------------------------------------------------
109
110
111 _CONTENT_V1 = b"# version 1\nHELLO = 'world'\n"
112 _CONTENT_V2 = b"# version 2\nHELLO = 'updated'\n"
113
114
115 @pytest.fixture
116 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
117 _init_repo(tmp_path)
118 _commit(tmp_path, {"readme.md": b"# readme\n", "src/main.py": _CONTENT_V1})
119 return tmp_path
120
121
122 @pytest.fixture
123 def two_commit_repo(tmp_path: pathlib.Path) -> pathlib.Path:
124 _init_repo(tmp_path)
125 c1 = _commit(tmp_path, {"src/main.py": _CONTENT_V1}, message="v1")
126 _commit(tmp_path, {"src/main.py": _CONTENT_V2}, message="v2", parent_id=c1)
127 return tmp_path
128
129
130 # ---------------------------------------------------------------------------
131 # Unit: argument validation
132 # ---------------------------------------------------------------------------
133
134
135 class TestArgumentValidation:
136 def test_no_args_exits_nonzero(self, repo: pathlib.Path) -> None:
137 result = runner.invoke(cli, ["cat"], env=_env(repo))
138 assert result.exit_code != 0
139
140 def test_no_args_json_is_valid_json_with_error(self, repo: pathlib.Path) -> None:
141 result = runner.invoke(cli, ["cat", "--json"], env=_env(repo))
142 assert result.exit_code != 0
143 data = json.loads(result.output)
144 assert "error" in data
145
146 def test_symbol_address_rejected(self, repo: pathlib.Path) -> None:
147 """muse cat does not accept file.py::Symbol — that is muse code cat."""
148 result = runner.invoke(cli, ["cat", "src/main.py::HELLO"], env=_env(repo))
149 assert result.exit_code != 0
150
151 def test_symbol_address_json_has_error_code(self, repo: pathlib.Path) -> None:
152 result = runner.invoke(
153 cli, ["cat", "src/main.py::HELLO", "--json"], env=_env(repo)
154 )
155 assert result.exit_code != 0
156 data = json.loads(result.output)
157 assert "error" in data
158
159 def test_untracked_file_exits_nonzero(self, repo: pathlib.Path) -> None:
160 result = runner.invoke(cli, ["cat", "nothere.txt"], env=_env(repo))
161 assert result.exit_code != 0
162
163 def test_untracked_file_json_has_error_code(self, repo: pathlib.Path) -> None:
164 result = runner.invoke(
165 cli, ["cat", "nothere.txt", "--json"], env=_env(repo)
166 )
167 assert result.exit_code != 0
168 data = json.loads(result.output)
169 # Single file error: multi-file schema with errors list
170 assert "errors" in data
171 assert len(data["errors"]) == 1
172 assert "error_code" in data["errors"][0]
173
174
175 # ---------------------------------------------------------------------------
176 # Integration: single file
177 # ---------------------------------------------------------------------------
178
179
180 class TestSingleFile:
181 def test_prints_file_content(self, repo: pathlib.Path) -> None:
182 result = runner.invoke(cli, ["cat", "readme.md"], env=_env(repo))
183 assert result.exit_code == 0
184 assert "# readme" in result.output
185
186 def test_json_schema_single_file(self, repo: pathlib.Path) -> None:
187 result = runner.invoke(cli, ["cat", "readme.md", "--json"], env=_env(repo))
188 assert result.exit_code == 0
189 data = json.loads(result.output)
190 assert "file_path" in data
191 assert "content" in data
192 assert "size_bytes" in data
193 assert "source_ref" in data
194 assert "duration_ms" in data
195
196 def test_json_file_path_correct(self, repo: pathlib.Path) -> None:
197 result = runner.invoke(cli, ["cat", "readme.md", "--json"], env=_env(repo))
198 data = json.loads(result.output)
199 assert data["file_path"] == "readme.md"
200
201 def test_json_source_ref_working_tree(self, repo: pathlib.Path) -> None:
202 result = runner.invoke(cli, ["cat", "readme.md", "--json"], env=_env(repo))
203 data = json.loads(result.output)
204 assert data["source_ref"] == "working tree"
205
206 def test_json_size_bytes_accurate(self, repo: pathlib.Path) -> None:
207 result = runner.invoke(cli, ["cat", "readme.md", "--json"], env=_env(repo))
208 data = json.loads(result.output)
209 assert data["size_bytes"] == len(b"# readme\n")
210
211 def test_json_shorthand_flag(self, repo: pathlib.Path) -> None:
212 result = runner.invoke(cli, ["cat", "readme.md", "-j"], env=_env(repo))
213 assert result.exit_code == 0
214 json.loads(result.output) # valid JSON
215
216 def test_subdirectory_file(self, repo: pathlib.Path) -> None:
217 result = runner.invoke(cli, ["cat", "src/main.py"], env=_env(repo))
218 assert result.exit_code == 0
219 assert "HELLO" in result.output
220
221
222 # ---------------------------------------------------------------------------
223 # Integration: multi-file
224 # ---------------------------------------------------------------------------
225
226
227 class TestMultiFile:
228 def test_multi_file_json_has_files_key(self, repo: pathlib.Path) -> None:
229 result = runner.invoke(
230 cli, ["cat", "readme.md", "src/main.py", "--json"], env=_env(repo)
231 )
232 assert result.exit_code == 0
233 data = json.loads(result.output)
234 assert "files" in data
235 assert len(data["files"]) == 2
236
237 def test_multi_file_json_each_has_schema(self, repo: pathlib.Path) -> None:
238 result = runner.invoke(
239 cli, ["cat", "readme.md", "src/main.py", "--json"], env=_env(repo)
240 )
241 data = json.loads(result.output)
242 for entry in data["files"]:
243 assert "file_path" in entry
244 assert "content" in entry
245 assert "size_bytes" in entry
246
247 def test_multi_file_text_prints_all(self, repo: pathlib.Path) -> None:
248 result = runner.invoke(
249 cli, ["cat", "readme.md", "src/main.py"], env=_env(repo)
250 )
251 assert result.exit_code == 0
252 assert "# readme" in result.output
253 assert "HELLO" in result.output
254
255 def test_multi_file_one_missing_exits_nonzero(self, repo: pathlib.Path) -> None:
256 result = runner.invoke(
257 cli, ["cat", "readme.md", "missing.txt", "--json"], env=_env(repo)
258 )
259 assert result.exit_code != 0
260 data = json.loads(result.output)
261 assert len(data["errors"]) == 1
262 assert len(data["files"]) == 1 # the valid file still returned
263
264
265 # ---------------------------------------------------------------------------
266 # Integration: --at ref
267 # ---------------------------------------------------------------------------
268
269
270 class TestAtRef:
271 def test_at_old_commit_shows_old_content(
272 self, two_commit_repo: pathlib.Path
273 ) -> None:
274 log = runner.invoke(cli, ["log", "--json"], env=_env(two_commit_repo))
275 old_cid = json.loads(log.output)["commits"][-1]["commit_id"]
276 result = runner.invoke(
277 cli, ["cat", "src/main.py", "--at", old_cid], env=_env(two_commit_repo)
278 )
279 assert result.exit_code == 0
280 assert "version 1" in result.output
281 assert "updated" not in result.output
282
283 def test_at_json_source_ref_contains_commit(
284 self, two_commit_repo: pathlib.Path
285 ) -> None:
286 log = runner.invoke(cli, ["log", "--json"], env=_env(two_commit_repo))
287 old_cid = json.loads(log.output)["commits"][-1]["commit_id"]
288 result = runner.invoke(
289 cli, ["cat", "src/main.py", "--at", old_cid, "--json"],
290 env=_env(two_commit_repo),
291 )
292 data = json.loads(result.output)
293 assert "commit" in data["source_ref"]
294
295 def test_at_branch_name(self, two_commit_repo: pathlib.Path) -> None:
296 result = runner.invoke(
297 cli, ["cat", "src/main.py", "--at", "main"], env=_env(two_commit_repo)
298 )
299 assert result.exit_code == 0
300 assert "version 2" in result.output
301
302 def test_at_bad_ref_exits_nonzero(self, repo: pathlib.Path) -> None:
303 result = runner.invoke(
304 cli, ["cat", "readme.md", "--at", "deadbeef00"], env=_env(repo)
305 )
306 assert result.exit_code != 0
307
308 def test_at_bad_ref_json_has_error(self, repo: pathlib.Path) -> None:
309 result = runner.invoke(
310 cli, ["cat", "readme.md", "--at", "deadbeef00", "--json"], env=_env(repo)
311 )
312 assert result.exit_code != 0
313 data = json.loads(result.output)
314 assert "error" in data
315
316
317 # ---------------------------------------------------------------------------
318 # E2E: working tree vs historical
319 # ---------------------------------------------------------------------------
320
321
322 class TestE2E:
323 def test_head_shows_latest_content(self, two_commit_repo: pathlib.Path) -> None:
324 result = runner.invoke(
325 cli, ["cat", "src/main.py"], env=_env(two_commit_repo)
326 )
327 assert result.exit_code == 0
328 assert "version 2" in result.output
329
330 def test_old_ref_shows_old_content(self, two_commit_repo: pathlib.Path) -> None:
331 log = runner.invoke(cli, ["log", "--json"], env=_env(two_commit_repo))
332 old_cid = json.loads(log.output)["commits"][-1]["commit_id"]
333 result = runner.invoke(
334 cli, ["cat", "src/main.py", "--at", old_cid], env=_env(two_commit_repo)
335 )
336 assert result.exit_code == 0
337 assert "version 1" in result.output
338
339 def test_working_tree_edit_visible_without_at(
340 self, repo: pathlib.Path
341 ) -> None:
342 """Uncommitted edit on disk should appear when no --at is given."""
343 (repo / "readme.md").write_text("# modified\n", encoding="utf-8")
344 result = runner.invoke(cli, ["cat", "readme.md"], env=_env(repo))
345 assert result.exit_code == 0
346 assert "modified" in result.output
347
348 def test_requires_repo(self, tmp_path: pathlib.Path) -> None:
349 no_repo = tmp_path / "no_repo"
350 no_repo.mkdir()
351 result = runner.invoke(cli, ["cat", "file.py"], env=_env(no_repo))
352 assert result.exit_code != 0
353
354
355 # ---------------------------------------------------------------------------
356 # Security
357 # ---------------------------------------------------------------------------
358
359
360 class TestSecurity:
361 def test_symlink_rejected(self, repo: pathlib.Path) -> None:
362 link = repo / "link.md"
363 link.symlink_to("/etc/passwd")
364 result = runner.invoke(cli, ["cat", "link.md"], env=_env(repo))
365 assert result.exit_code != 0
366
367 def test_path_traversal_rejected(self, repo: pathlib.Path) -> None:
368 result = runner.invoke(
369 cli, ["cat", "../../../etc/passwd"], env=_env(repo)
370 )
371 assert result.exit_code != 0
372
373 def test_ansi_in_path_not_in_output(self, repo: pathlib.Path) -> None:
374 result = runner.invoke(
375 cli, ["cat", "\x1b[31mreadme.md\x1b[0m"], env=_env(repo)
376 )
377 assert result.exit_code != 0
378 assert "\x1b[31m" not in result.output
379
380 def test_newline_in_path_rejected(self, repo: pathlib.Path) -> None:
381 result = runner.invoke(cli, ["cat", "read\nme.md"], env=_env(repo))
382 assert result.exit_code != 0
383
384 def test_null_byte_in_path_rejected(self, repo: pathlib.Path) -> None:
385 result = runner.invoke(cli, ["cat", "read\x00me.md"], env=_env(repo))
386 assert result.exit_code != 0
387
388
389 # ---------------------------------------------------------------------------
390 # Data integrity
391 # ---------------------------------------------------------------------------
392
393
394 class TestDataIntegrity:
395 def test_json_content_equals_disk_bytes(self, repo: pathlib.Path) -> None:
396 result = runner.invoke(cli, ["cat", "readme.md", "--json"], env=_env(repo))
397 data = json.loads(result.output)
398 disk = (repo / "readme.md").read_bytes().decode("utf-8", errors="replace")
399 assert data["content"] == disk
400
401 def test_json_size_bytes_equals_len_of_content_utf8(
402 self, repo: pathlib.Path
403 ) -> None:
404 result = runner.invoke(cli, ["cat", "src/main.py", "--json"], env=_env(repo))
405 data = json.loads(result.output)
406 assert data["size_bytes"] == len(data["content"].encode("utf-8"))
407
408 def test_at_content_differs_from_head(
409 self, two_commit_repo: pathlib.Path
410 ) -> None:
411 log = runner.invoke(cli, ["log", "--json"], env=_env(two_commit_repo))
412 old_cid = json.loads(log.output)["commits"][-1]["commit_id"]
413 head = json.loads(
414 runner.invoke(
415 cli, ["cat", "src/main.py", "--json"], env=_env(two_commit_repo)
416 ).output
417 )["content"]
418 old = json.loads(
419 runner.invoke(
420 cli, ["cat", "src/main.py", "--at", old_cid, "--json"],
421 env=_env(two_commit_repo),
422 ).output
423 )["content"]
424 assert head != old
425
426 def test_multi_file_sizes_sum_correctly(self, repo: pathlib.Path) -> None:
427 result = runner.invoke(
428 cli, ["cat", "readme.md", "src/main.py", "--json"], env=_env(repo)
429 )
430 data = json.loads(result.output)
431 for entry in data["files"]:
432 assert entry["size_bytes"] == len(
433 entry["content"].encode("utf-8", errors="replace")
434 )
435
436 def test_empty_file_handled(self, tmp_path: pathlib.Path) -> None:
437 _init_repo(tmp_path)
438 _commit(tmp_path, {"empty.txt": b""})
439 result = runner.invoke(
440 cli, ["cat", "empty.txt", "--json"], env=_env(tmp_path)
441 )
442 assert result.exit_code == 0
443 data = json.loads(result.output)
444 assert data["content"] == ""
445 assert data["size_bytes"] == 0
446
447
448 # ---------------------------------------------------------------------------
449 # Stress
450 # ---------------------------------------------------------------------------
451
452
453 class TestStress:
454 def test_large_file_1mib(self, tmp_path: pathlib.Path) -> None:
455 _init_repo(tmp_path)
456 content = b"x" * (1024 * 1024)
457 _commit(tmp_path, {"large.bin": content})
458 result = runner.invoke(cli, ["cat", "large.bin"], env=_env(tmp_path))
459 assert result.exit_code == 0
460 assert len(result.output.encode()) >= 1024 * 1024
461
462 def test_10_files_json(self, tmp_path: pathlib.Path) -> None:
463 _init_repo(tmp_path)
464 files = {f"file_{i}.txt": f"content {i}\n".encode() for i in range(10)}
465 _commit(tmp_path, files)
466 args = ["cat"] + list(files.keys()) + ["--json"]
467 result = runner.invoke(cli, args, env=_env(tmp_path))
468 assert result.exit_code == 0
469 data = json.loads(result.output)
470 assert len(data["files"]) == 10
471
472
473 # ---------------------------------------------------------------------------
474 # Performance
475 # ---------------------------------------------------------------------------
476
477
478 class TestPerformance:
479 def test_single_file_under_300ms(self, repo: pathlib.Path) -> None:
480 t0 = time.monotonic()
481 result = runner.invoke(cli, ["cat", "readme.md"], env=_env(repo))
482 elapsed = time.monotonic() - t0
483 assert result.exit_code == 0
484 assert elapsed < 0.3
485
486 def test_10_files_under_1s(self, tmp_path: pathlib.Path) -> None:
487 _init_repo(tmp_path)
488 files = {f"f{i}.txt": f"line {i}\n".encode() for i in range(10)}
489 _commit(tmp_path, files)
490 args = ["cat"] + list(files.keys()) + ["--json"]
491 t0 = time.monotonic()
492 result = runner.invoke(cli, args, env=_env(tmp_path))
493 elapsed = time.monotonic() - t0
494 assert result.exit_code == 0
495 assert elapsed < 1.0
496
497 def test_large_file_json_under_1s(self, tmp_path: pathlib.Path) -> None:
498 _init_repo(tmp_path)
499 content = b"a" * (512 * 1024)
500 _commit(tmp_path, {"half_mib.txt": content})
501 t0 = time.monotonic()
502 result = runner.invoke(
503 cli, ["cat", "half_mib.txt", "--json"], env=_env(tmp_path)
504 )
505 elapsed = time.monotonic() - t0
506 assert result.exit_code == 0
507 assert elapsed < 1.0
508
509
510 class TestRegisterFlags:
511 def test_default_json_out_is_false(self):
512 import argparse
513 from muse.cli.commands.core_cat import register
514 p = argparse.ArgumentParser()
515 subs = p.add_subparsers()
516 register(subs)
517 args = p.parse_args(["cat"])
518 assert args.json_out is False
519
520 def test_json_flag_sets_json_out(self):
521 import argparse
522 from muse.cli.commands.core_cat import register
523 p = argparse.ArgumentParser()
524 subs = p.add_subparsers()
525 register(subs)
526 args = p.parse_args(["cat", "--json"])
527 assert args.json_out is True
528
529 def test_j_shorthand_sets_json_out(self):
530 import argparse
531 from muse.cli.commands.core_cat import register
532 p = argparse.ArgumentParser()
533 subs = p.add_subparsers()
534 register(subs)
535 args = p.parse_args(["cat", "-j"])
536 assert args.json_out is True
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago