Files
FabledGrimoire/tests/test_integration_backup_file_ordering.py
T
bvandeusenandClaude Opus 5.5 79de8938b9 docs(6209): forty test files — design cascade and stylesheet, embeddings, family canon, forge, backup round trips, inception, placement, platforms, moments, rule move and verification, search scope, facets
Module and test docstrings state what is pinned without the record number
or step that introduced it. Incident accounts become the failure shape in
the present tense: the pipefail head, the code_shapes import, the export
order, the facet dialects, the cross-kind repeat, the write-once kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:40:25 -04:00

95 lines
3.1 KiB
Python

"""Real-Postgres check that the full export is ORDERED, so the backup file
deduplicates between runs.
Only Postgres shows this. An unordered select returns rows in heap order, and
an UPDATE writes the row's new version at the end of the heap — so without an
ORDER BY in export_full_backup, editing one old note moves it to the bottom of
`notes`. Every byte after its old place shifts, and a restic run over the next
backup stores most of the table again. A mocked session returns
whatever order it was handed and could never catch it.
"""
import io
import tarfile
import pytest
import pytest_asyncio
from sqlalchemy import select
from scribe.models import async_session
from scribe.models.note import Note
from scribe.models.user import User
from scribe.services import backup
from scribe.services.backup_file import write_archive_to
from tests.helpers import ensure_user
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
USERNAME = "backup_file_ordering_owner"
async def _purge() -> None:
async with async_session() as s:
for user in (await s.execute(
select(User).where(User.username == USERNAME)
)).scalars().all():
for note in (await s.execute(
select(Note).where(Note.user_id == user.id)
)).scalars().all():
await s.delete(note)
await s.delete(user)
await s.commit()
@pytest_asyncio.fixture(autouse=True)
async def _no_leftovers():
"""Setup only — see the rule_version roundtrip module for why a teardown
here would break the next test's event loop."""
await _purge()
@pytest_asyncio.fixture
async def notes():
async with async_session() as s:
user = await ensure_user(s, USERNAME)
rows = [Note(user_id=user.id, title=f"ordering {n}", body="b" * 100)
for n in range(5)]
s.add_all(rows)
await s.commit()
ids = [n.id for n in rows]
yield ids
await _purge()
def _sections(payload: dict) -> dict[str, bytes]:
buf = io.BytesIO()
write_archive_to(payload, buf, app_version="test")
buf.seek(0)
with tarfile.open(fileobj=buf, mode="r:") as tar:
return {m.name: tar.extractfile(m).read() for m in tar.getmembers()
if m.name != "manifest.json"}
async def _edit_first(ids: list[int]) -> None:
async with async_session() as s:
note = await s.get(Note, ids[0])
note.body = "edited after the first export"
await s.commit()
async def test_an_edited_row_keeps_its_place(notes):
await _edit_first(notes)
payload = await backup.export_full_backup()
exported = [row["id"] for row in payload["notes"]]
assert exported == sorted(exported)
mine = [i for i in exported if i in set(notes)]
assert mine == notes
async def test_two_exports_of_unchanged_data_are_the_same_bytes(notes):
await _edit_first(notes)
first = _sections(await backup.export_full_backup())
second = _sections(await backup.export_full_backup())
assert first.keys() == second.keys()
for name in first:
assert first[name] == second[name], name