Files
FabledGrimoire/tests/test_snippet_provenance.py
T
bvandeusenandClaude Opus 5.5 e99f1169f7
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
rename(547): what people read says Grimoire
Step 1 of the Scribe -> Grimoire rename: every capitalised "Scribe" in prose
becomes "Grimoire" ("Fabled Scribe" -> "Fabled Grimoire") across the UI,
the PWA manifest and page title, the MCP instructions and tool docstrings,
hook and skill text, docs and README, with the tests that assert on that
prose moving with it. Old migrations keep their wording.

Identifiers are untouched here: the package, plugin name, MCP server key,
hook file names, env vars, the .scribe marker and FabledScribe repo/image
names move in steps 2 and 3, along with prose that quotes them.

The sweep matched "Scribe" only as a whole word, so describe/subscribe and
friends are untouched. The longer name took the MCP instructions one
character over their 1,600 budget; two phrases are shortened to fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:46:37 -04:00

168 lines
6.3 KiB
Python

"""Body provenance — the cache-with-provenance half of the pointer model.
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 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 (a DB-touching path
with only mocked coverage is a path with no coverage).
"""
import pytest
import pytest_asyncio
from tests.helpers import ensure_user
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, 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 user_id(_dispose_engine):
from scribe.models import async_session
async with async_session() as s:
uid = (await ensure_user(s, "snippet_prov_itest")).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="Grimoire", 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="Grimoire", 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