gabriel / muse public
test_plumbing_ls_files.py python
321 lines 11.9 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Comprehensive tests for ``muse plumbing ls-files``.
2
3 Coverage tiers
4 --------------
5 - Integration: JSON/text format, --commit, --path-prefix, empty manifest
6 - Security: ANSI in file path stripped in text mode, JSON mode safe
7 - Stress: 1 000-file manifest, 200 sequential calls
8 """
9 from __future__ import annotations
10
11 type _FileStore = dict[str, bytes]
12
13 import datetime
14 import hashlib
15 import json
16 import pathlib
17
18 from muse.core.errors import ExitCode
19 from muse.core.object_store import write_object
20 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
21 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
22 from muse.core._types import Manifest
23 from tests.cli_test_helper import CliRunner, InvokeResult
24
25 runner = CliRunner()
26
27
28 # ---------------------------------------------------------------------------
29 # Helpers
30 # ---------------------------------------------------------------------------
31
32 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
33 repo = tmp_path / "repo"
34 muse = repo / ".muse"
35 for sub in ("objects", "commits", "snapshots", "refs/heads"):
36 (muse / sub).mkdir(parents=True)
37 (muse / "HEAD").write_text("ref: refs/heads/main")
38 (muse / "repo.json").write_text(json.dumps({"repo_id": "test", "domain": "code"}))
39 return repo
40
41
42 def _oid(content: bytes) -> str:
43 return hashlib.sha256(content).hexdigest()
44
45
46 def _add_commit(
47 repo: pathlib.Path,
48 manifest: _FileStore,
49 *,
50 commit_suffix: str = "a",
51 branch: str = "main",
52 set_head: bool = True,
53 ) -> str:
54 """Store objects, snapshot, and commit; return commit_id."""
55 stored: Manifest = {}
56 for path, content in manifest.items():
57 oid = _oid(content)
58 write_object(repo, oid, content)
59 stored[path] = oid
60
61 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
62 snap_id = compute_snapshot_id(stored)
63 write_snapshot(repo, SnapshotRecord(
64 snapshot_id=snap_id,
65 manifest=stored,
66 created_at=committed_at,
67 ))
68 commit_id = compute_commit_id([], snap_id, "test", committed_at.isoformat())
69 write_commit(repo, CommitRecord(
70 commit_id=commit_id,
71 repo_id="test",
72 branch=branch,
73 snapshot_id=snap_id,
74 message="test",
75 committed_at=committed_at,
76 author="tester",
77 parent_commit_id=None,
78 ))
79 if set_head:
80 ref = repo / ".muse" / "refs" / "heads" / branch
81 ref.parent.mkdir(parents=True, exist_ok=True)
82 ref.write_text(commit_id)
83 return commit_id
84
85
86 def _ls(repo: pathlib.Path, *args: str) -> InvokeResult:
87 from muse.cli.app import main as cli
88 return runner.invoke(
89 cli,
90 ["ls-files", *args],
91 env={"MUSE_REPO_ROOT": str(repo)},
92 )
93
94
95 # ---------------------------------------------------------------------------
96 # Integration — JSON format
97 # ---------------------------------------------------------------------------
98
99
100 class TestJsonFormat:
101 def test_lists_files(self, tmp_path: pathlib.Path) -> None:
102 repo = _make_repo(tmp_path)
103 cid = _add_commit(repo, {"src/main.py": b"# main", "README.md": b"# readme"})
104 result = _ls(repo)
105 assert result.exit_code == 0
106 data = json.loads(result.output)
107 assert data["file_count"] == 2
108 paths = [f["path"] for f in data["files"]]
109 assert "src/main.py" in paths
110 assert "README.md" in paths
111
112 def test_files_sorted_alphabetically(self, tmp_path: pathlib.Path) -> None:
113 repo = _make_repo(tmp_path)
114 _add_commit(repo, {"z.py": b"z", "a.py": b"a", "m.py": b"m"})
115 data = json.loads(_ls(repo).output)
116 paths = [f["path"] for f in data["files"]]
117 assert paths == sorted(paths)
118
119 def test_json_has_commit_and_snapshot_id(self, tmp_path: pathlib.Path) -> None:
120 repo = _make_repo(tmp_path)
121 cid = _add_commit(repo, {"f.py": b"x"})
122 data = json.loads(_ls(repo).output)
123 assert data["commit_id"] == cid
124 assert len(data["snapshot_id"]) == 64
125
126 def test_empty_manifest(self, tmp_path: pathlib.Path) -> None:
127 repo = _make_repo(tmp_path)
128 _add_commit(repo, {})
129 data = json.loads(_ls(repo).output)
130 assert data["file_count"] == 0
131 assert data["files"] == []
132
133 def test_json_flag_shorthand(self, tmp_path: pathlib.Path) -> None:
134 repo = _make_repo(tmp_path)
135 _add_commit(repo, {"f.py": b"x"})
136 result = _ls(repo, "--json")
137 assert result.exit_code == 0
138 data = json.loads(result.output)
139 assert data["file_count"] == 1
140
141 def test_object_ids_are_64_hex(self, tmp_path: pathlib.Path) -> None:
142 repo = _make_repo(tmp_path)
143 _add_commit(repo, {"a.py": b"content"})
144 data = json.loads(_ls(repo).output)
145 for f in data["files"]:
146 assert len(f["object_id"]) == 64
147 assert all(c in "0123456789abcdef" for c in f["object_id"])
148
149
150 # ---------------------------------------------------------------------------
151 # Integration — text format
152 # ---------------------------------------------------------------------------
153
154
155 class TestTextFormat:
156 def test_text_tab_separated(self, tmp_path: pathlib.Path) -> None:
157 repo = _make_repo(tmp_path)
158 _add_commit(repo, {"hello.py": b"hi"})
159 result = _ls(repo, "--format", "text")
160 assert result.exit_code == 0
161 line = result.output.strip()
162 parts = line.split("\t")
163 assert len(parts) == 2
164 assert len(parts[0]) == 64 # object_id
165 assert parts[1] == "hello.py"
166
167 def test_text_oid_matches_json_oid(self, tmp_path: pathlib.Path) -> None:
168 repo = _make_repo(tmp_path)
169 _add_commit(repo, {"check.py": b"content"})
170 json_data = json.loads(_ls(repo).output)
171 text_out = _ls(repo, "--format", "text").output.strip()
172 json_oid = json_data["files"][0]["object_id"]
173 text_oid = text_out.split("\t")[0]
174 assert json_oid == text_oid
175
176
177 # ---------------------------------------------------------------------------
178 # Integration — --commit flag
179 # ---------------------------------------------------------------------------
180
181
182 class TestCommitFlag:
183 def test_explicit_commit_resolves(self, tmp_path: pathlib.Path) -> None:
184 repo = _make_repo(tmp_path)
185 cid = _add_commit(repo, {"explicit.py": b"content"})
186 result = _ls(repo, "--commit", cid)
187 assert result.exit_code == 0
188 data = json.loads(result.output)
189 assert data["commit_id"] == cid
190
191 def test_invalid_commit_id_errors(self, tmp_path: pathlib.Path) -> None:
192 repo = _make_repo(tmp_path)
193 result = _ls(repo, "--commit", "not-a-valid-id")
194 assert result.exit_code == ExitCode.USER_ERROR
195
196 def test_nonexistent_commit_id_errors(self, tmp_path: pathlib.Path) -> None:
197 repo = _make_repo(tmp_path)
198 result = _ls(repo, "--commit", "f" * 64)
199 assert result.exit_code == ExitCode.USER_ERROR
200
201 def test_no_commits_on_branch_errors(self, tmp_path: pathlib.Path) -> None:
202 repo = _make_repo(tmp_path)
203 result = _ls(repo)
204 assert result.exit_code == ExitCode.USER_ERROR
205
206
207 # ---------------------------------------------------------------------------
208 # Integration — --path-prefix filter
209 # ---------------------------------------------------------------------------
210
211
212 class TestPathPrefix:
213 def test_prefix_filters_to_subtree(self, tmp_path: pathlib.Path) -> None:
214 repo = _make_repo(tmp_path)
215 _add_commit(repo, {
216 "src/main.py": b"main",
217 "src/utils.py": b"utils",
218 "tests/test_main.py": b"test",
219 "README.md": b"readme",
220 })
221 data = json.loads(_ls(repo, "--path-prefix", "src/").output)
222 paths = [f["path"] for f in data["files"]]
223 assert all(p.startswith("src/") for p in paths)
224 assert len(paths) == 2
225
226 def test_prefix_file_count_reflects_filter(self, tmp_path: pathlib.Path) -> None:
227 repo = _make_repo(tmp_path)
228 _add_commit(repo, {
229 "a/x.py": b"x",
230 "a/y.py": b"y",
231 "b/z.py": b"z",
232 })
233 data = json.loads(_ls(repo, "--path-prefix", "a/").output)
234 assert data["file_count"] == 2
235
236 def test_prefix_no_match_returns_empty(self, tmp_path: pathlib.Path) -> None:
237 repo = _make_repo(tmp_path)
238 _add_commit(repo, {"src/main.py": b"main"})
239 data = json.loads(_ls(repo, "--path-prefix", "tests/").output)
240 assert data["file_count"] == 0
241 assert data["files"] == []
242
243 def test_prefix_text_format(self, tmp_path: pathlib.Path) -> None:
244 repo = _make_repo(tmp_path)
245 _add_commit(repo, {"src/a.py": b"a", "tests/b.py": b"b"})
246 result = _ls(repo, "--path-prefix", "src/", "--format", "text")
247 assert result.exit_code == 0
248 lines = [l for l in result.output.strip().splitlines() if l]
249 assert len(lines) == 1
250 assert "src/a.py" in lines[0]
251
252
253 # ---------------------------------------------------------------------------
254 # Security
255 # ---------------------------------------------------------------------------
256
257
258 class TestSecurity:
259 def test_ansi_in_path_stripped_in_text_mode(self, tmp_path: pathlib.Path) -> None:
260 """File path with ANSI escape must be sanitized in text mode."""
261 repo = _make_repo(tmp_path)
262 evil_path = "src/\x1b[31mevil\x1b[0m.py"
263 _add_commit(repo, {evil_path: b"content"})
264 result = _ls(repo, "--format", "text")
265 assert result.exit_code == 0
266 assert "\x1b" not in result.output
267
268 def test_ansi_in_path_preserved_in_json(self, tmp_path: pathlib.Path) -> None:
269 """JSON mode encodes ANSI as \\u001b — never emits raw escape sequences."""
270 repo = _make_repo(tmp_path)
271 evil_path = "src/\x1b[31mevil\x1b[0m.py"
272 _add_commit(repo, {evil_path: b"content"})
273 result = _ls(repo)
274 assert result.exit_code == 0
275 assert "\x1b" not in result.output
276 data = json.loads(result.output)
277 # The path is in the JSON — safely encoded as \u001b
278 assert any("\u001b" in f["path"] or "\x1b" not in f["path"] for f in data["files"])
279
280 def test_path_traversal_commit_id_rejected(self, tmp_path: pathlib.Path) -> None:
281 repo = _make_repo(tmp_path)
282 result = _ls(repo, "--commit", "../../../etc/passwd")
283 assert result.exit_code == ExitCode.USER_ERROR
284
285 def test_no_traceback_on_invalid_input(self, tmp_path: pathlib.Path) -> None:
286 repo = _make_repo(tmp_path)
287 result = _ls(repo, "--commit", "bad!")
288 assert "Traceback" not in result.output
289
290
291 # ---------------------------------------------------------------------------
292 # Stress
293 # ---------------------------------------------------------------------------
294
295
296 class TestStress:
297 def test_1000_file_manifest(self, tmp_path: pathlib.Path) -> None:
298 """1 000-file manifest lists and returns in reasonable time."""
299 repo = _make_repo(tmp_path)
300 manifest = {f"src/file_{i:04d}.py": f"content {i}".encode() for i in range(1000)}
301 _add_commit(repo, manifest)
302 result = _ls(repo)
303 assert result.exit_code == 0
304 data = json.loads(result.output)
305 assert data["file_count"] == 1000
306
307 def test_1000_file_prefix_filter(self, tmp_path: pathlib.Path) -> None:
308 repo = _make_repo(tmp_path)
309 manifest = {f"a/file_{i:04d}.py": b"a" for i in range(500)}
310 manifest.update({f"b/file_{i:04d}.py": b"b" for i in range(500)})
311 _add_commit(repo, manifest)
312 data = json.loads(_ls(repo, "--path-prefix", "a/").output)
313 assert data["file_count"] == 500
314
315 def test_200_sequential_calls(self, tmp_path: pathlib.Path) -> None:
316 repo = _make_repo(tmp_path)
317 _add_commit(repo, {"stable.py": b"content"})
318 for i in range(200):
319 result = _ls(repo)
320 assert result.exit_code == 0, f"failed at iteration {i}"
321 assert json.loads(result.output)["file_count"] == 1
File History 3 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago