"""Milestone 297 step 2 — always-on exclusions reach every rule surface. The SQL is the integration lane's; here the contracts: rules_payload carries the seventh key, list_always_on_rules takes project_id, the session-start block names the excluded rulebooks, and the MCP tools mount. """ from unittest.mock import AsyncMock, patch import pytest from scribe.services.rulebooks import rules_payload def test_rules_payload_carries_excluded_always_on_as_the_seventh_key(): out = rules_payload({ "rules": [], "truncated": False, "subscribed_rulebooks": [], "excluded_always_on": [{"id": 1, "title": "Family"}], }) assert set(out) == { "applicable_rules", "applicable_rules_truncated", "subscribed_rulebooks", "project_rules", "suppressed_rules", "suppressed_topics", "excluded_always_on", } assert out["excluded_always_on"] == [{"id": 1, "title": "Family"}] # An older applicable dict without the key still renders (empty list). assert rules_payload({"rules": [], "truncated": False, "subscribed_rulebooks": []})["excluded_always_on"] == [] def test_list_always_on_rules_service_and_tool_take_a_project_id(): import inspect from scribe.mcp.tools import rulebooks as tools from scribe.services import rulebooks as svc assert "project_id" in inspect.signature(svc.list_always_on_rules).parameters assert "project_id" in inspect.signature(tools.list_always_on_rules).parameters @pytest.mark.asyncio async def test_session_context_names_the_excluded_always_on_rulebooks(): from types import SimpleNamespace as NS from scribe.services.plugin_context import build_session_context rules = [NS(id=1, title="`dev` is home", topic_id=1, statement="x")] project = NS(id=9, title="Widget", goal="", design_system_id=None) with patch("scribe.services.plugin_context.rulebooks_svc.list_always_on_rules", AsyncMock(return_value=rules)) as lao, \ patch("scribe.services.plugin_context.rulebooks_svc.excluded_always_on_rulebooks", AsyncMock(return_value=[{"id": 5, "title": "Design standards"}])), \ patch("scribe.services.plugin_context._topic_titles", AsyncMock(return_value={1: "git"})), \ patch("scribe.services.plugin_context.projects_svc.get_project", AsyncMock(return_value=project)), \ patch("scribe.services.plugin_context.notes_svc.list_notes", AsyncMock(return_value=([], 0))), \ patch("scribe.services.plugin_context.rulebooks_svc.get_applicable_rules", AsyncMock(return_value={"rules": [], "truncated": False, "subscribed_rulebooks": [], "project_rules": [], "suppressed_rules": [], "suppressed_topics": [], "excluded_always_on": []})): out = await build_session_context(user_id=7, project_id=9) # The always-on set was asked FOR THIS PROJECT, and the departure is named. assert lao.await_args.kwargs.get("project_id") == 9 assert "Excluded for this project by its inception decision" in out["context"] assert "Design standards (#5)" in out["context"]