gabriel / muse public
test_cmd_reflog.py python
197 lines 8.0 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d 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 import uuid
17
18 import pytest
19 from tests.cli_test_helper import CliRunner
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) -> dict[str, str]:
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 = str(uuid.uuid4())
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: dict[str, str] = {}
62 snap_id = compute_snapshot_id(manifest)
63 committed_at = datetime.datetime.now(datetime.timezone.utc)
64 commit_id = compute_commit_id(
65 parent_ids=[parent_id] if parent_id else [],
66 snapshot_id=snap_id, message=message,
67 committed_at_iso=committed_at.isoformat(),
68 )
69 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
70 write_commit(root, CommitRecord(
71 commit_id=commit_id, repo_id=repo_id, branch=branch,
72 snapshot_id=snap_id, message=message, committed_at=committed_at,
73 parent_commit_id=parent_id,
74 ))
75 ref_file.parent.mkdir(parents=True, exist_ok=True)
76 ref_file.write_text(commit_id, encoding="utf-8")
77 append_reflog(root, branch, old_id=parent_id or "0" * 64, new_id=commit_id,
78 author="user", operation=f"commit: {message}")
79 return commit_id
80
81
82 # ---------------------------------------------------------------------------
83 # Unit tests
84 # ---------------------------------------------------------------------------
85
86 class TestReflogUnit:
87 def test_fmt_entry_sanitizes_operation(self) -> None:
88 from muse.cli.commands.reflog import _fmt_entry
89 from muse.core.reflog import ReflogEntry
90
91 entry = ReflogEntry(
92 old_id="0" * 64, new_id="a" * 64, author="user",
93 timestamp=datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
94 operation="commit: Hello\x1b[31mRED\x1b[0m",
95 )
96 result = _fmt_entry(0, entry)
97 assert "\x1b" not in result
98
99 def test_fmt_entry_initial_shown_as_initial(self) -> None:
100 from muse.cli.commands.reflog import _fmt_entry
101 from muse.core.reflog import ReflogEntry
102
103 entry = ReflogEntry(
104 old_id="0" * 64, new_id="b" * 64, author="user",
105 timestamp=datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
106 operation="branch: created",
107 )
108 result = _fmt_entry(0, entry)
109 assert "initial" in result
110
111
112 # ---------------------------------------------------------------------------
113 # Integration tests
114 # ---------------------------------------------------------------------------
115
116 class TestReflogIntegration:
117 def test_reflog_empty_repo(self, tmp_path: pathlib.Path) -> None:
118 root, _ = _init_repo(tmp_path)
119 result = runner.invoke(cli, ["reflog"], env=_env(root), catch_exceptions=False)
120 assert result.exit_code == 0
121 assert "No reflog entries" in result.output
122
123 def test_reflog_after_commit(self, tmp_path: pathlib.Path) -> None:
124 root, repo_id = _init_repo(tmp_path)
125 _make_commit_with_reflog(root, repo_id, message="my first commit")
126 result = runner.invoke(cli, ["reflog"], env=_env(root), catch_exceptions=False)
127 assert result.exit_code == 0
128 assert "@{0" in result.output
129 assert "commit: my first commit" in result.output
130
131 def test_reflog_limit(self, tmp_path: pathlib.Path) -> None:
132 root, repo_id = _init_repo(tmp_path)
133 for i in range(10):
134 _make_commit_with_reflog(root, repo_id, message=f"commit {i}")
135 result = runner.invoke(cli, ["reflog", "--limit", "3"], env=_env(root), catch_exceptions=False)
136 assert result.exit_code == 0
137 lines = [l for l in result.output.splitlines() if "@{" in l]
138 assert len(lines) <= 3
139
140 def test_reflog_branch_flag(self, tmp_path: pathlib.Path) -> None:
141 root, repo_id = _init_repo(tmp_path)
142 _make_commit_with_reflog(root, repo_id, message="on main")
143 result = runner.invoke(cli, ["reflog", "--branch", "main"], env=_env(root), catch_exceptions=False)
144 assert result.exit_code == 0
145 assert "main" in result.output
146
147 def test_reflog_short_flags(self, tmp_path: pathlib.Path) -> None:
148 root, repo_id = _init_repo(tmp_path)
149 for i in range(5):
150 _make_commit_with_reflog(root, repo_id, message=f"commit {i}")
151 result = runner.invoke(cli, ["reflog", "-n", "2", "-b", "main"], env=_env(root), catch_exceptions=False)
152 assert result.exit_code == 0
153 lines = [l for l in result.output.splitlines() if "@{" in l]
154 assert len(lines) <= 2
155
156 def test_reflog_all_flag_lists_refs(self, tmp_path: pathlib.Path) -> None:
157 root, repo_id = _init_repo(tmp_path)
158 _make_commit_with_reflog(root, repo_id, message="first")
159 result = runner.invoke(cli, ["reflog", "--all"], env=_env(root), catch_exceptions=False)
160 assert result.exit_code == 0
161
162
163 # ---------------------------------------------------------------------------
164 # Security tests
165 # ---------------------------------------------------------------------------
166
167 class TestReflogSecurity:
168 def test_invalid_branch_name_rejected(self, tmp_path: pathlib.Path) -> None:
169 root, repo_id = _init_repo(tmp_path)
170 _make_commit_with_reflog(root, repo_id)
171 result = runner.invoke(cli, ["reflog", "--branch", "../../../etc/passwd"], env=_env(root))
172 assert result.exit_code != 0
173
174 def test_operation_with_control_chars_sanitized(self, tmp_path: pathlib.Path) -> None:
175 root, repo_id = _init_repo(tmp_path)
176 from muse.core.reflog import append_reflog
177 _make_commit_with_reflog(root, repo_id, message="clean")
178 append_reflog(root, "main", old_id="0" * 64, new_id="a" * 64,
179 author="user", operation="evil\x1b[31mRED\x1b[0m op")
180 result = runner.invoke(cli, ["reflog"], env=_env(root), catch_exceptions=False)
181 assert result.exit_code == 0
182 assert "\x1b" not in result.output
183
184
185 # ---------------------------------------------------------------------------
186 # Stress tests
187 # ---------------------------------------------------------------------------
188
189 class TestReflogStress:
190 def test_large_reflog_with_limit(self, tmp_path: pathlib.Path) -> None:
191 root, repo_id = _init_repo(tmp_path)
192 for i in range(50):
193 _make_commit_with_reflog(root, repo_id, message=f"commit {i:03d}")
194 result = runner.invoke(cli, ["reflog", "-n", "5"], env=_env(root), catch_exceptions=False)
195 assert result.exit_code == 0
196 lines = [l for l in result.output.splitlines() if "@{" in l]
197 assert len(lines) <= 5