test_clones_state_integrity.py
python
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef
debug(push/stream): instrument O-frame decode path with INF…
Sonnet 4.6
patch
121 days ago
| 1 | """Tier 5 — State integrity tests for musehub_intel_clones (issue #17). |
| 2 | |
| 3 | Validates that the DB table invariants hold under normal and pathological |
| 4 | conditions: JSON parseability, tier enum, count/content agreement, upsert |
| 5 | idempotency, stale-row update, and CASCADE delete behavior. |
| 6 | |
| 7 | Cases: |
| 8 | SI01 All DB rows have parseable members_json |
| 9 | SI02 All tier values are exactly "exact" or "near" |
| 10 | SI03 member_count matches len(json.loads(members_json)) for each row |
| 11 | SI04 ClonesProvider upsert is idempotent — running twice = same row count |
| 12 | SI05 ClonesProvider upsert updates existing row on re-run with new count |
| 13 | SI06 CASCADE delete — deleting repo removes all clones rows |
| 14 | """ |
| 15 | from __future__ import annotations |
| 16 | |
| 17 | import json |
| 18 | from unittest.mock import AsyncMock, patch |
| 19 | |
| 20 | import pytest |
| 21 | import pytest_asyncio |
| 22 | import sqlalchemy as sa |
| 23 | from sqlalchemy.dialects.postgresql import insert as pg_insert |
| 24 | from sqlalchemy.ext.asyncio import AsyncSession |
| 25 | |
| 26 | from musehub.db import musehub_models as dbm |
| 27 | from muse.core.types import long_id |
| 28 | from tests.factories import create_repo |
| 29 | |
| 30 | _REF = long_id("a" * 64) |
| 31 | |
| 32 | |
| 33 | def _make_members(n: int, single_file: bool = False) -> str: |
| 34 | return json.dumps([ |
| 35 | { |
| 36 | "address": f"src/{'a' if single_file else chr(97+i%4)}.py::fn_{i}", |
| 37 | "kind": "function", |
| 38 | "language": "Python", |
| 39 | "body_hash": long_id("a" * 64), |
| 40 | "signature_id": long_id("b" * 64), |
| 41 | "content_id": long_id("a" * 64), |
| 42 | } |
| 43 | for i in range(n) |
| 44 | ]) |
| 45 | |
| 46 | |
| 47 | async def _insert( |
| 48 | session: AsyncSession, |
| 49 | repo_id: str, |
| 50 | cluster_hash: str, |
| 51 | tier: str = "exact", |
| 52 | member_count: int = 2, |
| 53 | members_json: str | None = None, |
| 54 | ) -> None: |
| 55 | mj = members_json if members_json is not None else _make_members(member_count) |
| 56 | await session.execute( |
| 57 | pg_insert(dbm.MusehubIntelClones) |
| 58 | .values( |
| 59 | repo_id=repo_id, |
| 60 | cluster_hash=cluster_hash, |
| 61 | tier=tier, |
| 62 | member_count=member_count, |
| 63 | members_json=mj, |
| 64 | ref=_REF, |
| 65 | ) |
| 66 | .on_conflict_do_update( |
| 67 | index_elements=["repo_id", "cluster_hash"], |
| 68 | set_={"tier": tier, "member_count": member_count, "members_json": mj}, |
| 69 | ) |
| 70 | ) |
| 71 | await session.commit() |
| 72 | |
| 73 | |
| 74 | @pytest_asyncio.fixture |
| 75 | async def repo(db_session: AsyncSession): |
| 76 | return await create_repo(db_session, owner="siuser", slug="state-integrity") |
| 77 | |
| 78 | |
| 79 | class TestClonesStateIntegrity: |
| 80 | |
| 81 | @pytest.mark.asyncio |
| 82 | async def test_SI01_all_rows_parseable_members_json( |
| 83 | self, db_session: AsyncSession, repo |
| 84 | ) -> None: |
| 85 | """Every members_json stored in the DB must deserialise without error.""" |
| 86 | for i in range(5): |
| 87 | await _insert( |
| 88 | db_session, str(repo.repo_id), |
| 89 | cluster_hash=f"sha256:si01{str(i).zfill(60)}", |
| 90 | member_count=i + 2, |
| 91 | members_json=_make_members(i + 2), |
| 92 | ) |
| 93 | |
| 94 | result = await db_session.execute( |
| 95 | sa.select(dbm.MusehubIntelClones).where( |
| 96 | dbm.MusehubIntelClones.repo_id == str(repo.repo_id) |
| 97 | ) |
| 98 | ) |
| 99 | rows = result.scalars().all() |
| 100 | for row in rows: |
| 101 | try: |
| 102 | parsed = json.loads(row.members_json) |
| 103 | assert isinstance(parsed, list) |
| 104 | except json.JSONDecodeError as exc: |
| 105 | pytest.fail(f"Unparseable members_json for {row.cluster_hash}: {exc}") |
| 106 | |
| 107 | @pytest.mark.asyncio |
| 108 | async def test_SI02_tier_values_in_valid_set( |
| 109 | self, db_session: AsyncSession, repo |
| 110 | ) -> None: |
| 111 | """All tier values must be exactly 'exact' or 'near'.""" |
| 112 | for tier in ("exact", "near", "exact"): |
| 113 | await _insert( |
| 114 | db_session, str(repo.repo_id), |
| 115 | cluster_hash=f"sha256:si02{tier[:1]}{str(id(tier)).zfill(59)}", |
| 116 | tier=tier, |
| 117 | ) |
| 118 | |
| 119 | result = await db_session.execute( |
| 120 | sa.select(dbm.MusehubIntelClones.tier).where( |
| 121 | dbm.MusehubIntelClones.repo_id == str(repo.repo_id) |
| 122 | ) |
| 123 | ) |
| 124 | for (tier,) in result.all(): |
| 125 | assert tier in ("exact", "near"), f"Unexpected tier value: {tier!r}" |
| 126 | |
| 127 | @pytest.mark.asyncio |
| 128 | async def test_SI03_member_count_matches_json_length( |
| 129 | self, db_session: AsyncSession, repo |
| 130 | ) -> None: |
| 131 | """member_count must equal the number of entries in members_json.""" |
| 132 | for n in (2, 5, 10): |
| 133 | await _insert( |
| 134 | db_session, str(repo.repo_id), |
| 135 | cluster_hash=f"sha256:si03n{n}{str(n).zfill(58)}", |
| 136 | member_count=n, |
| 137 | members_json=_make_members(n), |
| 138 | ) |
| 139 | |
| 140 | result = await db_session.execute( |
| 141 | sa.select(dbm.MusehubIntelClones).where( |
| 142 | dbm.MusehubIntelClones.repo_id == str(repo.repo_id) |
| 143 | ) |
| 144 | ) |
| 145 | for row in result.scalars().all(): |
| 146 | actual = len(json.loads(row.members_json)) |
| 147 | assert actual == row.member_count, ( |
| 148 | f"{row.cluster_hash}: member_count={row.member_count} " |
| 149 | f"but members_json has {actual} entries" |
| 150 | ) |
| 151 | |
| 152 | @pytest.mark.asyncio |
| 153 | async def test_SI04_upsert_is_idempotent( |
| 154 | self, db_session: AsyncSession, repo |
| 155 | ) -> None: |
| 156 | """Inserting the same cluster twice leaves exactly one row.""" |
| 157 | h = long_id("4" * 64) |
| 158 | mj = _make_members(3) |
| 159 | for _ in range(2): |
| 160 | await _insert( |
| 161 | db_session, str(repo.repo_id), |
| 162 | cluster_hash=h, member_count=3, members_json=mj, |
| 163 | ) |
| 164 | |
| 165 | count_result = await db_session.execute( |
| 166 | sa.select(sa.func.count()) |
| 167 | .select_from(dbm.MusehubIntelClones) |
| 168 | .where( |
| 169 | dbm.MusehubIntelClones.repo_id == str(repo.repo_id), |
| 170 | dbm.MusehubIntelClones.cluster_hash == h, |
| 171 | ) |
| 172 | ) |
| 173 | assert count_result.scalar_one() == 1 |
| 174 | |
| 175 | @pytest.mark.asyncio |
| 176 | async def test_SI05_upsert_updates_existing_row( |
| 177 | self, db_session: AsyncSession, repo |
| 178 | ) -> None: |
| 179 | """Re-running with a new member_count updates the existing row.""" |
| 180 | h = long_id("5" * 64) |
| 181 | await _insert(db_session, str(repo.repo_id), cluster_hash=h, member_count=2) |
| 182 | new_mj = _make_members(7) |
| 183 | await _insert( |
| 184 | db_session, str(repo.repo_id), |
| 185 | cluster_hash=h, member_count=7, members_json=new_mj, |
| 186 | ) |
| 187 | |
| 188 | result = await db_session.execute( |
| 189 | sa.select(dbm.MusehubIntelClones).where( |
| 190 | dbm.MusehubIntelClones.repo_id == str(repo.repo_id), |
| 191 | dbm.MusehubIntelClones.cluster_hash == h, |
| 192 | ) |
| 193 | ) |
| 194 | row = result.scalar_one() |
| 195 | assert row.member_count == 7 |
| 196 | assert len(json.loads(row.members_json)) == 7 |
| 197 | |
| 198 | @pytest.mark.asyncio |
| 199 | async def test_SI06_cascade_delete_removes_clones( |
| 200 | self, db_session: AsyncSession, repo |
| 201 | ) -> None: |
| 202 | """Deleting the repo cascades and removes all associated clone rows.""" |
| 203 | for i in range(3): |
| 204 | await _insert( |
| 205 | db_session, str(repo.repo_id), |
| 206 | cluster_hash=f"sha256:si06{str(i).zfill(60)}", |
| 207 | ) |
| 208 | |
| 209 | # Verify rows exist |
| 210 | before = await db_session.execute( |
| 211 | sa.select(sa.func.count()) |
| 212 | .select_from(dbm.MusehubIntelClones) |
| 213 | .where(dbm.MusehubIntelClones.repo_id == str(repo.repo_id)) |
| 214 | ) |
| 215 | assert before.scalar_one() == 3 |
| 216 | |
| 217 | # Delete the repo |
| 218 | await db_session.execute( |
| 219 | sa.delete(dbm.MusehubRepo).where( |
| 220 | dbm.MusehubRepo.repo_id == str(repo.repo_id) |
| 221 | ) |
| 222 | ) |
| 223 | await db_session.commit() |
| 224 | |
| 225 | # Clones rows must be gone |
| 226 | after = await db_session.execute( |
| 227 | sa.select(sa.func.count()) |
| 228 | .select_from(dbm.MusehubIntelClones) |
| 229 | .where(dbm.MusehubIntelClones.repo_id == str(repo.repo_id)) |
| 230 | ) |
| 231 | assert after.scalar_one() == 0 |
File History
1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef
debug(push/stream): instrument O-frame decode path with INF…
Sonnet 4.6
patch
121 days ago