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
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>
168 lines
6.3 KiB
Python
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
|