gabriel / muse public
test_cmd_reflog.py python
221 lines 8.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Comprehensive tests for ``muse reflog``.
2
3 Covers:
4 - Unit: _fmt_entry sanitizes operation field
5 - Integration: reflog populated by commits, --all flag
6 - E2E: full CLI via CliRunner
7 - Security: branch name validated before use as path, operation sanitized
8 - Stress: large reflog with limit
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16
17 import pytest
18 from tests.cli_test_helper import CliRunner
19 from muse.core._types import fake_id
20
21 cli = None # argparse migration — CliRunner ignores this arg
22
23 runner = CliRunner()
24
25
26 # ---------------------------------------------------------------------------
27 # Helpers
28 # ---------------------------------------------------------------------------
29
30 def _env(root: pathlib.Path) -> Manifest:
31 return {"MUSE_REPO_ROOT": str(root)}
32
33
34 def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
35 muse_dir = tmp_path / ".muse"
36 muse_dir.mkdir()
37 repo_id = fake_id("repo")
38 (muse_dir / "repo.json").write_text(json.dumps({
39 "repo_id": repo_id,
40 "domain": "midi",
41 "default_branch": "main",
42 "created_at": "2025-01-01T00:00:00+00:00",
43 }), encoding="utf-8")
44 (muse_dir / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
45 (muse_dir / "refs" / "heads").mkdir(parents=True)
46 (muse_dir / "snapshots").mkdir()
47 (muse_dir / "commits").mkdir()
48 (muse_dir / "objects").mkdir()
49 return tmp_path, repo_id
50
51
52 def _make_commit_with_reflog(
53 root: pathlib.Path, repo_id: str, message: str = "commit", branch: str = "main"
54 ) -> str:
55 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
56 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
57 from muse.core.reflog import append_reflog
58
59 ref_file = root / ".muse" / "refs" / "heads" / branch
60 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
61 manifest: Manifest = {}
62 snap_id = compute_snapshot_id(manifest)
63 committed_at = datetime.datetime.now(datetime.timezone.utc)
64 commit_id = compute_commit_id(
65 repo_id=repo_id,
66 parent_ids=[parent_id] if parent_id else [],
67 snapshot_id=snap_id, message=message,
68 committed_at_iso=committed_at.isoformat(),
69 )
70 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
71 write_commit(root, CommitRecord(
72 commit_id=commit_id, repo_id=repo_id, created_on_branch=branch,
73 snapshot_id=snap_id, message=message, committed_at=committed_at,
74 parent_commit_id=parent_id,
75 ))
76 ref_file.parent.mkdir(parents=True, exist_ok=True)
77 ref_file.write_text(commit_id, encoding="utf-8")
78 append_reflog(root, branch, old_id=parent_id or "0" * 64, new_id=commit_id,
79 author="user", operation=f"commit: {message}")
80 return commit_id
81
82
83 # ---------------------------------------------------------------------------
84 # Unit tests
85 # ---------------------------------------------------------------------------
86
87
88 class TestRegisterFlags:
89 def _parse(self, *args: str) -> "argparse.Namespace":
90 import argparse
91 from muse.cli.commands.reflog import register
92 p = argparse.ArgumentParser()
93 sub = p.add_subparsers()
94 register(sub)
95 return p.parse_args(["reflog", *args])
96
97 def test_default_json_out_is_false(self) -> None:
98 ns = self._parse()
99 assert ns.json_out is False
100
101 def test_json_flag_sets_json_out(self) -> None:
102 ns = self._parse("--json")
103 assert ns.json_out is True
104
105 def test_j_shorthand_sets_json_out(self) -> None:
106 ns = self._parse("-j")
107 assert ns.json_out is True
108
109
110 class TestReflogUnit:
111 def test_fmt_entry_sanitizes_operation(self) -> None:
112 from muse.cli.commands.reflog import _fmt_entry
113 from muse.core.reflog import ReflogEntry
114
115 entry = ReflogEntry(
116 old_id="0" * 64, new_id="a" * 64, author="user",
117 timestamp=datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
118 operation="commit: Hello\x1b[31mRED\x1b[0m",
119 )
120 result = _fmt_entry(0, entry)
121 assert "\x1b" not in result
122
123 def test_fmt_entry_initial_shown_as_initial(self) -> None:
124 from muse.cli.commands.reflog import _fmt_entry
125 from muse.core.reflog import ReflogEntry
126
127 entry = ReflogEntry(
128 old_id="0" * 64, new_id="b" * 64, author="user",
129 timestamp=datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
130 operation="branch: created",
131 )
132 result = _fmt_entry(0, entry)
133 assert "initial" in result
134
135
136 # ---------------------------------------------------------------------------
137 # Integration tests
138 # ---------------------------------------------------------------------------
139
140 class TestReflogIntegration:
141 def test_reflog_empty_repo(self, tmp_path: pathlib.Path) -> None:
142 root, _ = _init_repo(tmp_path)
143 result = runner.invoke(cli, ["reflog"], env=_env(root), catch_exceptions=False)
144 assert result.exit_code == 0
145 assert "No reflog entries" in result.output
146
147 def test_reflog_after_commit(self, tmp_path: pathlib.Path) -> None:
148 root, repo_id = _init_repo(tmp_path)
149 _make_commit_with_reflog(root, repo_id, message="my first commit")
150 result = runner.invoke(cli, ["reflog"], env=_env(root), catch_exceptions=False)
151 assert result.exit_code == 0
152 assert "@{0" in result.output
153 assert "commit: my first commit" in result.output
154
155 def test_reflog_limit(self, tmp_path: pathlib.Path) -> None:
156 root, repo_id = _init_repo(tmp_path)
157 for i in range(10):
158 _make_commit_with_reflog(root, repo_id, message=f"commit {i}")
159 result = runner.invoke(cli, ["reflog", "--limit", "3"], env=_env(root), catch_exceptions=False)
160 assert result.exit_code == 0
161 lines = [l for l in result.output.splitlines() if "@{" in l]
162 assert len(lines) <= 3
163
164 def test_reflog_branch_flag(self, tmp_path: pathlib.Path) -> None:
165 root, repo_id = _init_repo(tmp_path)
166 _make_commit_with_reflog(root, repo_id, message="on main")
167 result = runner.invoke(cli, ["reflog", "--branch", "main"], env=_env(root), catch_exceptions=False)
168 assert result.exit_code == 0
169 assert "main" in result.output
170
171 def test_reflog_short_flags(self, tmp_path: pathlib.Path) -> None:
172 root, repo_id = _init_repo(tmp_path)
173 for i in range(5):
174 _make_commit_with_reflog(root, repo_id, message=f"commit {i}")
175 result = runner.invoke(cli, ["reflog", "--limit", "2", "-b", "main"], env=_env(root), catch_exceptions=False)
176 assert result.exit_code == 0
177 lines = [l for l in result.output.splitlines() if "@{" in l]
178 assert len(lines) <= 2
179
180 def test_reflog_all_flag_lists_refs(self, tmp_path: pathlib.Path) -> None:
181 root, repo_id = _init_repo(tmp_path)
182 _make_commit_with_reflog(root, repo_id, message="first")
183 result = runner.invoke(cli, ["reflog", "--all"], env=_env(root), catch_exceptions=False)
184 assert result.exit_code == 0
185
186
187 # ---------------------------------------------------------------------------
188 # Security tests
189 # ---------------------------------------------------------------------------
190
191 class TestReflogSecurity:
192 def test_invalid_branch_name_rejected(self, tmp_path: pathlib.Path) -> None:
193 root, repo_id = _init_repo(tmp_path)
194 _make_commit_with_reflog(root, repo_id)
195 result = runner.invoke(cli, ["reflog", "--branch", "../../../etc/passwd"], env=_env(root))
196 assert result.exit_code != 0
197
198 def test_operation_with_control_chars_sanitized(self, tmp_path: pathlib.Path) -> None:
199 root, repo_id = _init_repo(tmp_path)
200 from muse.core.reflog import append_reflog
201 _make_commit_with_reflog(root, repo_id, message="clean")
202 append_reflog(root, "main", old_id="0" * 64, new_id="a" * 64,
203 author="user", operation="evil\x1b[31mRED\x1b[0m op")
204 result = runner.invoke(cli, ["reflog"], env=_env(root), catch_exceptions=False)
205 assert result.exit_code == 0
206 assert "\x1b" not in result.output
207
208
209 # ---------------------------------------------------------------------------
210 # Stress tests
211 # ---------------------------------------------------------------------------
212
213 class TestReflogStress:
214 def test_large_reflog_with_limit(self, tmp_path: pathlib.Path) -> None:
215 root, repo_id = _init_repo(tmp_path)
216 for i in range(50):
217 _make_commit_with_reflog(root, repo_id, message=f"commit {i:03d}")
218 result = runner.invoke(cli, ["reflog", "--limit", "5"], env=_env(root), catch_exceptions=False)
219 assert result.exit_code == 0
220 lines = [l for l in result.output.splitlines() if "@{" in l]
221 assert len(lines) <= 5
File History 3 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
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago