gabriel / musehub public
test_phase3_db_walk_dag_async.py python
280 lines 10.6 KB
Raw
sha256:d8cbca3a06f39f82f66be6c29de3f41c3dec5f367722958fb5454dcbc007cc15 fix: rc11 test fixes — event→verdict rename, migration coun… Sonnet 4.6 patch 15 days ago
1 """TDD — Phase 3 of issue #40: DB-query BFS sites migrated to walk_dag_async.
2
3 Structural tests confirm each function uses walk_dag_async and has no inline
4 BFS/DFS loop. Behavioural tests confirm the migrated functions produce
5 correct output when given a fake session.
6
7 P3-1 Structural — _walk_new_commits uses walk_dag_async; no inline while stack
8 P3-2 Structural — _resolve_ancestor_manifest uses walk_dag_async; no inline while frontier
9 P3-3 Structural — _resolve_ancestor_manifest uses walk_dag_async; no inline while fid
10 P3-4 Structural — _is_ancestor_db uses walk_dag_async; no inline while frontier
11 P3-5 Behavioural — _walk_new_commits with stop_at returns commits up to boundary
12 P3-6 Behavioural — _walk_new_commits without stop_at returns all repo commits
13 P3-7 Behavioural — _resolve_ancestor_manifest returns manifest at LCA of two branches
14 P3-8 Behavioural — _is_ancestor_db: True when ancestor is reachable
15 P3-9 Behavioural — _is_ancestor_db: False when max_hops cap exceeded before finding ancestor
16 """
17 from __future__ import annotations
18
19 import datetime
20 import inspect
21 from types import SimpleNamespace
22 from unittest.mock import AsyncMock, MagicMock, patch
23
24 import pytest
25
26
27 # ---------------------------------------------------------------------------
28 # P3-1 Structural — _walk_new_commits
29 # ---------------------------------------------------------------------------
30
31 def test_p3_1_walk_new_commits_uses_walk_dag_async() -> None:
32 """_walk_new_commits must use walk_dag_async; no inline while stack."""
33 from musehub.services import musehub_symbol_indexer as mod
34
35 src = inspect.getsource(mod._walk_new_commits)
36 assert "walk_dag_async" in src, (
37 "_walk_new_commits must delegate DFS to walk_dag_async."
38 )
39 assert "while stack" not in src, (
40 "_walk_new_commits still has an inline while-stack DFS."
41 )
42
43
44 # ---------------------------------------------------------------------------
45 # P3-2 Structural — _resolve_ancestor_manifest (walk 1: while frontier)
46 # ---------------------------------------------------------------------------
47
48 def test_p3_2_resolve_ancestor_manifest_uses_walk_dag_async() -> None:
49 """_resolve_ancestor_manifest must use walk_dag_async; no inline while frontier."""
50 from musehub.services import musehub_proposals as mod
51
52 src = inspect.getsource(mod._resolve_ancestor_manifest)
53 assert "walk_dag_async" in src, (
54 "_resolve_ancestor_manifest must delegate BFS to walk_dag_async."
55 )
56 assert "while frontier" not in src, (
57 "_resolve_ancestor_manifest still has an inline while-frontier BFS."
58 )
59
60
61 # ---------------------------------------------------------------------------
62 # P3-3 Structural — _resolve_ancestor_manifest (walk 2: while fid)
63 # ---------------------------------------------------------------------------
64
65 def test_p3_3_resolve_ancestor_manifest_no_while_fid() -> None:
66 """_resolve_ancestor_manifest must not have an inline while-fid first-parent walk."""
67 from musehub.services import musehub_proposals as mod
68
69 src = inspect.getsource(mod._resolve_ancestor_manifest)
70 assert "while fid" not in src, (
71 "_resolve_ancestor_manifest still has an inline while-fid first-parent walk."
72 )
73
74
75 # ---------------------------------------------------------------------------
76 # P3-4 Structural — _is_ancestor_db
77 # ---------------------------------------------------------------------------
78
79 def test_p3_4_is_ancestor_db_uses_walk_dag_async() -> None:
80 """_is_ancestor_db must use walk_dag_async; no inline while frontier."""
81 from musehub.services import musehub_wire as mod
82
83 src = inspect.getsource(mod._is_ancestor_db)
84 assert "walk_dag_async" in src, (
85 "_is_ancestor_db must delegate BFS to walk_dag_async."
86 )
87 assert "while frontier" not in src, (
88 "_is_ancestor_db still has an inline while-frontier BFS."
89 )
90
91
92 # ---------------------------------------------------------------------------
93 # Helpers for behavioural tests
94 # ---------------------------------------------------------------------------
95
96 _TZ = datetime.timezone.utc
97
98
99 def _ts(n: int) -> datetime.datetime:
100 return datetime.datetime(2026, 1, n, tzinfo=_TZ)
101
102
103 def _commit(cid: str, parents: list[str], n: int, repo: str = "repo1",
104 snapshot_id: str | None = None) -> SimpleNamespace:
105 return SimpleNamespace(
106 commit_id=cid,
107 parent_ids=parents,
108 repo_id=repo,
109 timestamp=_ts(n),
110 snapshot_id=snapshot_id,
111 )
112
113
114 def _exec_result(*, scalar: SimpleNamespace | None = None, scalars: list[SimpleNamespace] | None = None, one: SimpleNamespace | None = None) -> MagicMock:
115 """Build a MagicMock mimicking an AsyncSession execute result."""
116 r = MagicMock()
117 r.scalar_one_or_none.return_value = scalar
118 r.scalars.return_value = scalars or []
119 r.one_or_none.return_value = one
120 return r
121
122
123 # ---------------------------------------------------------------------------
124 # P3-5 Behavioural — _walk_new_commits with stop_at
125 # ---------------------------------------------------------------------------
126
127 @pytest.mark.asyncio
128 async def test_p3_5_walk_new_commits_with_stop_at() -> None:
129 """_walk_new_commits with stop_at=C1 returns only C3 and C2 (C1 excluded).
130
131 Chain: C3 → C2 → C1. stop_at=C1.
132 Expected: sorted by timestamp → [C2, C3].
133 """
134 from musehub.services.musehub_symbol_indexer import _walk_new_commits
135
136 c1 = _commit("C1", [], 1)
137 c2 = _commit("C2", ["C1"], 2)
138 c3 = _commit("C3", ["C2"], 3)
139
140 session = MagicMock()
141 # DFS from C3 (stop_at=C1 excluded): adj("C3")→[C2], adj("C2")→[C1] (C1 excluded)
142 session.execute = AsyncMock(side_effect=[
143 _exec_result(scalar=c3), # adj("C3")
144 _exec_result(scalar=c2), # adj("C2")
145 ])
146
147 result = await _walk_new_commits(session, "repo1", "C3", "C1")
148 assert [c.commit_id for c in result] == ["C2", "C3"], (
149 f"Expected [C2, C3] sorted oldest→newest, got {[c.commit_id for c in result]}"
150 )
151
152
153 # ---------------------------------------------------------------------------
154 # P3-6 Behavioural — _walk_new_commits without stop_at
155 # ---------------------------------------------------------------------------
156
157 @pytest.mark.asyncio
158 async def test_p3_6_walk_new_commits_no_stop_at() -> None:
159 """_walk_new_commits without stop_at returns all commits sorted by timestamp.
160
161 Chain: C3 → C2 → C1. no stop_at.
162 Expected: sorted by timestamp → [C1, C2, C3].
163 """
164 from musehub.services.musehub_symbol_indexer import _walk_new_commits
165
166 c1 = _commit("C1", [], 1)
167 c2 = _commit("C2", ["C1"], 2)
168 c3 = _commit("C3", ["C2"], 3)
169
170 session = MagicMock()
171 # One bulk execute to fetch all commits for the repo
172 session.execute = AsyncMock(return_value=_exec_result(scalars=[c3, c1, c2]))
173
174 result = await _walk_new_commits(session, "repo1", "C3", None)
175 assert [c.commit_id for c in result] == ["C1", "C2", "C3"], (
176 f"Expected [C1, C2, C3] sorted oldest→newest, got {[c.commit_id for c in result]}"
177 )
178
179
180 # ---------------------------------------------------------------------------
181 # P3-7 Behavioural — _resolve_ancestor_manifest
182 # ---------------------------------------------------------------------------
183
184 @pytest.mark.asyncio
185 async def test_p3_7_resolve_ancestor_manifest_finds_lca() -> None:
186 """_resolve_ancestor_manifest returns the ancestor's snapshot manifest.
187
188 Diamond: BASE ← L1 ← L2 (from_branch tip = L2)
189 ← R1 (to_branch tip = R1)
190
191 LCA = BASE. Expect the manifest at BASE's snapshot.
192 """
193 from musehub.services.musehub_proposals import _resolve_ancestor_manifest
194
195 base = _commit("BASE", [], 1, snapshot_id="snap-BASE")
196 r1 = _commit("R1", ["BASE"], 2, snapshot_id="snap-R1")
197 l1 = _commit("L1", ["BASE"], 2, snapshot_id="snap-L1")
198 l2 = _commit("L2", ["L1"], 3, snapshot_id="snap-L2")
199
200 commits = {"BASE": base, "R1": r1, "L1": l1, "L2": l2}
201
202 to_branch_row = SimpleNamespace(head_commit_id="R1")
203 from_branch_row = SimpleNamespace(head_commit_id="L2")
204
205 session = MagicMock()
206 session.get = AsyncMock(side_effect=lambda model, pk: commits.get(pk))
207
208 expected_manifest = {"files": {"base_file.py": "sha256:abc"}}
209
210 with patch(
211 "musehub.services.musehub_proposals._get_branch",
212 new=AsyncMock(side_effect=[to_branch_row, from_branch_row]),
213 ), patch(
214 "musehub.services.musehub_snapshot.get_snapshot_manifest",
215 new=AsyncMock(return_value=expected_manifest),
216 ):
217 result = await _resolve_ancestor_manifest(
218 session, "repo1", "from_branch", "to_branch"
219 )
220
221 assert result == expected_manifest, (
222 f"Expected manifest at BASE, got {result}"
223 )
224
225
226 # ---------------------------------------------------------------------------
227 # P3-8 Behavioural — _is_ancestor_db: True case
228 # ---------------------------------------------------------------------------
229
230 @pytest.mark.asyncio
231 async def test_p3_8_is_ancestor_db_true() -> None:
232 """_is_ancestor_db returns True when ancestor is reachable.
233
234 Chain: C3 → C2 → C1. ancestor=C1, descendant=C3 → True.
235 """
236 from musehub.services.musehub_wire import _is_ancestor_db
237
238 c1 = _commit("C1", [], 1)
239 c2 = _commit("C2", ["C1"], 2)
240 c3 = _commit("C3", ["C2"], 3)
241
242 session = MagicMock()
243 # DFS from C3: adj("C3"), adj("C2"), adj("C1") → C1 yielded → match
244 session.execute = AsyncMock(side_effect=[
245 _exec_result(scalar=c3),
246 _exec_result(scalar=c2),
247 _exec_result(scalar=c1),
248 ])
249
250 assert await _is_ancestor_db(session, "C1", "C3", "repo1") is True
251
252
253 # ---------------------------------------------------------------------------
254 # P3-9 Behavioural — _is_ancestor_db: max_hops cap → False
255 # ---------------------------------------------------------------------------
256
257 @pytest.mark.asyncio
258 async def test_p3_9_is_ancestor_db_max_hops_cap() -> None:
259 """_is_ancestor_db returns False when max_hops is reached before finding ancestor.
260
261 Chain: C5 → C4 → C3 → C2 → C1.
262 ancestor=C1, descendant=C5, max_hops=3 → walk stops at C3 → False.
263 """
264 from musehub.services.musehub_wire import _is_ancestor_db
265
266 c1 = _commit("C1", [], 1)
267 c2 = _commit("C2", ["C1"], 2)
268 c3 = _commit("C3", ["C2"], 3)
269 c4 = _commit("C4", ["C3"], 4)
270 c5 = _commit("C5", ["C4"], 5)
271
272 session = MagicMock()
273 # DFS from C5, max 3 nodes: adj("C5"), adj("C4"), adj("C3") → stop
274 session.execute = AsyncMock(side_effect=[
275 _exec_result(scalar=c5),
276 _exec_result(scalar=c4),
277 _exec_result(scalar=c3),
278 ])
279
280 assert await _is_ancestor_db(session, "C1", "C5", "repo1", max_hops=3) is False
File History 2 commits
sha256:d8cbca3a06f39f82f66be6c29de3f41c3dec5f367722958fb5454dcbc007cc15 fix: rc11 test fixes — event→verdict rename, migration coun… Sonnet 4.6 patch 15 days ago
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 22 days ago