feat(rules): project rule + topic suppressions
Lets a project mute individual rules or whole topics from rulebooks it subscribes to, without unsubscribing the rulebook. Two new association tables (migration 0060), 4 MCP tools (suppress/unsuppress × rule/topic), 4 REST endpoints, and an inline "× skip" affordance plus collapsed "Suppressed (N)" section in the project's Rules tab. get_applicable_rules now emits suppressed_rules and suppressed_topics (detail objects with rulebook/topic context, not just IDs) so the UI can render the suppressed list without a follow-up lookup. The main rules projection grew topic_id and rulebook_id columns for the per-row suppress affordance. Project deletion cascades the suppression rows via hard DELETE — they are pure associations with no soft-delete column, and restoring a deleted project should start fresh, not inherit stale mutes. Project-scoped rules (Rule.project_id) are deliberately not suppressible — delete them with delete_rule instead. Implements plan-task #187. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -48,18 +48,24 @@ async def test_delete_returns_none_when_not_found():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_project_cascades_to_notes_milestones_and_project_rules():
|
||||
async def test_delete_project_cascades_to_notes_milestones_project_rules_and_suppressions():
|
||||
session = _make_mock_session()
|
||||
# exists-check + 4 cascade updates (notes, milestones, project-scoped rules, project)
|
||||
# exists-check + 6 cascade ops:
|
||||
# notes (soft) → milestones (soft) → project-scoped rules (soft) →
|
||||
# project_rule_suppressions (hard DELETE) → project_topic_suppressions (hard DELETE) →
|
||||
# project (soft)
|
||||
session.execute = AsyncMock(side_effect=[
|
||||
_exists_result(True), MagicMock(), MagicMock(), MagicMock(), MagicMock(),
|
||||
_exists_result(True),
|
||||
MagicMock(), MagicMock(), MagicMock(),
|
||||
MagicMock(), MagicMock(),
|
||||
MagicMock(),
|
||||
])
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import delete
|
||||
batch = await delete(user_id=1, entity_type="project", entity_id=3)
|
||||
assert isinstance(batch, str)
|
||||
assert session.execute.await_count == 5
|
||||
assert session.execute.await_count == 7
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user