gabriel / muse public
test_plumbing_ls_files.py python
234 lines 8.5 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Tests for ``muse plumbing ls-files``.
2
3 Covers: default HEAD listing, explicit ``--commit`` flag, text-format output,
4 empty manifest, commit-not-found and snapshot-not-found error paths, sorted
5 output, and a stress case with a 500-file manifest.
6 """
7
8 from __future__ import annotations
9
10 import datetime
11 import hashlib
12 import json
13 import pathlib
14
15 from tests.cli_test_helper import CliRunner
16
17 cli = None # argparse migration — CliRunner ignores this arg
18 from muse.core.errors import ExitCode
19 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
20
21 runner = CliRunner()
22
23
24 # ---------------------------------------------------------------------------
25 # Helpers
26 # ---------------------------------------------------------------------------
27
28
29 def _sha(tag: str) -> str:
30 return hashlib.sha256(tag.encode()).hexdigest()
31
32
33 def _init_repo(path: pathlib.Path) -> pathlib.Path:
34 muse = path / ".muse"
35 (muse / "commits").mkdir(parents=True)
36 (muse / "snapshots").mkdir(parents=True)
37 (muse / "objects").mkdir(parents=True)
38 (muse / "refs" / "heads").mkdir(parents=True)
39 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
40 (muse / "repo.json").write_text(
41 json.dumps({"repo_id": "test-repo", "domain": "midi"}), encoding="utf-8"
42 )
43 return path
44
45
46 def _env(repo: pathlib.Path) -> dict[str, str]:
47 return {"MUSE_REPO_ROOT": str(repo)}
48
49
50 def _snap(repo: pathlib.Path, manifest: dict[str, str], tag: str = "s") -> str:
51 sid = _sha(f"snap-{tag}-{json.dumps(sorted(manifest.items()))}")
52 write_snapshot(
53 repo,
54 SnapshotRecord(
55 snapshot_id=sid,
56 manifest=manifest,
57 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
58 ),
59 )
60 return sid
61
62
63 def _commit(
64 repo: pathlib.Path, tag: str, sid: str, branch: str = "main", parent: str | None = None
65 ) -> str:
66 cid = _sha(tag)
67 write_commit(
68 repo,
69 CommitRecord(
70 commit_id=cid,
71 repo_id="test-repo",
72 branch=branch,
73 snapshot_id=sid,
74 message=tag,
75 committed_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
76 author="tester",
77 parent_commit_id=parent,
78 ),
79 )
80 ref = repo / ".muse" / "refs" / "heads" / branch
81 ref.parent.mkdir(parents=True, exist_ok=True)
82 ref.write_text(cid, encoding="utf-8")
83 return cid
84
85
86 # ---------------------------------------------------------------------------
87 # Integration: default HEAD
88 # ---------------------------------------------------------------------------
89
90
91 class TestLsFilesHead:
92 def test_default_lists_head_manifest(self, tmp_path: pathlib.Path) -> None:
93 repo = _init_repo(tmp_path)
94 manifest = {"tracks/drums.mid": _sha("drums"), "tracks/bass.mid": _sha("bass")}
95 sid = _snap(repo, manifest)
96 cid = _commit(repo, "c1", sid)
97 result = runner.invoke(cli, ["plumbing", "ls-files"], env=_env(repo))
98 assert result.exit_code == 0, result.output
99 data = json.loads(result.stdout)
100 assert data["commit_id"] == cid
101 assert data["snapshot_id"] == sid
102 assert data["file_count"] == 2
103 paths = {f["path"] for f in data["files"]}
104 assert paths == set(manifest.keys())
105
106 def test_object_ids_match_manifest(self, tmp_path: pathlib.Path) -> None:
107 repo = _init_repo(tmp_path)
108 oid = _sha("drums-content")
109 sid = _snap(repo, {"drums.mid": oid})
110 _commit(repo, "c2", sid)
111 result = runner.invoke(cli, ["plumbing", "ls-files"], env=_env(repo))
112 assert result.exit_code == 0
113 data = json.loads(result.stdout)
114 assert data["files"][0]["object_id"] == oid
115
116 def test_empty_manifest_reports_zero_files(self, tmp_path: pathlib.Path) -> None:
117 repo = _init_repo(tmp_path)
118 sid = _snap(repo, {})
119 _commit(repo, "empty", sid)
120 result = runner.invoke(cli, ["plumbing", "ls-files"], env=_env(repo))
121 assert result.exit_code == 0
122 data = json.loads(result.stdout)
123 assert data["file_count"] == 0
124 assert data["files"] == []
125
126 def test_no_commits_exits_user_error(self, tmp_path: pathlib.Path) -> None:
127 repo = _init_repo(tmp_path)
128 result = runner.invoke(cli, ["plumbing", "ls-files"], env=_env(repo))
129 assert result.exit_code == ExitCode.USER_ERROR
130
131
132 # ---------------------------------------------------------------------------
133 # Integration: --commit flag
134 # ---------------------------------------------------------------------------
135
136
137 class TestLsFilesCommitFlag:
138 def test_explicit_commit_lists_that_commits_manifest(self, tmp_path: pathlib.Path) -> None:
139 repo = _init_repo(tmp_path)
140 oid_a = _sha("a")
141 oid_b = _sha("b")
142 sid1 = _snap(repo, {"a.mid": oid_a}, "s1")
143 sid2 = _snap(repo, {"b.mid": oid_b}, "s2")
144 cid1 = _commit(repo, "c1", sid1)
145 cid2 = _commit(repo, "c2", sid2, parent=cid1)
146 result = runner.invoke(
147 cli, ["plumbing", "ls-files", "--commit", cid1], env=_env(repo)
148 )
149 assert result.exit_code == 0
150 data = json.loads(result.stdout)
151 assert data["commit_id"] == cid1
152 assert {f["path"] for f in data["files"]} == {"a.mid"}
153
154 def test_short_commit_flag(self, tmp_path: pathlib.Path) -> None:
155 repo = _init_repo(tmp_path)
156 sid = _snap(repo, {"x.mid": _sha("x")})
157 cid = _commit(repo, "cx", sid)
158 result = runner.invoke(cli, ["plumbing", "ls-files", "-c", cid], env=_env(repo))
159 assert result.exit_code == 0
160 assert json.loads(result.stdout)["commit_id"] == cid
161
162 def test_nonexistent_commit_exits_user_error(self, tmp_path: pathlib.Path) -> None:
163 repo = _init_repo(tmp_path)
164 ghost = _sha("ghost")
165 result = runner.invoke(cli, ["plumbing", "ls-files", "--commit", ghost], env=_env(repo))
166 assert result.exit_code == ExitCode.USER_ERROR
167 assert "error" in json.loads(result.stdout)
168
169
170 # ---------------------------------------------------------------------------
171 # Integration: output formats
172 # ---------------------------------------------------------------------------
173
174
175 class TestLsFilesFormats:
176 def test_text_format_shows_tab_separated_lines(self, tmp_path: pathlib.Path) -> None:
177 repo = _init_repo(tmp_path)
178 oid = _sha("t")
179 sid = _snap(repo, {"track.mid": oid})
180 _commit(repo, "tf", sid)
181 result = runner.invoke(
182 cli, ["plumbing", "ls-files", "--format", "text"], env=_env(repo)
183 )
184 assert result.exit_code == 0
185 assert "\t" in result.stdout
186 assert "track.mid" in result.stdout
187
188 def test_text_format_short_flag(self, tmp_path: pathlib.Path) -> None:
189 repo = _init_repo(tmp_path)
190 sid = _snap(repo, {"f.mid": _sha("f")})
191 _commit(repo, "ftf", sid)
192 result = runner.invoke(cli, ["plumbing", "ls-files", "-f", "text"], env=_env(repo))
193 assert result.exit_code == 0
194
195 def test_bad_format_exits_user_error(self, tmp_path: pathlib.Path) -> None:
196 repo = _init_repo(tmp_path)
197 sid = _snap(repo, {})
198 _commit(repo, "bfmt", sid)
199 result = runner.invoke(
200 cli, ["plumbing", "ls-files", "--format", "csv"], env=_env(repo)
201 )
202 assert result.exit_code == ExitCode.USER_ERROR
203
204 def test_files_sorted_lexicographically(self, tmp_path: pathlib.Path) -> None:
205 repo = _init_repo(tmp_path)
206 manifest = {
207 "zzz/z.mid": _sha("z"),
208 "aaa/a.mid": _sha("a"),
209 "mmm/m.mid": _sha("m"),
210 }
211 sid = _snap(repo, manifest)
212 _commit(repo, "sorted", sid)
213 result = runner.invoke(cli, ["plumbing", "ls-files"], env=_env(repo))
214 assert result.exit_code == 0
215 paths = [f["path"] for f in json.loads(result.stdout)["files"]]
216 assert paths == sorted(paths)
217
218
219 # ---------------------------------------------------------------------------
220 # Stress: 500-file manifest
221 # ---------------------------------------------------------------------------
222
223
224 class TestLsFilesStress:
225 def test_500_file_manifest_all_reported(self, tmp_path: pathlib.Path) -> None:
226 repo = _init_repo(tmp_path)
227 manifest = {f"track_{i:04d}.mid": _sha(f"oid-{i}") for i in range(500)}
228 sid = _snap(repo, manifest)
229 _commit(repo, "big-manifest", sid)
230 result = runner.invoke(cli, ["plumbing", "ls-files"], env=_env(repo))
231 assert result.exit_code == 0
232 data = json.loads(result.stdout)
233 assert data["file_count"] == 500
234 assert len(data["files"]) == 500