gabriel / musehub public
test_symbols_v2_p4_intel_fields.py python
402 lines 14.9 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 122 days ago
1 """TDD spec — Phase 4: populate op and last_commit_id on musehub_symbol_intel.
2
3 Problem
4 ───────
5 musehub_symbol_intel.op and last_commit_id are always NULL.
6 The columns exist on the model and are read by the symbol list route,
7 but _compute_symbol_intel never computes them and _upsert_symbol_intel
8 never writes them.
9
10 Solution
11 ────────
12 1. Add last_op and last_commit_id to SymbolIntel TypedDict.
13 2. _compute_symbol_intel: track them alongside last_author (same loop,
14 same "is this the newest ts?" guard).
15 3. _upsert_symbol_intel: include op and last_commit_id in the row dict
16 and in the on_conflict set_{} update.
17 4. backfill_intel_fields(session, repo_id): one UPDATE...FROM that reads
18 the most-recent history entry per (repo_id, address) and writes op +
19 last_commit_id into intel rows — so existing records are fixed without
20 a full re-index.
21
22 Tier breakdown
23 ──────────────
24 I401 _compute_symbol_intel returns last_op per symbol
25 I402 _compute_symbol_intel returns last_commit_id per symbol
26 I403 last_op reflects the most recent commit's op, not the first
27 I404 _upsert_symbol_intel writes op to musehub_symbol_intel
28 I405 _upsert_symbol_intel writes last_commit_id to musehub_symbol_intel
29 I406 build_symbol_index populates op end-to-end
30 I407 build_symbol_index populates last_commit_id end-to-end
31 I408 backfill_intel_fields fixes NULL op from existing history entries
32 I409 backfill_intel_fields fixes NULL last_commit_id from existing history
33 I410 backfill_intel_fields is idempotent (safe to run twice)
34 """
35 from __future__ import annotations
36
37 import secrets
38 from datetime import datetime, timezone, timedelta
39
40 import pytest
41 from sqlalchemy import select
42 from sqlalchemy.ext.asyncio import AsyncSession
43
44 from musehub.db import musehub_models as db
45 from muse.core.types import blob_id, long_id
46 from tests.factories import create_repo
47
48
49 # ---------------------------------------------------------------------------
50 # Helpers
51 # ---------------------------------------------------------------------------
52
53 def _now() -> datetime:
54 return datetime.now(tz=timezone.utc)
55
56
57 def _cid() -> str:
58 return blob_id(secrets.token_bytes(32))
59
60
61 def _lid() -> str:
62 return long_id(secrets.token_hex(32))
63
64
65 async def _make_commit(
66 session: AsyncSession,
67 repo_id: str,
68 addresses: list[str],
69 *,
70 parent_id: str | None = None,
71 branch: str = "dev",
72 message: str = "feat: test",
73 op: str = "insert",
74 ts: datetime | None = None,
75 ) -> db.MusehubCommit:
76 commit_id = _lid()
77 committed_at = ts or _now()
78 commit = db.MusehubCommit(
79 repo_id=repo_id,
80 commit_id=commit_id,
81 message=message,
82 author="gabriel",
83 branch=branch,
84 timestamp=committed_at,
85 parent_ids=[parent_id] if parent_id else [],
86 structured_delta={
87 "ops": [
88 {"address": addr, "op": op, "new_content_id": _cid()}
89 for addr in addresses
90 ]
91 },
92 )
93 session.add(commit)
94 await session.flush()
95 return commit
96
97
98 # ---------------------------------------------------------------------------
99 # I401 — _compute_symbol_intel returns last_op per symbol
100 # ---------------------------------------------------------------------------
101
102 @pytest.mark.asyncio
103 async def test_i401_compute_returns_last_op() -> None:
104 """_compute_symbol_intel must include last_op on every symbol entry."""
105 from musehub.services.musehub_symbol_indexer import _compute_symbol_intel
106
107 history = {
108 "src/a.py::fn": [
109 {"commit_id": _lid(), "committed_at": _now().isoformat(),
110 "author": "gabriel", "op": "insert", "op_payload": {}, "content_id": _cid()},
111 ]
112 }
113 result = _compute_symbol_intel(history)
114 assert "src/a.py::fn" in result
115 assert "last_op" in result["src/a.py::fn"]
116 assert result["src/a.py::fn"]["last_op"] == "insert"
117
118
119 # ---------------------------------------------------------------------------
120 # I402 — _compute_symbol_intel returns last_commit_id per symbol
121 # ---------------------------------------------------------------------------
122
123 @pytest.mark.asyncio
124 async def test_i402_compute_returns_last_commit_id() -> None:
125 """_compute_symbol_intel must include last_commit_id on every symbol entry."""
126 from musehub.services.musehub_symbol_indexer import _compute_symbol_intel
127
128 commit_id = _lid()
129 history = {
130 "src/b.py::fn": [
131 {"commit_id": commit_id, "committed_at": _now().isoformat(),
132 "author": "gabriel", "op": "replace", "op_payload": {}, "content_id": _cid()},
133 ]
134 }
135 result = _compute_symbol_intel(history)
136 assert result["src/b.py::fn"]["last_commit_id"] == commit_id
137
138
139 # ---------------------------------------------------------------------------
140 # I403 — last_op reflects the MOST RECENT commit, not the first
141 # ---------------------------------------------------------------------------
142
143 @pytest.mark.asyncio
144 async def test_i403_last_op_is_most_recent() -> None:
145 """When a symbol has multiple history entries, last_op must be from the newest."""
146 from musehub.services.musehub_symbol_indexer import _compute_symbol_intel
147
148 older = (_now() - timedelta(days=10)).isoformat()
149 newer = _now().isoformat()
150 cid_new = _lid()
151
152 history = {
153 "src/c.py::fn": [
154 {"commit_id": _lid(), "committed_at": older,
155 "author": "gabriel", "op": "insert", "op_payload": {}, "content_id": _cid()},
156 {"commit_id": cid_new, "committed_at": newer,
157 "author": "gabriel", "op": "replace", "op_payload": {}, "content_id": _cid()},
158 ]
159 }
160 result = _compute_symbol_intel(history)
161 assert result["src/c.py::fn"]["last_op"] == "replace"
162 assert result["src/c.py::fn"]["last_commit_id"] == cid_new
163
164
165 # ---------------------------------------------------------------------------
166 # I404 — _upsert_symbol_intel writes op to the DB
167 # ---------------------------------------------------------------------------
168
169 @pytest.mark.asyncio
170 async def test_i404_upsert_writes_op(db_session: AsyncSession) -> None:
171 """After _upsert_symbol_intel, musehub_symbol_intel.op must be set."""
172 from musehub.services.musehub_symbol_indexer import _upsert_symbol_intel
173
174 repo = await create_repo(db_session, owner="gabriel")
175 intel = {
176 "src/d.py::fn": {
177 "churn": 2, "churn_30d": 1, "churn_90d": 2,
178 "blast": 0, "blast_direct": 0, "blast_cross": 0, "blast_top": [],
179 "last_changed": _now().isoformat(), "last_author": "gabriel",
180 "author_count": 1, "gravity": 0.1, "weekly": [0] * 12,
181 "last_op": "replace",
182 "last_commit_id": _lid(),
183 }
184 }
185 await _upsert_symbol_intel(db_session, repo.repo_id, intel)
186 await db_session.flush()
187
188 row = (await db_session.execute(
189 select(db.MusehubSymbolIntel).where(
190 db.MusehubSymbolIntel.repo_id == repo.repo_id,
191 db.MusehubSymbolIntel.address == "src/d.py::fn",
192 )
193 )).scalar_one()
194 assert row.op == "replace"
195
196
197 # ---------------------------------------------------------------------------
198 # I405 — _upsert_symbol_intel writes last_commit_id to the DB
199 # ---------------------------------------------------------------------------
200
201 @pytest.mark.asyncio
202 async def test_i405_upsert_writes_last_commit_id(db_session: AsyncSession) -> None:
203 """After _upsert_symbol_intel, musehub_symbol_intel.last_commit_id must be set."""
204 from musehub.services.musehub_symbol_indexer import _upsert_symbol_intel
205
206 repo = await create_repo(db_session, owner="gabriel")
207 commit_id = _lid()
208 intel = {
209 "src/e.py::fn": {
210 "churn": 1, "churn_30d": 1, "churn_90d": 1,
211 "blast": 0, "blast_direct": 0, "blast_cross": 0, "blast_top": [],
212 "last_changed": _now().isoformat(), "last_author": "gabriel",
213 "author_count": 1, "gravity": 0.0, "weekly": [0] * 12,
214 "last_op": "insert",
215 "last_commit_id": commit_id,
216 }
217 }
218 await _upsert_symbol_intel(db_session, repo.repo_id, intel)
219 await db_session.flush()
220
221 row = (await db_session.execute(
222 select(db.MusehubSymbolIntel).where(
223 db.MusehubSymbolIntel.repo_id == repo.repo_id,
224 db.MusehubSymbolIntel.address == "src/e.py::fn",
225 )
226 )).scalar_one()
227 assert row.last_commit_id == commit_id
228
229
230 # ---------------------------------------------------------------------------
231 # I406 — build_symbol_index populates op end-to-end
232 # ---------------------------------------------------------------------------
233
234 @pytest.mark.asyncio
235 async def test_i406_build_index_populates_op(db_session: AsyncSession) -> None:
236 """After build_symbol_index, musehub_symbol_intel.op must not be NULL."""
237 from musehub.services.musehub_symbol_indexer import build_symbol_index
238
239 repo = await create_repo(db_session, owner="gabriel")
240 commit = await _make_commit(
241 db_session, repo.repo_id, ["src/f.py::my_fn"], op="insert"
242 )
243 await db_session.flush()
244
245 await build_symbol_index(db_session, repo.repo_id, commit.commit_id)
246 await db_session.flush()
247
248 row = (await db_session.execute(
249 select(db.MusehubSymbolIntel).where(
250 db.MusehubSymbolIntel.repo_id == repo.repo_id,
251 db.MusehubSymbolIntel.address == "src/f.py::my_fn",
252 )
253 )).scalar_one()
254 assert row.op is not None
255 assert row.op == "insert"
256
257
258 # ---------------------------------------------------------------------------
259 # I407 — build_symbol_index populates last_commit_id end-to-end
260 # ---------------------------------------------------------------------------
261
262 @pytest.mark.asyncio
263 async def test_i407_build_index_populates_last_commit_id(db_session: AsyncSession) -> None:
264 """After build_symbol_index, musehub_symbol_intel.last_commit_id must not be NULL."""
265 from musehub.services.musehub_symbol_indexer import build_symbol_index
266
267 repo = await create_repo(db_session, owner="gabriel")
268 commit = await _make_commit(
269 db_session, repo.repo_id, ["src/g.py::helper"], op="insert"
270 )
271 await db_session.flush()
272
273 await build_symbol_index(db_session, repo.repo_id, commit.commit_id)
274 await db_session.flush()
275
276 row = (await db_session.execute(
277 select(db.MusehubSymbolIntel).where(
278 db.MusehubSymbolIntel.repo_id == repo.repo_id,
279 db.MusehubSymbolIntel.address == "src/g.py::helper",
280 )
281 )).scalar_one()
282 assert row.last_commit_id is not None
283 assert row.last_commit_id == commit.commit_id
284
285
286 # ---------------------------------------------------------------------------
287 # I408 — backfill_intel_fields fixes NULL op from existing history entries
288 # ---------------------------------------------------------------------------
289
290 @pytest.mark.asyncio
291 async def test_i408_backfill_fixes_null_op(db_session: AsyncSession) -> None:
292 """backfill_intel_fields must set op from the most recent history entry."""
293 from musehub.services.musehub_symbol_indexer import backfill_intel_fields
294
295 repo = await create_repo(db_session, owner="gabriel")
296 commit_id = _lid()
297
298 # Insert intel row with NULL op
299 db_session.add(db.MusehubSymbolIntel(
300 repo_id=repo.repo_id, address="src/h.py::fn",
301 churn=1, churn_30d=1, churn_90d=1,
302 blast=0, blast_direct=0, blast_cross=0, blast_top=[],
303 last_changed=_now(), author_count=1, gravity=0.0,
304 weekly=[0] * 12,
305 ))
306 # Insert matching history entry
307 db_session.add(db.MusehubSymbolHistoryEntry(
308 repo_id=repo.repo_id, address="src/h.py::fn",
309 commit_id=commit_id, committed_at=_now(),
310 author="gabriel", op="mutate",
311 ))
312 await db_session.flush()
313
314 await backfill_intel_fields(db_session, repo.repo_id)
315 await db_session.flush()
316
317 row = (await db_session.execute(
318 select(db.MusehubSymbolIntel).where(
319 db.MusehubSymbolIntel.repo_id == repo.repo_id,
320 db.MusehubSymbolIntel.address == "src/h.py::fn",
321 )
322 )).scalar_one()
323 assert row.op == "mutate"
324
325
326 # ---------------------------------------------------------------------------
327 # I409 — backfill_intel_fields fixes NULL last_commit_id
328 # ---------------------------------------------------------------------------
329
330 @pytest.mark.asyncio
331 async def test_i409_backfill_fixes_null_last_commit_id(db_session: AsyncSession) -> None:
332 """backfill_intel_fields must set last_commit_id from the most recent history entry."""
333 from musehub.services.musehub_symbol_indexer import backfill_intel_fields
334
335 repo = await create_repo(db_session, owner="gabriel")
336 commit_id = _lid()
337
338 db_session.add(db.MusehubSymbolIntel(
339 repo_id=repo.repo_id, address="src/i.py::fn",
340 churn=1, churn_30d=1, churn_90d=1,
341 blast=0, blast_direct=0, blast_cross=0, blast_top=[],
342 last_changed=_now(), author_count=1, gravity=0.0,
343 weekly=[0] * 12,
344 ))
345 db_session.add(db.MusehubSymbolHistoryEntry(
346 repo_id=repo.repo_id, address="src/i.py::fn",
347 commit_id=commit_id, committed_at=_now(),
348 author="gabriel", op="replace",
349 ))
350 await db_session.flush()
351
352 await backfill_intel_fields(db_session, repo.repo_id)
353 await db_session.flush()
354
355 row = (await db_session.execute(
356 select(db.MusehubSymbolIntel).where(
357 db.MusehubSymbolIntel.repo_id == repo.repo_id,
358 db.MusehubSymbolIntel.address == "src/i.py::fn",
359 )
360 )).scalar_one()
361 assert row.last_commit_id == commit_id
362
363
364 # ---------------------------------------------------------------------------
365 # I410 — backfill_intel_fields is idempotent
366 # ---------------------------------------------------------------------------
367
368 @pytest.mark.asyncio
369 async def test_i410_backfill_idempotent(db_session: AsyncSession) -> None:
370 """Running backfill_intel_fields twice must produce the same result."""
371 from musehub.services.musehub_symbol_indexer import backfill_intel_fields
372
373 repo = await create_repo(db_session, owner="gabriel")
374 commit_id = _lid()
375
376 db_session.add(db.MusehubSymbolIntel(
377 repo_id=repo.repo_id, address="src/j.py::fn",
378 churn=1, churn_30d=1, churn_90d=1,
379 blast=0, blast_direct=0, blast_cross=0, blast_top=[],
380 last_changed=_now(), author_count=1, gravity=0.0,
381 weekly=[0] * 12,
382 ))
383 db_session.add(db.MusehubSymbolHistoryEntry(
384 repo_id=repo.repo_id, address="src/j.py::fn",
385 commit_id=commit_id, committed_at=_now(),
386 author="gabriel", op="patch",
387 ))
388 await db_session.flush()
389
390 await backfill_intel_fields(db_session, repo.repo_id)
391 await db_session.flush()
392 await backfill_intel_fields(db_session, repo.repo_id)
393 await db_session.flush()
394
395 row = (await db_session.execute(
396 select(db.MusehubSymbolIntel).where(
397 db.MusehubSymbolIntel.repo_id == repo.repo_id,
398 db.MusehubSymbolIntel.address == "src/j.py::fn",
399 )
400 )).scalar_one()
401 assert row.op == "patch"
402 assert row.last_commit_id == commit_id
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 122 days ago