gabriel / musehub public
test_phase3_db_walk_dag_async.py python
277 lines 10.5 KB
Raw
sha256:94ef169c149a452bff7c604ded8b280b19bd477c2dabcb56972780b0b784c7aa Merge 'fix/assignee-sigil-inline' into 'dev' — proposal: As… Human 1 day 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 # One bulk execute returns all repo commits; walk then excludes stop_at=C1 in memory
142 session.execute = AsyncMock(return_value=_exec_result(scalars=[c1, c2, c3]))
143
144 result = await _walk_new_commits(session, "repo1", "C3", "C1")
145 assert [c.commit_id for c in result] == ["C2", "C3"], (
146 f"Expected [C2, C3] sorted oldest→newest, got {[c.commit_id for c in result]}"
147 )
148
149
150 # ---------------------------------------------------------------------------
151 # P3-6 Behavioural — _walk_new_commits without stop_at
152 # ---------------------------------------------------------------------------
153
154 @pytest.mark.asyncio
155 async def test_p3_6_walk_new_commits_no_stop_at() -> None:
156 """_walk_new_commits without stop_at returns all commits sorted by timestamp.
157
158 Chain: C3 → C2 → C1. no stop_at.
159 Expected: sorted by timestamp → [C1, C2, C3].
160 """
161 from musehub.services.musehub_symbol_indexer import _walk_new_commits
162
163 c1 = _commit("C1", [], 1)
164 c2 = _commit("C2", ["C1"], 2)
165 c3 = _commit("C3", ["C2"], 3)
166
167 session = MagicMock()
168 # One bulk execute to fetch all commits for the repo
169 session.execute = AsyncMock(return_value=_exec_result(scalars=[c3, c1, c2]))
170
171 result = await _walk_new_commits(session, "repo1", "C3", None)
172 assert [c.commit_id for c in result] == ["C1", "C2", "C3"], (
173 f"Expected [C1, C2, C3] sorted oldest→newest, got {[c.commit_id for c in result]}"
174 )
175
176
177 # ---------------------------------------------------------------------------
178 # P3-7 Behavioural — _resolve_ancestor_manifest
179 # ---------------------------------------------------------------------------
180
181 @pytest.mark.asyncio
182 async def test_p3_7_resolve_ancestor_manifest_finds_lca() -> None:
183 """_resolve_ancestor_manifest returns the ancestor's snapshot manifest.
184
185 Diamond: BASE ← L1 ← L2 (from_branch tip = L2)
186 ← R1 (to_branch tip = R1)
187
188 LCA = BASE. Expect the manifest at BASE's snapshot.
189 """
190 from musehub.services.musehub_proposals import _resolve_ancestor_manifest
191
192 base = _commit("BASE", [], 1, snapshot_id="snap-BASE")
193 r1 = _commit("R1", ["BASE"], 2, snapshot_id="snap-R1")
194 l1 = _commit("L1", ["BASE"], 2, snapshot_id="snap-L1")
195 l2 = _commit("L2", ["L1"], 3, snapshot_id="snap-L2")
196
197 commits = {"BASE": base, "R1": r1, "L1": l1, "L2": l2}
198
199 to_branch_row = SimpleNamespace(head_commit_id="R1")
200 from_branch_row = SimpleNamespace(head_commit_id="L2")
201
202 session = MagicMock()
203 session.get = AsyncMock(side_effect=lambda model, pk: commits.get(pk))
204
205 expected_manifest = {"files": {"base_file.py": "sha256:abc"}}
206
207 with patch(
208 "musehub.services.musehub_proposals._get_branch",
209 new=AsyncMock(side_effect=[to_branch_row, from_branch_row]),
210 ), patch(
211 "musehub.services.musehub_snapshot.get_snapshot_manifest",
212 new=AsyncMock(return_value=expected_manifest),
213 ):
214 result = await _resolve_ancestor_manifest(
215 session, "repo1", "from_branch", "to_branch"
216 )
217
218 assert result == expected_manifest, (
219 f"Expected manifest at BASE, got {result}"
220 )
221
222
223 # ---------------------------------------------------------------------------
224 # P3-8 Behavioural — _is_ancestor_db: True case
225 # ---------------------------------------------------------------------------
226
227 @pytest.mark.asyncio
228 async def test_p3_8_is_ancestor_db_true() -> None:
229 """_is_ancestor_db returns True when ancestor is reachable.
230
231 Chain: C3 → C2 → C1. ancestor=C1, descendant=C3 → True.
232 """
233 from musehub.services.musehub_wire import _is_ancestor_db
234
235 c1 = _commit("C1", [], 1)
236 c2 = _commit("C2", ["C1"], 2)
237 c3 = _commit("C3", ["C2"], 3)
238
239 session = MagicMock()
240 # DFS from C3: adj("C3"), adj("C2"), adj("C1") → C1 yielded → match
241 session.execute = AsyncMock(side_effect=[
242 _exec_result(scalar=c3),
243 _exec_result(scalar=c2),
244 _exec_result(scalar=c1),
245 ])
246
247 assert await _is_ancestor_db(session, "C1", "C3", "repo1") is True
248
249
250 # ---------------------------------------------------------------------------
251 # P3-9 Behavioural — _is_ancestor_db: max_hops cap → False
252 # ---------------------------------------------------------------------------
253
254 @pytest.mark.asyncio
255 async def test_p3_9_is_ancestor_db_max_hops_cap() -> None:
256 """_is_ancestor_db returns False when max_hops is reached before finding ancestor.
257
258 Chain: C5 → C4 → C3 → C2 → C1.
259 ancestor=C1, descendant=C5, max_hops=3 → walk stops at C3 → False.
260 """
261 from musehub.services.musehub_wire import _is_ancestor_db
262
263 c1 = _commit("C1", [], 1)
264 c2 = _commit("C2", ["C1"], 2)
265 c3 = _commit("C3", ["C2"], 3)
266 c4 = _commit("C4", ["C3"], 4)
267 c5 = _commit("C5", ["C4"], 5)
268
269 session = MagicMock()
270 # DFS from C5, max 3 nodes: adj("C5"), adj("C4"), adj("C3") → stop
271 session.execute = AsyncMock(side_effect=[
272 _exec_result(scalar=c5),
273 _exec_result(scalar=c4),
274 _exec_result(scalar=c3),
275 ])
276
277 assert await _is_ancestor_db(session, "C1", "C5", "repo1", max_hops=3) is False
File History 3 commits
sha256:94ef169c149a452bff7c604ded8b280b19bd477c2dabcb56972780b0b784c7aa Merge 'fix/assignee-sigil-inline' into 'dev' — proposal: As… Human 1 day ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 9 days ago
sha256:4aed3d8601c8dd3ed37074de35f11f4a9699a0a4b99d43727048fd3f8e6fd13d chore: doc sweep, ignore wrangler build state, misc fixes Sonnet 4.6 minor 12 days ago