feat(rulebook): service layer — subscriptions + get_applicable_rules

This commit is contained in:
2026-05-27 21:19:14 -04:00
parent d3833ba5a4
commit 45fe198d54
2 changed files with 179 additions and 0 deletions
+66
View File
@@ -221,3 +221,69 @@ async def test_get_rule_returns_none_when_not_owner():
from fabledassistant.services.rulebooks import get_rule
result = await get_rule(rule_id=1, user_id=99)
assert result is None
# ── Subscriptions + applicable_rules ────────────────────────────────────
@pytest.mark.asyncio
async def test_subscribe_project_requires_owned_rulebook():
"""subscribe_project raises if user doesn't own the rulebook."""
mock_session = _make_mock_session()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_session.execute = AsyncMock(return_value=mock_result)
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
mock_cls.return_value = mock_session
from fabledassistant.services.rulebooks import subscribe_project
with pytest.raises(ValueError, match="not found"):
await subscribe_project(
project_id=1, rulebook_id=999, user_id=7,
)
@pytest.mark.asyncio
async def test_get_applicable_rules_returns_shape():
"""get_applicable_rules returns {rules, truncated, subscribed_rulebooks}."""
mock_session = _make_mock_session()
sub_result = MagicMock()
sub_result.all.return_value = [(1, "FabledSword family")]
rules_result = MagicMock()
rules_result.all.return_value = [
(i, f"Rule {i}", f"Statement {i}", "git-workflow", "FabledSword family")
for i in range(50)
]
mock_session.execute = AsyncMock(side_effect=[sub_result, rules_result])
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
mock_cls.return_value = mock_session
from fabledassistant.services.rulebooks import get_applicable_rules
result = await get_applicable_rules(project_id=3, user_id=7, limit=50)
assert "rules" in result
assert "truncated" in result
assert "subscribed_rulebooks" in result
assert result["subscribed_rulebooks"] == [{"id": 1, "title": "FabledSword family"}]
assert len(result["rules"]) == 50
assert result["truncated"] is False # exactly 50, not over
@pytest.mark.asyncio
async def test_get_applicable_rules_truncates_when_over_limit():
"""When limit+1 rows are returned, truncated=True and only `limit` returned."""
mock_session = _make_mock_session()
sub_result = MagicMock()
sub_result.all.return_value = []
rules_result = MagicMock()
# 51 rows means truncation detected
rules_result.all.return_value = [
(i, f"r{i}", "stmt", "topic", "rb") for i in range(51)
]
mock_session.execute = AsyncMock(side_effect=[sub_result, rules_result])
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
mock_cls.return_value = mock_session
from fabledassistant.services.rulebooks import get_applicable_rules
result = await get_applicable_rules(project_id=3, user_id=7, limit=50)
assert result["truncated"] is True
assert len(result["rules"]) == 50