gabriel / musehub public
test_musehub_stash_models.py python
268 lines 8.5 KB
e6fad116 Remove all Stori, Maestro, and AgentCeption references; rebrand to Muse VCS Gabriel Cardona <gabriel@tellurstori.com> 6d ago
1 """Unit tests for MusehubStash and MusehubStashEntry ORM models.
2
3 Verifies:
4 - MusehubStash instantiation: id auto-generated, is_applied=False by default,
5 created_at populated, applied_at None on creation.
6 - MusehubStashEntry instantiation: id auto-generated, position stored correctly.
7 - Relationship: MusehubStash.entries returns entries ordered by position.
8 - applied_at lifecycle: None on creation, can be set to a UTC datetime.
9 """
10 from __future__ import annotations
11
12 import uuid
13 from collections.abc import AsyncGenerator
14 from datetime import datetime, timezone
15
16 import pytest
17 from sqlalchemy import select
18 from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
19 from sqlalchemy.orm import selectinload
20
21 from musehub.db.database import Base
22 from musehub.db import models as _user_models # noqa: F401 — register muse_users table
23 from musehub.db import musehub_models as _hub_models # noqa: F401 — register musehub_repos table
24 from musehub.db.musehub_stash_models import MusehubStash, MusehubStashEntry
25
26
27 # ---------------------------------------------------------------------------
28 # Fixtures
29 # ---------------------------------------------------------------------------
30
31
32 @pytest.fixture
33 async def async_session() -> AsyncGenerator[AsyncSession, None]:
34 """In-memory SQLite async session.
35
36 SQLite does not enforce foreign-key constraints by default, so we can
37 insert stash records without needing real parent repo/user rows. All
38 tables registered on Base.metadata are created so the schema is
39 consistent across the whole suite.
40 """
41 engine = create_async_engine("sqlite+aiosqlite:///:memory:")
42 async with engine.begin() as conn:
43 await conn.run_sync(Base.metadata.create_all)
44 Session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
45 async with Session() as session:
46 yield session
47 await engine.dispose()
48
49
50 def _repo_id() -> str:
51 return str(uuid.uuid4())
52
53
54 def _user_id() -> str:
55 return str(uuid.uuid4())
56
57
58 def _make_stash(
59 repo_id: str | None = None,
60 user_id: str | None = None,
61 branch: str = "main",
62 message: str | None = None,
63 ) -> MusehubStash:
64 """Build a MusehubStash without committing it to any session."""
65 return MusehubStash(
66 repo_id=repo_id or _repo_id(),
67 user_id=user_id or _user_id(),
68 branch=branch,
69 message=message,
70 )
71
72
73 # ---------------------------------------------------------------------------
74 # MusehubStash — instantiation defaults
75 # ---------------------------------------------------------------------------
76
77
78 @pytest.mark.anyio
79 async def test_stash_defaults_id_generated(async_session: AsyncSession) -> None:
80 """id is auto-generated as a non-empty UUID string on flush."""
81 stash = _make_stash()
82 async_session.add(stash)
83 await async_session.flush()
84 assert stash.id is not None
85 assert len(stash.id) == 36 # UUID canonical form: 8-4-4-4-12
86
87
88 @pytest.mark.anyio
89 async def test_stash_defaults_is_applied_false(async_session: AsyncSession) -> None:
90 """is_applied is False by default."""
91 stash = _make_stash()
92 async_session.add(stash)
93 await async_session.flush()
94 assert stash.is_applied is False
95
96
97 @pytest.mark.anyio
98 async def test_stash_defaults_created_at_set(async_session: AsyncSession) -> None:
99 """created_at is populated automatically on flush and is not None."""
100 stash = _make_stash()
101 async_session.add(stash)
102 await async_session.flush()
103 assert stash.created_at is not None
104 assert isinstance(stash.created_at, datetime)
105
106
107 @pytest.mark.anyio
108 async def test_stash_defaults_applied_at_none(async_session: AsyncSession) -> None:
109 """applied_at is None on creation — stash has not been popped yet."""
110 stash = _make_stash()
111 async_session.add(stash)
112 await async_session.flush()
113 assert stash.applied_at is None
114
115
116 @pytest.mark.anyio
117 async def test_stash_optional_message_stored(async_session: AsyncSession) -> None:
118 """Optional message field is persisted when provided."""
119 stash = _make_stash(message="WIP: rough intro")
120 async_session.add(stash)
121 await async_session.flush()
122 assert stash.message == "WIP: rough intro"
123
124
125 # ---------------------------------------------------------------------------
126 # MusehubStash — applied_at lifecycle
127 # ---------------------------------------------------------------------------
128
129
130 @pytest.mark.anyio
131 async def test_stash_applied_at_can_be_set(async_session: AsyncSession) -> None:
132 """applied_at can be set to a UTC datetime after creation."""
133 stash = _make_stash()
134 async_session.add(stash)
135 await async_session.flush()
136
137 applied_ts = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
138 stash.applied_at = applied_ts
139 stash.is_applied = True
140 await async_session.flush()
141
142 assert stash.applied_at == applied_ts
143 assert stash.is_applied is True
144
145
146 # ---------------------------------------------------------------------------
147 # MusehubStashEntry — instantiation
148 # ---------------------------------------------------------------------------
149
150
151 @pytest.mark.anyio
152 async def test_entry_id_generated(async_session: AsyncSession) -> None:
153 """MusehubStashEntry id is auto-generated as a UUID string on flush."""
154 stash = _make_stash()
155 async_session.add(stash)
156 await async_session.flush()
157
158 entry = MusehubStashEntry(
159 stash_id=stash.id,
160 path="tracks/bass.mid",
161 object_id="sha256:abc123",
162 position=0,
163 )
164 async_session.add(entry)
165 await async_session.flush()
166
167 assert entry.id is not None
168 assert len(entry.id) == 36
169
170
171 @pytest.mark.anyio
172 async def test_entry_position_stored_correctly(async_session: AsyncSession) -> None:
173 """position field is persisted exactly as provided."""
174 stash = _make_stash()
175 async_session.add(stash)
176 await async_session.flush()
177
178 entry = MusehubStashEntry(
179 stash_id=stash.id,
180 path="tracks/keys.mid",
181 object_id="sha256:deadbeef",
182 position=7,
183 )
184 async_session.add(entry)
185 await async_session.flush()
186
187 assert entry.position == 7
188
189
190 # ---------------------------------------------------------------------------
191 # Relationship: entries ordered by position
192 # ---------------------------------------------------------------------------
193
194
195 @pytest.mark.anyio
196 async def test_stash_entries_ordered_by_position(async_session: AsyncSession) -> None:
197 """MusehubStash.entries returns entries in ascending position order."""
198 stash = _make_stash()
199 async_session.add(stash)
200 await async_session.flush()
201 stash_id = stash.id
202
203 # Insert in reverse order to ensure ordering is by position, not insert order.
204 for pos in [2, 0, 1]:
205 entry = MusehubStashEntry(
206 stash_id=stash_id,
207 path=f"tracks/track-{pos}.mid",
208 object_id=f"sha256:{pos:04x}",
209 position=pos,
210 )
211 async_session.add(entry)
212
213 await async_session.commit()
214
215 result = await async_session.execute(
216 select(MusehubStash)
217 .where(MusehubStash.id == stash_id)
218 .options(selectinload(MusehubStash.entries))
219 )
220 loaded = result.scalar_one()
221 positions = [e.position for e in loaded.entries]
222 assert positions == [0, 1, 2]
223
224
225 @pytest.mark.anyio
226 async def test_stash_entries_empty_on_creation(async_session: AsyncSession) -> None:
227 """A freshly created stash has an empty entries list."""
228 stash = _make_stash()
229 async_session.add(stash)
230 await async_session.commit()
231 stash_id = stash.id
232
233 result = await async_session.execute(
234 select(MusehubStash)
235 .where(MusehubStash.id == stash_id)
236 .options(selectinload(MusehubStash.entries))
237 )
238 loaded = result.scalar_one()
239 assert loaded.entries == []
240
241
242 @pytest.mark.anyio
243 async def test_stash_entries_path_and_object_id_stored(async_session: AsyncSession) -> None:
244 """path and object_id are persisted exactly as provided on each entry."""
245 stash = _make_stash()
246 async_session.add(stash)
247 await async_session.flush()
248 stash_id = stash.id
249
250 entry = MusehubStashEntry(
251 stash_id=stash_id,
252 path="tracks/lead.mid",
253 object_id="sha256:cafebabe",
254 position=0,
255 )
256 async_session.add(entry)
257 await async_session.commit()
258
259 result = await async_session.execute(
260 select(MusehubStash)
261 .where(MusehubStash.id == stash_id)
262 .options(selectinload(MusehubStash.entries))
263 )
264 loaded = result.scalar_one()
265 assert len(loaded.entries) == 1
266 persisted = loaded.entries[0]
267 assert persisted.path == "tracks/lead.mid"
268 assert persisted.object_id == "sha256:cafebabe"