"""Body provenance — the cache-with-provenance half of the pointer model (#2688). Decision #2686: the recorded location is the source of truth for a snippet's code and the stored body is a CACHE of it. `data.provenance` records what that cache is a cache OF — the commit the body was read at, and when — so staleness becomes judgeable instead of guessed, and so the forge fetch (milestone 288 step 5) has something to refresh. The rules under test, because each has a way to rot silently: - Provenance follows the CODE. An edit that changes the code without a fresh SHA must DROP the stamp — carrying it would claim the new body came from the old commit, which is worse than not knowing. - Writes that are ABOUT the code rather than changes TO it (a verification verdict, a metadata edit) must CARRY it — record_verification rebuilds `data` from scratch, so forgetting the field there erases it invisibly. - An "ok" verdict at a known commit RESTAMPS it: the checker just proved the cached body matches the source there. Unit tests cover the compose/carry logic; the integration section runs the same rules through the real service paths on real Postgres (the #2663 lesson — a DB-touching path with only mocked coverage is a path with no coverage). """ import pytest import pytest_asyncio from scribe.services.snippets import ( VERIFY_CHANGED, VERIFY_OK, compose_data, compose_provenance, compose_verification, snippet_fields, verification_view, ) SHA_A = "a" * 40 SHA_B = "b" * 40 # --- unit: composing --------------------------------------------------------- def test_compose_provenance_stamps_sha_and_time(): prov = compose_provenance(commit_sha=f" {SHA_A} ") assert prov["commit_sha"] == SHA_A assert prov["fetched_at"] # ISO stamp, defaulted def test_compose_provenance_without_a_sha_is_none_not_an_empty_record(): # Absent provenance must stay ABSENT (the pre-#2688 semantics), never an # empty dict that readers would have to distinguish from a real one. assert compose_provenance(commit_sha="") is None assert compose_provenance(commit_sha=" ") is None def test_compose_data_carries_provenance_only_when_present(): with_it = compose_data(name="x", provenance={"commit_sha": SHA_A, "fetched_at": "t"}) without = compose_data(name="x", provenance=None) assert with_it["provenance"]["commit_sha"] == SHA_A assert "provenance" not in without def test_verification_records_and_reads_back_the_checked_commit(): verdict = compose_verification( status=VERIFY_OK, checked_code_sha="c" * 32, commit_sha=SHA_A, ) assert verdict["commit_sha"] == SHA_A # And an empty one is omitted, not stored as "". bare = compose_verification(status=VERIFY_OK, checked_code_sha="c" * 32) assert "commit_sha" not in bare class _N: # minimal note stand-in for the read-time view data = None fields = {"code": "", "verification": verdict} view = verification_view(_N(), fields) assert view["commit_sha"] == SHA_A # --- integration: the rules through the real service paths ------------------- @pytest_asyncio.fixture async def _dispose_engine(): from scribe.models import engine yield await engine.dispose() @pytest_asyncio.fixture async def user_id(_dispose_engine): # Get-or-create: the lane's database persists across tests, so a second # test re-creating the same username dies on the unique constraint. from sqlalchemy import select from scribe.models import async_session from scribe.models.user import User async with async_session() as s: existing = ( await s.execute( select(User).where(User.username == "snippet_prov_itest") ) ).scalar_one_or_none() if existing is not None: return existing.id user = User(username="snippet_prov_itest") s.add(user) await s.flush() uid = user.id await s.commit() return uid async def _fresh(uid, note_id): from scribe.services import snippets as svc note = await svc.get_snippet(uid, note_id) return snippet_fields(note), svc.snippet_to_dict(note) @pytest.mark.integration async def test_provenance_lives_and_dies_with_the_code_end_to_end(user_id): from scribe.services import snippets as svc note = await svc.create_snippet( user_id, name="prov_helper", code="def prov_helper():\n return 1\n", language="python", repo="Scribe", path="src/x.py", symbol="prov_helper", commit_sha=SHA_A, ) fields, view = await _fresh(user_id, note.id) assert fields["provenance"]["commit_sha"] == SHA_A assert view["snippet"]["provenance"]["commit_sha"] == SHA_A # A metadata edit leaves the code alone → carried. await svc.update_snippet(user_id, note.id, when_to_use="when proving") fields, _ = await _fresh(user_id, note.id) assert fields["provenance"]["commit_sha"] == SHA_A # A code edit with a fresh SHA → restamped. await svc.update_snippet( user_id, note.id, code="def prov_helper():\n return 2\n", commit_sha=SHA_B, ) fields, _ = await _fresh(user_id, note.id) assert fields["provenance"]["commit_sha"] == SHA_B # A code edit WITHOUT one → dropped, not carried: the new body does not # come from SHA_B and the record must not claim it does. await svc.update_snippet( user_id, note.id, code="def prov_helper():\n return 3\n", ) fields, view = await _fresh(user_id, note.id) assert "provenance" not in fields assert "provenance" not in view["snippet"] @pytest.mark.integration async def test_verification_stamps_the_commit_and_ok_refreshes_provenance(user_id): from scribe.services import snippets as svc note = await svc.create_snippet( user_id, name="prov_verify", code="def prov_verify():\n return 1\n", language="python", repo="Scribe", path="src/y.py", symbol="prov_verify", commit_sha=SHA_A, ) # A non-ok verdict at a newer commit records where the check ran but must # CARRY provenance — the check didn't change what the cached body is. await svc.record_verification( user_id, note.id, status=VERIFY_CHANGED, detail="diverged", commit_sha=SHA_B, ) fields, view = await _fresh(user_id, note.id) assert view["verification"]["commit_sha"] == SHA_B assert fields["provenance"]["commit_sha"] == SHA_A # An OK verdict at that commit proves the cache matches the source there — # provenance refreshes without an edit. await svc.record_verification( user_id, note.id, status=VERIFY_OK, detail="matches", commit_sha=SHA_B, ) fields, view = await _fresh(user_id, note.id) assert view["verification"]["commit_sha"] == SHA_B assert fields["provenance"]["commit_sha"] == SHA_B # And the verdict itself survives untouched by the restamp. assert view["verification"]["status"] == VERIFY_OK assert view["verification"]["current"] is True