Files
FabledGrimoire/tests/test_integration_rule_move.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

134 lines
5.9 KiB
Python

"""Real-Postgres tests for moving a rule between homes.
A rule's home is its reach: a rulebook topic makes it global, a project makes
it that project's. The alternative to a move — recreate the rule in the other
home and trash the original — loses the id every record cites, the edit
history, the area tags and the typed edges. These pin that a move keeps all
four, and that the refusals happen before anything is written: the topic/
project CHECK (migration 0059) and the per-topic title index would otherwise
fail the commit with a raw database error.
"""
import uuid
from unittest.mock import MagicMock, patch
import pytest
import pytest_asyncio
from scribe.models import async_session
from scribe.models.project import Project
from scribe.models.rulebook import Rule
from scribe.services import canonical_systems as canonical_svc
from scribe.services import rule_versions as rv_svc
from scribe.services import rulebooks as rulebooks_svc
from tests.helpers import ensure_user, rule_row
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
@pytest.fixture(autouse=True)
def _no_reindex():
"""Rule writes detach an embedding refresh that outlives the test's loop
and races the next fixture; nothing here is about recall."""
with patch("scribe.services.rulebooks._refresh_rule_embedding", MagicMock()):
yield
@pytest_asyncio.fixture
async def homes():
"""A project rule with a version, an area tag and an incoming edge, plus a
topic to move it into and a second project."""
tag = uuid.uuid4().hex[:8]
async with async_session() as s:
owner = await ensure_user(s, f"rule_move_owner_{tag}")
stranger = await ensure_user(s, f"rule_move_stranger_{tag}")
home = Project(user_id=owner.id, title="Where it started")
other = Project(user_id=owner.id, title="Somewhere else")
theirs = Project(user_id=stranger.id, title="Not yours")
s.add_all([home, other, theirs])
await s.flush()
ids = {"owner": owner.id, "stranger": stranger.id, "home": home.id,
"other": other.id, "theirs": theirs.id}
await s.commit()
owner = ids["owner"]
book = await rulebooks_svc.create_rulebook(owner, "House style")
topic = await rulebooks_svc.create_topic(book.id, owner, "transport")
rule = await rulebooks_svc.create_project_rule(
ids["home"], owner, "Plain HTTP only", "No app-level TLS.",
when_to_apply="setting a cookie flag or a URL scheme",
)
await rulebooks_svc.update_rule(rule.id, owner, statement="No app-level TLS, ever.")
area = await canonical_svc.find_by_name("CI & Release")
await rulebooks_svc.set_rule_systems(rule.id, owner, [area.id])
downstream = await rulebooks_svc.create_project_rule(
ids["home"], owner, "No Secure-Context APIs", "Browsers withhold them.",
when_to_apply="reaching for the clipboard API",
)
await rulebooks_svc.add_rule_relation(owner, downstream.id, rule.id, "elaborates")
ids.update(topic=topic.id, rule=rule.id, area=area.id)
return ids
async def test_a_project_rule_becomes_global_and_keeps_everything(homes):
owner, rule_id = homes["owner"], homes["rule"]
moved = await rulebooks_svc.move_rule(rule_id, owner, topic_id=homes["topic"])
assert moved.id == rule_id
row = await rule_row(rule_id)
assert (row.topic_id, row.project_id) == (homes["topic"], None)
assert len(await rv_svc.list_versions(rule_id)) == 1, "the move must not drop history"
areas = await rulebooks_svc.list_rule_systems([rule_id])
assert [a["id"] for a in areas[rule_id]] == [homes["area"]]
edges = await rulebooks_svc.list_rule_relations([rule_id])
assert [e["kind"] for e in edges[rule_id]] == ["elaborates"]
async def test_a_move_writes_no_version(homes):
"""A version records what a rule SAID. A move
changes where it binds, not a word of it."""
before = len(await rv_svc.list_versions(homes["rule"]))
await rulebooks_svc.move_rule(homes["rule"], homes["owner"], topic_id=homes["topic"])
assert len(await rv_svc.list_versions(homes["rule"])) == before
async def test_a_global_rule_can_move_onto_a_project(homes):
owner, rule_id = homes["owner"], homes["rule"]
await rulebooks_svc.move_rule(rule_id, owner, topic_id=homes["topic"])
await rulebooks_svc.move_rule(rule_id, owner, project_id=homes["other"])
row = await rule_row(rule_id)
assert (row.topic_id, row.project_id) == (None, homes["other"])
async def test_refusals_happen_before_anything_is_written(homes):
owner, rule_id = homes["owner"], homes["rule"]
with pytest.raises(ValueError, match="exactly one"):
await rulebooks_svc.move_rule(rule_id, owner)
with pytest.raises(ValueError, match="exactly one"):
await rulebooks_svc.move_rule(rule_id, owner, topic_id=homes["topic"],
project_id=homes["other"])
with pytest.raises(ValueError, match="already on project"):
await rulebooks_svc.move_rule(rule_id, owner, project_id=homes["home"])
with pytest.raises(ValueError, match="not found"):
await rulebooks_svc.move_rule(rule_id, owner, project_id=homes["theirs"])
# A topic already holding a live rule with this title: named, not a raw
# IntegrityError from uq_rule_per_topic at commit.
clash = await rulebooks_svc.create_rule(
homes["topic"], owner, "Plain HTTP only", "Already here.",
when_to_apply="setting a cookie flag",
)
with pytest.raises(ValueError, match=f"rule {clash.id}"):
await rulebooks_svc.move_rule(rule_id, owner, topic_id=homes["topic"])
row = await rule_row(rule_id)
assert (row.topic_id, row.project_id) == (None, homes["home"])
async def test_someone_elses_rule_is_not_found(homes):
"""None, like every other rule read the caller cannot see — not an error
that confirms the rule exists."""
assert await rulebooks_svc.move_rule(
homes["rule"], homes["stranger"], project_id=homes["theirs"],
) is None