gabriel / muse public
test_cmd_log.py python
204 lines 8.3 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 log``.
2
3 Covers:
4 - Unit: _parse_date helper
5 - Integration: log with history, filters, authors
6 - E2E: full CLI via CliRunner
7 - Security: no path traversal via --ref, sanitized output
8 - Stress: long commit history 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(
53 root: pathlib.Path, repo_id: str, message: str = "commit",
54 author: str = "Alice",
55 ) -> str:
56 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
57 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
58
59 ref_file = root / ".muse" / "refs" / "heads" / "main"
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="main",
72 snapshot_id=snap_id, message=message, committed_at=committed_at,
73 parent_commit_id=parent_id, author=author,
74 ))
75 ref_file.parent.mkdir(parents=True, exist_ok=True)
76 ref_file.write_text(commit_id, encoding="utf-8")
77 return commit_id
78
79
80 # ---------------------------------------------------------------------------
81 # Unit tests
82 # ---------------------------------------------------------------------------
83
84 class TestLogUnit:
85 def test_parse_date_iso(self) -> None:
86 from muse.cli.commands.log import _parse_date
87 dt = _parse_date("2025-01-15")
88 assert dt.year == 2025
89 assert dt.month == 1
90 assert dt.day == 15
91
92 def test_parse_date_relative_days(self) -> None:
93 from muse.cli.commands.log import _parse_date
94 dt = _parse_date("7 days ago")
95 assert isinstance(dt, datetime.datetime)
96
97
98 # ---------------------------------------------------------------------------
99 # Integration tests
100 # ---------------------------------------------------------------------------
101
102 class TestLogIntegration:
103 def test_log_empty_repo_shows_nothing(self, tmp_path: pathlib.Path) -> None:
104 root, repo_id = _init_repo(tmp_path)
105 result = runner.invoke(cli, ["log"], env=_env(root), catch_exceptions=False)
106 assert result.exit_code == 0
107
108 def test_log_single_commit(self, tmp_path: pathlib.Path) -> None:
109 root, repo_id = _init_repo(tmp_path)
110 _make_commit(root, repo_id, message="first commit")
111 result = runner.invoke(cli, ["log"], env=_env(root), catch_exceptions=False)
112 assert result.exit_code == 0
113 assert "first commit" in result.output
114
115 def test_log_multiple_commits_newest_first(self, tmp_path: pathlib.Path) -> None:
116 root, repo_id = _init_repo(tmp_path)
117 _make_commit(root, repo_id, message="commit one")
118 _make_commit(root, repo_id, message="commit two")
119 _make_commit(root, repo_id, message="commit three")
120 result = runner.invoke(cli, ["log"], env=_env(root), catch_exceptions=False)
121 pos_one = result.output.find("commit one")
122 pos_three = result.output.find("commit three")
123 assert pos_three < pos_one # newest appears first
124
125 def test_log_oneline_format(self, tmp_path: pathlib.Path) -> None:
126 root, repo_id = _init_repo(tmp_path)
127 _make_commit(root, repo_id, message="oneline test")
128 result = runner.invoke(cli, ["log", "--oneline"], env=_env(root), catch_exceptions=False)
129 assert result.exit_code == 0
130 lines = [l for l in result.output.splitlines() if l.strip()]
131 assert len(lines) == 1
132 assert "oneline test" in lines[0]
133
134 def test_log_max_count_limits_output(self, tmp_path: pathlib.Path) -> None:
135 root, repo_id = _init_repo(tmp_path)
136 for i in range(5):
137 _make_commit(root, repo_id, message=f"commit {i}")
138 result = runner.invoke(cli, ["log", "-n", "2"], env=_env(root), catch_exceptions=False)
139 assert result.exit_code == 0
140 # Count distinct commit IDs in output (each is 64 chars long)
141 import re
142 commit_ids = re.findall(r"commit [0-9a-f]{64}", result.output)
143 assert len(commit_ids) <= 2
144
145 def test_log_filter_by_author(self, tmp_path: pathlib.Path) -> None:
146 root, repo_id = _init_repo(tmp_path)
147 _make_commit(root, repo_id, message="by alice", author="Alice")
148 _make_commit(root, repo_id, message="by bob", author="Bob")
149 result = runner.invoke(cli, ["log", "--author", "Alice"], env=_env(root), catch_exceptions=False)
150 assert "by alice" in result.output
151 assert "by bob" not in result.output
152
153 def test_log_stat_flag(self, tmp_path: pathlib.Path) -> None:
154 root, repo_id = _init_repo(tmp_path)
155 _make_commit(root, repo_id, message="stat commit")
156 result = runner.invoke(cli, ["log", "--stat"], env=_env(root), catch_exceptions=False)
157 assert result.exit_code == 0
158
159 def test_log_max_count_zero_rejected(self, tmp_path: pathlib.Path) -> None:
160 root, repo_id = _init_repo(tmp_path)
161 result = runner.invoke(cli, ["log", "-n", "0"], env=_env(root))
162 assert result.exit_code != 0
163
164 def test_log_short_flags(self, tmp_path: pathlib.Path) -> None:
165 root, repo_id = _init_repo(tmp_path)
166 _make_commit(root, repo_id, message="short flag")
167 result = runner.invoke(cli, ["log", "-n", "1", "-p"], env=_env(root), catch_exceptions=False)
168 assert result.exit_code == 0
169
170
171 # ---------------------------------------------------------------------------
172 # Security tests
173 # ---------------------------------------------------------------------------
174
175 class TestLogSecurity:
176 def test_log_invalid_branch_name_handled(self, tmp_path: pathlib.Path) -> None:
177 root, repo_id = _init_repo(tmp_path)
178 _make_commit(root, repo_id)
179 result = runner.invoke(cli, ["log", "../../../etc/passwd"], env=_env(root))
180 assert "\x1b" not in result.output
181
182
183 # ---------------------------------------------------------------------------
184 # Stress tests
185 # ---------------------------------------------------------------------------
186
187 class TestLogStress:
188 def test_long_history_with_limit(self, tmp_path: pathlib.Path) -> None:
189 root, repo_id = _init_repo(tmp_path)
190 for i in range(200):
191 _make_commit(root, repo_id, message=f"commit {i:03d}")
192 result = runner.invoke(cli, ["log", "-n", "10", "--oneline"], env=_env(root), catch_exceptions=False)
193 assert result.exit_code == 0
194 lines = [l for l in result.output.splitlines() if l.strip()]
195 assert 1 <= len(lines) <= 10
196
197 def test_many_commits_author_filter_performance(self, tmp_path: pathlib.Path) -> None:
198 root, repo_id = _init_repo(tmp_path)
199 for i in range(100):
200 author = "Alice" if i % 2 == 0 else "Bob"
201 _make_commit(root, repo_id, message=f"msg {i}", author=author)
202 result = runner.invoke(cli, ["log", "--author", "Alice", "-n", "50"], env=_env(root), catch_exceptions=False)
203 assert result.exit_code == 0
204 assert "Bob" not in result.output