gabriel / muse public
test_core_blame.py python
213 lines 7.3 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for muse/core/blame.py — line-level text attribution."""
2
3 from __future__ import annotations
4
5 import datetime
6 import hashlib
7 import json
8 import pathlib
9
10 import pytest
11
12 from muse.core.blame import BlameLine, blame_file
13 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
14 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
15 from muse.core._types import Manifest
16
17 _BASE_DT = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
18
19
20 # ---------------------------------------------------------------------------
21 # Helpers
22 # ---------------------------------------------------------------------------
23
24
25 def _sha256(content: bytes) -> str:
26 return hashlib.sha256(content).hexdigest()
27
28
29 def _write_object(repo: pathlib.Path, content: bytes) -> str:
30 sha = _sha256(content)
31 obj_dir = repo / ".muse" / "objects" / sha[:2]
32 obj_dir.mkdir(parents=True, exist_ok=True)
33 (obj_dir / sha[2:]).write_bytes(content)
34 return sha
35
36
37 def _write_snapshot(repo: pathlib.Path, manifest: Manifest) -> str:
38 """Write a snapshot with a properly computed ID; return the snapshot ID."""
39 snap_id = compute_snapshot_id(manifest)
40 write_snapshot(repo, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
41 return snap_id
42
43
44 def _write_commit(
45 repo: pathlib.Path,
46 snap_id: str,
47 message: str = "test",
48 parent: str | None = None,
49 author: str = "Author",
50 committed_at: datetime.datetime | None = None,
51 ) -> str:
52 """Write a commit with a properly computed ID; return the commit ID."""
53 dt = committed_at if committed_at is not None else _BASE_DT
54 parent_ids = [parent] if parent else []
55 commit_id = compute_commit_id(parent_ids, snap_id, message, dt.isoformat())
56 write_commit(repo, CommitRecord(
57 commit_id=commit_id,
58 repo_id="test-repo",
59 branch="main",
60 snapshot_id=snap_id,
61 message=message,
62 committed_at=dt,
63 parent_commit_id=parent,
64 author=author,
65 ))
66 return commit_id
67
68
69 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
70 muse = tmp_path / ".muse"
71 for d in ("objects", "commits", "snapshots", "refs/heads"):
72 (muse / d).mkdir(parents=True, exist_ok=True)
73 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
74 (muse / "HEAD").write_text("ref: refs/heads/main\n")
75 return tmp_path
76
77
78 # ---------------------------------------------------------------------------
79 # Tests
80 # ---------------------------------------------------------------------------
81
82
83 def test_blame_returns_none_for_missing_file(tmp_path: pathlib.Path) -> None:
84 repo = _make_repo(tmp_path)
85 snap_id = _write_snapshot(repo, {}) # empty manifest
86 commit_id = _write_commit(repo, snap_id)
87
88 result = blame_file(repo, "nonexistent.txt", commit_id)
89 assert result is None
90
91
92 def test_blame_single_commit_all_lines_attributed(tmp_path: pathlib.Path) -> None:
93 repo = _make_repo(tmp_path)
94 content = b"line one\nline two\nline three\n"
95 obj_id = _write_object(repo, content)
96 snap_id = _write_snapshot(repo, {"readme.txt": obj_id})
97 commit_id = _write_commit(repo, snap_id, message="initial commit", author="Alice")
98
99 result = blame_file(repo, "readme.txt", commit_id)
100 assert result is not None
101 assert len(result) == 3
102 for line in result:
103 assert isinstance(line, BlameLine)
104 assert line.commit_id == commit_id
105
106
107 def test_blame_line_numbers_are_1_indexed(tmp_path: pathlib.Path) -> None:
108 repo = _make_repo(tmp_path)
109 content = b"a\nb\nc\n"
110 obj_id = _write_object(repo, content)
111 snap_id = _write_snapshot(repo, {"f.txt": obj_id})
112 commit_id = _write_commit(repo, snap_id)
113
114 result = blame_file(repo, "f.txt", commit_id)
115 assert result is not None
116 assert [bl.lineno for bl in result] == [1, 2, 3]
117
118
119 def test_blame_content_matches_file(tmp_path: pathlib.Path) -> None:
120 repo = _make_repo(tmp_path)
121 content = b"hello\nworld\n"
122 obj_id = _write_object(repo, content)
123 snap_id = _write_snapshot(repo, {"f.txt": obj_id})
124 commit_id = _write_commit(repo, snap_id)
125
126 result = blame_file(repo, "f.txt", commit_id)
127 assert result is not None
128 assert result[0].content == "hello"
129 assert result[1].content == "world"
130
131
132 def test_blame_empty_file_returns_empty_list(tmp_path: pathlib.Path) -> None:
133 repo = _make_repo(tmp_path)
134 content = b""
135 obj_id = _write_object(repo, content)
136 snap_id = _write_snapshot(repo, {"empty.txt": obj_id})
137 commit_id = _write_commit(repo, snap_id)
138
139 result = blame_file(repo, "empty.txt", commit_id)
140 assert result == []
141
142
143 def test_blame_two_commits_attributes_older_lines_correctly(tmp_path: pathlib.Path) -> None:
144 """Lines present in both commits should be attributed to the older commit."""
145 repo = _make_repo(tmp_path)
146
147 # Commit 1: file with two lines.
148 content1 = b"original line 1\noriginal line 2\n"
149 obj1 = _write_object(repo, content1)
150 snap1 = _write_snapshot(repo, {"f.txt": obj1})
151 commit1 = _write_commit(
152 repo, snap1, message="initial", author="Alice",
153 committed_at=_BASE_DT,
154 )
155
156 # Commit 2: same two lines + one new line.
157 content2 = b"original line 1\noriginal line 2\nnew line 3\n"
158 obj2 = _write_object(repo, content2)
159 snap2 = _write_snapshot(repo, {"f.txt": obj2})
160 commit2 = _write_commit(
161 repo, snap2, message="add line 3", parent=commit1, author="Bob",
162 committed_at=_BASE_DT + datetime.timedelta(hours=1),
163 )
164
165 result = blame_file(repo, "f.txt", commit2)
166 assert result is not None
167 assert len(result) == 3
168 # Lines 1 and 2 should be attributed to commit1 (they existed before commit2).
169 assert result[0].commit_id == commit1
170 assert result[1].commit_id == commit1
171 # Line 3 was added by commit2.
172 assert result[2].commit_id == commit2
173
174
175 def test_blame_author_populated(tmp_path: pathlib.Path) -> None:
176 repo = _make_repo(tmp_path)
177 obj_id = _write_object(repo, b"line\n")
178 snap_id = _write_snapshot(repo, {"f.txt": obj_id})
179 commit_id = _write_commit(repo, snap_id, author="Carol")
180
181 result = blame_file(repo, "f.txt", commit_id)
182 assert result is not None
183 assert result[0].author == "Carol"
184
185
186 def test_blame_message_is_first_line_of_commit_message(tmp_path: pathlib.Path) -> None:
187 repo = _make_repo(tmp_path)
188 obj_id = _write_object(repo, b"line\n")
189 snap_id = _write_snapshot(repo, {"f.txt": obj_id})
190 commit_id = _write_commit(repo, snap_id, message="feat: add feature\n\nLong body here.")
191
192 result = blame_file(repo, "f.txt", commit_id)
193 assert result is not None
194 assert result[0].message == "feat: add feature"
195
196
197 # ---------------------------------------------------------------------------
198 # Stress
199 # ---------------------------------------------------------------------------
200
201
202 def test_blame_stress_100_line_file(tmp_path: pathlib.Path) -> None:
203 """Blame should handle a 100-line file without errors."""
204 repo = _make_repo(tmp_path)
205 content = "\n".join(f"line {i}" for i in range(100)).encode() + b"\n"
206 obj_id = _write_object(repo, content)
207 snap_id = _write_snapshot(repo, {"big.txt": obj_id})
208 commit_id = _write_commit(repo, snap_id)
209
210 result = blame_file(repo, "big.txt", commit_id)
211 assert result is not None
212 assert len(result) == 100
213 assert all(bl.commit_id == commit_id for bl in result)
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago