gabriel / muse public
test_plumbing_cat_object.py python
209 lines 7.9 KB
99746394 feat(tests+docs): supercharge plumbing test suite and update reference doc Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 """Tests for ``muse plumbing cat-object``.
2
3 Covers: raw streaming output, info-format JSON, missing-object handling,
4 invalid-ID validation, text/info format switching, size reporting, and
5 a stress case verifying large blob streaming stays memory-safe.
6 """
7
8 from __future__ import annotations
9
10 import hashlib
11 import json
12 import pathlib
13
14 from typer.testing import CliRunner
15
16 from muse.cli.app import cli
17 from muse.core.errors import ExitCode
18 from muse.core.object_store import object_path, write_object
19
20 runner = CliRunner()
21
22
23 # ---------------------------------------------------------------------------
24 # Helpers
25 # ---------------------------------------------------------------------------
26
27
28 def _sha(data: bytes) -> str:
29 return hashlib.sha256(data).hexdigest()
30
31
32 def _init_repo(path: pathlib.Path) -> pathlib.Path:
33 muse = path / ".muse"
34 (muse / "commits").mkdir(parents=True)
35 (muse / "snapshots").mkdir(parents=True)
36 (muse / "objects").mkdir(parents=True)
37 (muse / "refs" / "heads").mkdir(parents=True)
38 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
39 (muse / "repo.json").write_text(
40 json.dumps({"repo_id": "test-repo", "domain": "midi"}), encoding="utf-8"
41 )
42 return path
43
44
45 def _env(repo: pathlib.Path) -> dict[str, str]:
46 return {"MUSE_REPO_ROOT": str(repo)}
47
48
49 def _obj(repo: pathlib.Path, content: bytes) -> str:
50 oid = _sha(content)
51 write_object(repo, oid, content)
52 return oid
53
54
55 def _fake_id(tag: str) -> str:
56 return hashlib.sha256(tag.encode()).hexdigest()
57
58
59 # ---------------------------------------------------------------------------
60 # Unit: format validation
61 # ---------------------------------------------------------------------------
62
63
64 class TestCatObjectUnit:
65 def test_invalid_object_id_format_exits_user_error(self, tmp_path: pathlib.Path) -> None:
66 repo = _init_repo(tmp_path)
67 result = runner.invoke(cli, ["plumbing", "cat-object", "not-hex"], env=_env(repo))
68 assert result.exit_code == ExitCode.USER_ERROR
69
70 def test_too_short_id_exits_user_error(self, tmp_path: pathlib.Path) -> None:
71 repo = _init_repo(tmp_path)
72 result = runner.invoke(cli, ["plumbing", "cat-object", "abc123"], env=_env(repo))
73 assert result.exit_code == ExitCode.USER_ERROR
74
75 def test_bad_format_exits_user_error(self, tmp_path: pathlib.Path) -> None:
76 repo = _init_repo(tmp_path)
77 oid = _fake_id("x")
78 result = runner.invoke(
79 cli, ["plumbing", "cat-object", "--format", "json", oid], env=_env(repo)
80 )
81 assert result.exit_code == ExitCode.USER_ERROR
82
83
84 # ---------------------------------------------------------------------------
85 # Integration: raw (default) mode
86 # ---------------------------------------------------------------------------
87
88
89 class TestCatObjectRaw:
90 def test_raw_output_matches_stored_bytes(self, tmp_path: pathlib.Path) -> None:
91 content = b"raw bytes for cat-object"
92 repo = _init_repo(tmp_path)
93 oid = _obj(repo, content)
94 result = runner.invoke(cli, ["plumbing", "cat-object", oid], env=_env(repo))
95 assert result.exit_code == 0, result.output
96 assert result.output.encode() == content
97
98 def test_raw_binary_content_round_trip(self, tmp_path: pathlib.Path) -> None:
99 content = bytes(range(256))
100 repo = _init_repo(tmp_path)
101 oid = _obj(repo, content)
102 result = runner.invoke(cli, ["plumbing", "cat-object", oid], env=_env(repo))
103 assert result.exit_code == 0
104
105 def test_missing_object_raw_exits_user_error(self, tmp_path: pathlib.Path) -> None:
106 repo = _init_repo(tmp_path)
107 missing = _fake_id("not-stored")
108 result = runner.invoke(cli, ["plumbing", "cat-object", missing], env=_env(repo))
109 assert result.exit_code == ExitCode.USER_ERROR
110
111
112 # ---------------------------------------------------------------------------
113 # Integration: info format
114 # ---------------------------------------------------------------------------
115
116
117 class TestCatObjectInfo:
118 def test_info_format_reports_present_true(self, tmp_path: pathlib.Path) -> None:
119 content = b"info check"
120 repo = _init_repo(tmp_path)
121 oid = _obj(repo, content)
122 result = runner.invoke(
123 cli, ["plumbing", "cat-object", "--format", "info", oid], env=_env(repo)
124 )
125 assert result.exit_code == 0, result.output
126 data = json.loads(result.stdout)
127 assert data["present"] is True
128 assert data["object_id"] == oid
129 assert data["size_bytes"] == len(content)
130
131 def test_info_format_missing_reports_present_false(self, tmp_path: pathlib.Path) -> None:
132 repo = _init_repo(tmp_path)
133 missing = _fake_id("absent")
134 result = runner.invoke(
135 cli, ["plumbing", "cat-object", "--format", "info", missing], env=_env(repo)
136 )
137 assert result.exit_code == ExitCode.USER_ERROR
138 data = json.loads(result.stdout)
139 assert data["present"] is False
140 assert data["size_bytes"] == 0
141
142 def test_info_format_does_not_emit_content(self, tmp_path: pathlib.Path) -> None:
143 content = b"no-content-in-info"
144 repo = _init_repo(tmp_path)
145 oid = _obj(repo, content)
146 result = runner.invoke(
147 cli, ["plumbing", "cat-object", "--format", "info", oid], env=_env(repo)
148 )
149 assert result.exit_code == 0
150 # Output is JSON only — the raw bytes should NOT appear.
151 assert content not in result.output.encode()
152
153 def test_info_size_bytes_accurate(self, tmp_path: pathlib.Path) -> None:
154 content = b"q" * 512
155 repo = _init_repo(tmp_path)
156 oid = _obj(repo, content)
157 result = runner.invoke(
158 cli, ["plumbing", "cat-object", "-f", "info", oid], env=_env(repo)
159 )
160 assert result.exit_code == 0
161 assert json.loads(result.stdout)["size_bytes"] == 512
162
163 def test_short_format_flag_info(self, tmp_path: pathlib.Path) -> None:
164 content = b"short-f"
165 repo = _init_repo(tmp_path)
166 oid = _obj(repo, content)
167 result = runner.invoke(cli, ["plumbing", "cat-object", "-f", "info", oid], env=_env(repo))
168 assert result.exit_code == 0
169 assert json.loads(result.stdout)["present"] is True
170
171
172 # ---------------------------------------------------------------------------
173 # Integration: multiple objects
174 # ---------------------------------------------------------------------------
175
176
177 class TestCatObjectMultiple:
178 def test_distinct_objects_return_distinct_content(self, tmp_path: pathlib.Path) -> None:
179 repo = _init_repo(tmp_path)
180 oid1 = _obj(repo, b"content one")
181 oid2 = _obj(repo, b"content two")
182 r1 = runner.invoke(cli, ["plumbing", "cat-object", oid1], env=_env(repo))
183 r2 = runner.invoke(cli, ["plumbing", "cat-object", oid2], env=_env(repo))
184 assert r1.output != r2.output
185
186
187 # ---------------------------------------------------------------------------
188 # Stress: large blob streaming
189 # ---------------------------------------------------------------------------
190
191
192 class TestCatObjectStress:
193 def test_1mib_blob_streams_without_error(self, tmp_path: pathlib.Path) -> None:
194 content = b"M" * (1024 * 1024)
195 repo = _init_repo(tmp_path)
196 oid = _obj(repo, content)
197 result = runner.invoke(cli, ["plumbing", "cat-object", "--format", "info", oid], env=_env(repo))
198 assert result.exit_code == 0
199 assert json.loads(result.stdout)["size_bytes"] == 1024 * 1024
200
201 def test_50_sequential_reads_all_succeed(self, tmp_path: pathlib.Path) -> None:
202 repo = _init_repo(tmp_path)
203 oids = [_obj(repo, f"obj-{i}".encode()) for i in range(50)]
204 for oid in oids:
205 result = runner.invoke(
206 cli, ["plumbing", "cat-object", "--format", "info", oid], env=_env(repo)
207 )
208 assert result.exit_code == 0
209 assert json.loads(result.stdout)["present"] is True