test_phase3_db_walk_dag_async.py
python
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