"""Real-Postgres tests for rule mounts (milestone 458 step 3). A mount is the claim "this rule arrives at this moment". These pin what that claim depends on: - the set given is the set stored, and a refused name stores nothing; - only the owner can mount; - the rule's detail reads its mounts back; - a backup carries them, attached to the RESTORED rule's new id rather than the old number. """ import pytest import pytest_asyncio from sqlalchemy import select from scribe.models import async_session from scribe.models.rulebook import Rule, Rulebook, RulebookTopic from scribe.models.user import User from scribe.services import backup from scribe.services import rulebooks as rulebooks_svc from tests.helpers import ensure_user pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")] OWNER_USERNAME = "rule_moments_owner" STRANGER_USERNAME = "rule_moments_stranger" RESTORED_USERNAME = "rule_moments_restored" TRIGGER = "about to tell the operator a piece of work is finished" async def _purge_books(username: str) -> None: """At SETUP: rule writes fire a detached embedding refresh, and a teardown delete races it (test_integration_rule_versions records why).""" async with async_session() as s: for user in (await s.execute( select(User).where(User.username == username) )).scalars().all(): for book in (await s.execute( select(Rulebook).where(Rulebook.owner_user_id == user.id) )).scalars().all(): await s.delete(book) await s.commit() @pytest_asyncio.fixture async def world(): for name in (OWNER_USERNAME, STRANGER_USERNAME, RESTORED_USERNAME): await _purge_books(name) # The restore below creates a user under a unique username; a previous # run's must be gone first, as in the sibling roundtrip files. async with async_session() as s: for user in (await s.execute( select(User).where(User.username == RESTORED_USERNAME) )).scalars().all(): await s.delete(user) await s.commit() async with async_session() as s: owner = await ensure_user(s, OWNER_USERNAME) stranger = await ensure_user(s, STRANGER_USERNAME) await s.commit() uid, sid = owner.id, stranger.id book = await rulebooks_svc.create_rulebook(uid, "Moment fixtures") topic = await rulebooks_svc.create_topic(book.id, uid, "verification") rule = await rulebooks_svc.create_rule( topic.id, uid, "Done means delivered and checked", "Work is done when it is delivered and its automated checks pass.", when_to_apply=TRIGGER, ) return {"uid": uid, "sid": sid, "book_id": book.id, "rule": rule} async def test_the_set_given_is_the_set_stored(world): uid, rule = world["uid"], world["rule"] assert await rulebooks_svc.set_rule_moments( rule.id, uid, ["reply.report", " Work.Finish"], ) == ["reply.report", "work.finish"] # Read back in CATALOG order, whatever order they were given in. assert (await rulebooks_svc.list_rule_moments([rule.id]))[rule.id] == [ "work.finish", "reply.report", ] await rulebooks_svc.set_rule_moments(rule.id, uid, ["work.deliver"]) assert (await rulebooks_svc.list_rule_moments([rule.id]))[rule.id] == ["work.deliver"] await rulebooks_svc.set_rule_moments(rule.id, uid, []) assert rule.id not in await rulebooks_svc.list_rule_moments([rule.id]) async def test_a_refused_name_stores_nothing(world): uid, rule = world["uid"], world["rule"] await rulebooks_svc.set_rule_moments(rule.id, uid, ["work.finish"]) with pytest.raises(ValueError, match="unknown moment"): await rulebooks_svc.set_rule_moments(rule.id, uid, ["work.deliver", "work.shipped"]) assert (await rulebooks_svc.list_rule_moments([rule.id]))[rule.id] == ["work.finish"] async def test_only_the_owner_can_mount(world): rule = world["rule"] assert await rulebooks_svc.set_rule_moments(rule.id, world["sid"], ["work.finish"]) is None assert rule.id not in await rulebooks_svc.list_rule_moments([rule.id]) async def test_the_detail_carries_the_mounts_and_omits_an_empty_set(world): uid, rule = world["uid"], world["rule"] bare = await rulebooks_svc.rule_detail(uid, rule) assert "moments" not in bare mounted = await rulebooks_svc.rule_detail(uid, rule, moments=["work.finish"]) assert mounted["moments"] == ["work.finish"] # None leaves them alone: a later read without the argument still has them. assert (await rulebooks_svc.rule_detail(uid, rule))["moments"] == ["work.finish"] async def test_a_backup_carries_the_mounts_to_the_restored_rule(world): uid, rule = world["uid"], world["rule"] await rulebooks_svc.set_rule_moments(rule.id, uid, ["work.finish", "reply.report"]) async with async_session() as s: payload = { "version": backup.BACKUP_VERSION, "users": backup._user_rows( [(await s.execute(select(User).where(User.id == uid))).scalars().one()] ), "rulebooks": backup._rulebook_rows( [(await s.execute(select(Rulebook).where( Rulebook.id == world["book_id"]))).scalars().one()] ), "rulebook_topics": backup._topic_rows((await s.execute( select(RulebookTopic).where(RulebookTopic.rulebook_id == world["book_id"]) )).scalars().all()), "rules": backup._rule_rows( [(await s.execute(select(Rule).where(Rule.id == rule.id))).scalars().one()] ), } exported = await backup.export_user_backup(uid) payload["rule_moments"] = [ row for row in exported["rule_moments"] if row["rule_id"] == rule.id ] assert sorted(r["moment"] for r in payload["rule_moments"]) == [ "reply.report", "work.finish", ] payload["users"][0]["username"] = RESTORED_USERNAME stats = await backup.restore_full_backup(payload) assert stats["rule_moments"] == 2 async with async_session() as s: restored_user = (await s.execute( select(User).where(User.username == RESTORED_USERNAME) )).scalars().one() restored_rule = (await s.execute( select(Rule).join(RulebookTopic, RulebookTopic.id == Rule.topic_id) .join(Rulebook, Rulebook.id == RulebookTopic.rulebook_id) .where(Rulebook.owner_user_id == restored_user.id) )).scalars().one() assert restored_rule.id != rule.id assert (await rulebooks_svc.list_rule_moments([restored_rule.id]))[restored_rule.id] == [ "work.finish", "reply.report", ]