gabriel / muse public
test_cmd_reflog.py python
197 lines 8.0 KB
95b86799 feat: add --format json to all porcelain commands for agent-first output Gabriel Cardona <gabriel@tellurstori.com> 2d 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 typer.testing import CliRunner
20
21 from muse.cli.app import cli
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