gabriel / muse public
test_plumbing_for_each_ref.py python
183 lines 6.2 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Tests for muse plumbing for-each-ref."""
2
3 from __future__ import annotations
4
5 import datetime
6 import hashlib
7 import json
8 import pathlib
9
10 import pytest
11 from tests.cli_test_helper import CliRunner
12
13 cli = None # argparse migration — CliRunner ignores this arg
14 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
15
16 runner = CliRunner()
17
18
19 def _sha(tag: str) -> str:
20 return hashlib.sha256(tag.encode()).hexdigest()
21
22
23 def _init_repo(path: pathlib.Path) -> pathlib.Path:
24 muse = path / ".muse"
25 (muse / "commits").mkdir(parents=True)
26 (muse / "snapshots").mkdir(parents=True)
27 (muse / "objects").mkdir(parents=True)
28 (muse / "refs" / "heads").mkdir(parents=True)
29 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
30 (muse / "repo.json").write_text(
31 json.dumps({"repo_id": "test-repo", "domain": "midi"}), encoding="utf-8"
32 )
33 return path
34
35
36 def _env(repo: pathlib.Path) -> dict[str, str]:
37 return {"MUSE_REPO_ROOT": str(repo)}
38
39
40 def _snap(repo: pathlib.Path, tag: str = "snap") -> str:
41 sid = _sha(tag)
42 write_snapshot(
43 repo,
44 SnapshotRecord(
45 snapshot_id=sid,
46 manifest={},
47 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
48 ),
49 )
50 return sid
51
52
53 def _commit(
54 repo: pathlib.Path,
55 tag: str,
56 branch: str = "main",
57 parent: str | None = None,
58 ts: datetime.datetime | None = None,
59 ) -> str:
60 sid = _snap(repo, tag)
61 cid = _sha(tag)
62 write_commit(
63 repo,
64 CommitRecord(
65 commit_id=cid,
66 repo_id="test-repo",
67 branch=branch,
68 snapshot_id=sid,
69 message=tag,
70 committed_at=ts or datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
71 author="tester",
72 parent_commit_id=parent,
73 parent2_commit_id=None,
74 ),
75 )
76 ref_path = repo / ".muse" / "refs" / "heads" / branch
77 ref_path.parent.mkdir(parents=True, exist_ok=True)
78 ref_path.write_text(cid, encoding="utf-8")
79 return cid
80
81
82 class TestForEachRef:
83 def test_empty_repo_returns_empty_list(self, tmp_path: pathlib.Path) -> None:
84 _init_repo(tmp_path)
85 result = runner.invoke(cli, ["plumbing", "for-each-ref"], env=_env(tmp_path))
86 assert result.exit_code == 0
87 data = json.loads(result.output)
88 assert data["count"] == 0
89 assert data["refs"] == []
90
91 def test_after_commit_lists_ref(self, tmp_path: pathlib.Path) -> None:
92 _init_repo(tmp_path)
93 cid = _commit(tmp_path, "c1")
94 result = runner.invoke(cli, ["plumbing", "for-each-ref"], env=_env(tmp_path))
95 assert result.exit_code == 0
96 data = json.loads(result.output)
97 assert data["count"] == 1
98 assert data["refs"][0]["commit_id"] == cid
99
100 def test_ref_detail_fields(self, tmp_path: pathlib.Path) -> None:
101 _init_repo(tmp_path)
102 _commit(tmp_path, "c1")
103 result = runner.invoke(cli, ["plumbing", "for-each-ref"], env=_env(tmp_path))
104 assert result.exit_code == 0
105 data = json.loads(result.output)
106 ref = data["refs"][0]
107 for key in ("ref", "branch", "commit_id", "author", "message", "committed_at", "snapshot_id"):
108 assert key in ref, f"missing field: {key}"
109
110 def test_sort_by_ref(self, tmp_path: pathlib.Path) -> None:
111 _init_repo(tmp_path)
112 _commit(tmp_path, "c_dev", "dev")
113 _commit(tmp_path, "c_feat", "feat")
114 result = runner.invoke(
115 cli, ["plumbing", "for-each-ref", "--sort", "ref"], env=_env(tmp_path)
116 )
117 assert result.exit_code == 0
118 data = json.loads(result.output)
119 refs = [r["ref"] for r in data["refs"]]
120 assert refs == sorted(refs)
121
122 def test_sort_desc(self, tmp_path: pathlib.Path) -> None:
123 _init_repo(tmp_path)
124 _commit(tmp_path, "c_dev", "dev")
125 _commit(tmp_path, "c_feat", "feat")
126 result = runner.invoke(
127 cli, ["plumbing", "for-each-ref", "--sort", "ref", "--desc"], env=_env(tmp_path)
128 )
129 assert result.exit_code == 0
130 data = json.loads(result.output)
131 refs = [r["ref"] for r in data["refs"]]
132 assert refs == sorted(refs, reverse=True)
133
134 def test_count_limits_output(self, tmp_path: pathlib.Path) -> None:
135 _init_repo(tmp_path)
136 _commit(tmp_path, "c_dev", "dev")
137 _commit(tmp_path, "c_feat", "feat")
138 result = runner.invoke(
139 cli, ["plumbing", "for-each-ref", "--count", "1"], env=_env(tmp_path)
140 )
141 assert result.exit_code == 0
142 data = json.loads(result.output)
143 assert data["count"] == 1
144 assert len(data["refs"]) == 1
145
146 def test_pattern_filter(self, tmp_path: pathlib.Path) -> None:
147 _init_repo(tmp_path)
148 _commit(tmp_path, "c_main", "main")
149 _commit(tmp_path, "c_feat", "feat/new")
150 result = runner.invoke(
151 cli, ["plumbing", "for-each-ref", "--pattern", "refs/heads/feat/*"], env=_env(tmp_path)
152 )
153 assert result.exit_code == 0
154 data = json.loads(result.output)
155 for ref in data["refs"]:
156 assert ref["ref"].startswith("refs/heads/feat/")
157
158 def test_text_format(self, tmp_path: pathlib.Path) -> None:
159 _init_repo(tmp_path)
160 _commit(tmp_path, "c1")
161 result = runner.invoke(
162 cli, ["plumbing", "for-each-ref", "--format", "text"], env=_env(tmp_path)
163 )
164 assert result.exit_code == 0
165 lines = [l for l in result.output.strip().splitlines() if l]
166 assert len(lines) == 1
167 assert "refs/heads/" in lines[0]
168
169 def test_invalid_sort_field(self, tmp_path: pathlib.Path) -> None:
170 _init_repo(tmp_path)
171 result = runner.invoke(
172 cli, ["plumbing", "for-each-ref", "--sort", "nonexistent"], env=_env(tmp_path)
173 )
174 assert result.exit_code != 0
175 data = json.loads(result.output)
176 assert "error" in data
177
178 def test_invalid_format(self, tmp_path: pathlib.Path) -> None:
179 _init_repo(tmp_path)
180 result = runner.invoke(
181 cli, ["plumbing", "for-each-ref", "--format", "yaml"], env=_env(tmp_path)
182 )
183 assert result.exit_code != 0