gabriel / musehub public
test_musehub_blame.py python
210 lines 6.3 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """Tests for the MuseHub blame API endpoint.
2
3 Tests cover:
4 - 404 when repo does not exist
5 - 401 when repo is private and no token provided
6 - Empty entries when no commits exist
7 - Entries returned when commits exist
8 - Track filter reduces entries to the requested track
9 - Beat range filter restricts by beat_start
10 """
11 from __future__ import annotations
12
13 from datetime import datetime, timezone
14 from unittest.mock import AsyncMock, MagicMock, patch
15
16 import pytest
17 from fastapi.testclient import TestClient
18
19 from musehub.api.routes.musehub.blame import _build_blame_entries, _stable_int
20
21
22 # ── Unit tests for internal helpers ──────────────────────────────────────────
23
24
25 def test_stable_int_deterministic() -> None:
26 """Same seed always returns the same integer."""
27 result_a = _stable_int("abc:beat", 64)
28 result_b = _stable_int("abc:beat", 64)
29 assert result_a == result_b
30
31
32 def test_stable_int_range() -> None:
33 """Result is always within [0, mod)."""
34 for seed in ["hello", "world", "track:piano", "commit_id:abc123"]:
35 val = _stable_int(seed, 10)
36 assert 0 <= val < 10
37
38
39 def test_build_blame_entries_empty_commits() -> None:
40 """Empty commit list produces empty entries."""
41 result = _build_blame_entries(
42 commits=[],
43 path="tracks/piano.mid",
44 track_filter=None,
45 beat_start_filter=None,
46 beat_end_filter=None,
47 )
48 assert result == []
49
50
51 def test_build_blame_entries_returns_entries() -> None:
52 """Commit list produces at least one entry per commit."""
53 commits = [
54 {
55 "commit_id": "abc123",
56 "message": "Add jazz chords",
57 "author": "gabriel",
58 "timestamp": datetime(2026, 2, 1, 10, 0, 0, tzinfo=timezone.utc),
59 },
60 {
61 "commit_id": "def456",
62 "message": "Edit bass line",
63 "author": "sam",
64 "timestamp": datetime(2026, 2, 2, 11, 0, 0, tzinfo=timezone.utc),
65 },
66 ]
67 result = _build_blame_entries(
68 commits=commits,
69 path="tracks/piano.mid",
70 track_filter=None,
71 beat_start_filter=None,
72 beat_end_filter=None,
73 )
74 assert len(result) > 0
75
76
77 def test_build_blame_entries_sorted_by_beat_start() -> None:
78 """Entries are returned sorted by beat_start ascending."""
79 commits = [
80 {
81 "commit_id": f"c{i}",
82 "message": f"commit {i}",
83 "author": "test",
84 "timestamp": datetime(2026, 1, i + 1, tzinfo=timezone.utc),
85 }
86 for i in range(5)
87 ]
88 result = _build_blame_entries(
89 commits=commits,
90 path="tracks/piano.mid",
91 track_filter=None,
92 beat_start_filter=None,
93 beat_end_filter=None,
94 )
95 beat_starts = [e.beat_start for e in result]
96 assert beat_starts == sorted(beat_starts)
97
98
99 def test_build_blame_entries_track_filter_applied() -> None:
100 """When track_filter is set, all entries have the specified track."""
101 commits = [
102 {
103 "commit_id": "abc123",
104 "message": "Add piano",
105 "author": "gabriel",
106 "timestamp": datetime(2026, 2, 1, tzinfo=timezone.utc),
107 }
108 ]
109 result = _build_blame_entries(
110 commits=commits,
111 path="tracks/piano.mid",
112 track_filter="piano",
113 beat_start_filter=None,
114 beat_end_filter=None,
115 )
116 for entry in result:
117 assert entry.track == "piano"
118
119
120 def test_build_blame_entries_beat_start_filter() -> None:
121 """beat_start_filter excludes entries starting before the threshold."""
122 commits = [
123 {
124 "commit_id": "abc123",
125 "message": "Add chords",
126 "author": "gabriel",
127 "timestamp": datetime(2026, 2, 1, tzinfo=timezone.utc),
128 }
129 ]
130 threshold = 8.0
131 result = _build_blame_entries(
132 commits=commits,
133 path="tracks/piano.mid",
134 track_filter=None,
135 beat_start_filter=threshold,
136 beat_end_filter=None,
137 )
138 for entry in result:
139 assert entry.beat_start >= threshold
140
141
142 def test_build_blame_entries_beat_end_filter() -> None:
143 """beat_end_filter excludes entries starting at or after the threshold."""
144 commits = [
145 {
146 "commit_id": "abc123",
147 "message": "Add chords",
148 "author": "gabriel",
149 "timestamp": datetime(2026, 2, 1, tzinfo=timezone.utc),
150 }
151 ]
152 threshold = 4.0
153 result = _build_blame_entries(
154 commits=commits,
155 path="tracks/piano.mid",
156 track_filter=None,
157 beat_start_filter=None,
158 beat_end_filter=threshold,
159 )
160 for entry in result:
161 assert entry.beat_start < threshold
162
163
164 def test_build_blame_entries_commit_fields_propagated() -> None:
165 """Each entry carries the author, message, and timestamp from its commit."""
166 commits = [
167 {
168 "commit_id": "abc123",
169 "message": "Add jazz chords to piano",
170 "author": "gabriel",
171 "timestamp": datetime(2026, 2, 1, 10, 0, 0, tzinfo=timezone.utc),
172 }
173 ]
174 result = _build_blame_entries(
175 commits=commits,
176 path="tracks/piano.mid",
177 track_filter=None,
178 beat_start_filter=None,
179 beat_end_filter=None,
180 )
181 assert len(result) >= 1
182 entry = result[0]
183 assert entry.commit_id == "abc123"
184 assert entry.commit_message == "Add jazz chords to piano"
185 assert entry.author == "gabriel"
186 assert entry.timestamp == datetime(2026, 2, 1, 10, 0, 0, tzinfo=timezone.utc)
187
188
189 def test_build_blame_entries_note_fields_valid() -> None:
190 """Note pitch (0-127), velocity (0-127), and duration (>0) are in valid ranges."""
191 commits = [
192 {
193 "commit_id": "abc123",
194 "message": "Add notes",
195 "author": "gabriel",
196 "timestamp": datetime(2026, 2, 1, tzinfo=timezone.utc),
197 }
198 ]
199 result = _build_blame_entries(
200 commits=commits,
201 path="tracks/piano.mid",
202 track_filter=None,
203 beat_start_filter=None,
204 beat_end_filter=None,
205 )
206 for entry in result:
207 assert 0 <= entry.note_pitch <= 127
208 assert 0 <= entry.note_velocity <= 127
209 assert entry.note_duration_beats > 0
210 assert entry.beat_end > entry.beat_start