CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 18s
The shape ledger showed the same test scaffolding defined over and over: _bind_user x12 (byte-identical), _dispose_engine x10 in three wordings, _no_supersession x3, _make_mock_session x7 in three subsets, a get-or-create User helper x2 (+3 inlined), and fifteen hand-rolled MagicMock note factories each re-explaining the same "an auto-MagicMock attribute is truthy" hazard (note 2109). Now: conftest.py carries _bind_user / _dispose_engine / _no_supersession as opt-in fixtures (pytestmark = usefixtures(...) per module, so unit tests pay nothing), and tests/helpers.py carries make_mock_session(), ensure_user() and fake_note(**attrs) — the hazard documented once, real values on every attribute the product reads. Call sites were rewritten by AST so titles with dashes and commas survived; the three SimpleNamespace _note stand-ins that only feed a single function stay local. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
208 lines
9.2 KiB
Python
208 lines
9.2 KiB
Python
"""MCP design-system tools — the sentinel translations, mostly.
|
|
|
|
The tools are thin wrappers, so the only logic worth testing is where the MCP
|
|
calling convention meets the service's: an agent cannot omit an argument, so
|
|
"leave unchanged", "clear" and "set" have to be encoded in the value. Getting
|
|
that mapping wrong is silent — the call succeeds and changes the wrong thing.
|
|
"""
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from scribe.services.design_systems import DesignSystemCycle
|
|
|
|
|
|
pytestmark = pytest.mark.usefixtures("_bind_user")
|
|
|
|
|
|
def _fake_system():
|
|
s = MagicMock()
|
|
s.to_dict.return_value = {"id": 1, "title": "FabledSword", "parent_id": None}
|
|
return s
|
|
|
|
|
|
def _fake_token():
|
|
t = MagicMock()
|
|
t.to_dict.return_value = {"id": 9, "name": "--fs-obsidian"}
|
|
return t
|
|
|
|
|
|
# --- create -----------------------------------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creating_without_a_parent_passes_none_not_zero():
|
|
"""0 is the "omitted" sentinel, and it must not reach the service as a
|
|
system id — there is no system 0, so the create would fail an ACL check for
|
|
a record that cannot exist."""
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.create_design_system = AsyncMock(return_value=_fake_system())
|
|
from scribe.mcp.tools.design_systems import create_design_system
|
|
await create_design_system(title="FabledSword")
|
|
assert svc.create_design_system.await_args.kwargs["parent_id"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creating_with_a_parent_passes_it_through():
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.create_design_system = AsyncMock(return_value=_fake_system())
|
|
from scribe.mcp.tools.design_systems import create_design_system
|
|
await create_design_system(title="Scribe", parent_id=4)
|
|
assert svc.create_design_system.await_args.kwargs["parent_id"] == 4
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_raises_when_the_parent_is_not_writable():
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.create_design_system = AsyncMock(return_value=None)
|
|
from scribe.mcp.tools.design_systems import create_design_system
|
|
with pytest.raises(ValueError):
|
|
await create_design_system(title="Scribe", parent_id=4)
|
|
|
|
|
|
# --- the three-state parent -------------------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_with_parent_id_zero_leaves_the_parent_alone():
|
|
"""The common case — renaming a system must not silently re-root it."""
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_design_system = AsyncMock(return_value=_fake_system())
|
|
from scribe.mcp.tools.design_systems import update_design_system
|
|
await update_design_system(design_system_id=1, title="Renamed")
|
|
fields = svc.update_design_system.await_args.kwargs
|
|
assert "parent_id" not in fields
|
|
assert fields["title"] == "Renamed"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_with_parent_id_minus_one_clears_it():
|
|
"""-1 means "make this a family system". It has to arrive at the service as
|
|
None, which is the value the service reads as "become a root" — where
|
|
omitting the key means "leave alone"."""
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_design_system = AsyncMock(return_value=_fake_system())
|
|
from scribe.mcp.tools.design_systems import update_design_system
|
|
await update_design_system(design_system_id=1, parent_id=-1)
|
|
assert svc.update_design_system.await_args.kwargs["parent_id"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_with_a_positive_parent_id_sets_it():
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_design_system = AsyncMock(return_value=_fake_system())
|
|
from scribe.mcp.tools.design_systems import update_design_system
|
|
await update_design_system(design_system_id=1, parent_id=4)
|
|
assert svc.update_design_system.await_args.kwargs["parent_id"] == 4
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_cycle_surfaces_as_a_usable_error_not_a_not_found():
|
|
"""The service raises so this layer can keep the two apart. An agent told
|
|
"not found" would retry the same call; one told what the loop is can fix it."""
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_design_system = AsyncMock(
|
|
side_effect=DesignSystemCycle("2 already inherits from 1")
|
|
)
|
|
from scribe.mcp.tools.design_systems import update_design_system
|
|
with pytest.raises(ValueError, match="already inherits"):
|
|
await update_design_system(design_system_id=1, parent_id=2)
|
|
|
|
|
|
# --- tokens -----------------------------------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_token_treats_order_index_minus_one_as_unchanged():
|
|
"""0 is a VALID order_index, so it cannot double as the omitted sentinel —
|
|
the same reason the systems tools use -1."""
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_token = AsyncMock(return_value=_fake_token())
|
|
from scribe.mcp.tools.design_systems import update_design_token
|
|
await update_design_token(token_id=9, purpose="page bg")
|
|
fields = svc.update_token.await_args.kwargs
|
|
assert "order_index" not in fields
|
|
assert fields["purpose"] == "page bg"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_token_accepts_order_index_zero():
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_token = AsyncMock(return_value=_fake_token())
|
|
from scribe.mcp.tools.design_systems import update_design_token
|
|
await update_design_token(token_id=9, order_index=0)
|
|
assert svc.update_token.await_args.kwargs["order_index"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_token_can_set_an_empty_value_map():
|
|
"""`value_by_mode={}` is meaningful — it strips every mode from a token. The
|
|
guard is `is not None`, not truthiness, or that edit would be unreachable."""
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_token = AsyncMock(return_value=_fake_token())
|
|
from scribe.mcp.tools.design_systems import update_design_token
|
|
await update_design_token(token_id=9, value_by_mode={})
|
|
assert svc.update_token.await_args.kwargs["value_by_mode"] == {}
|
|
|
|
|
|
# --- the project pointer ----------------------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clearing_a_projects_design_system_passes_none():
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.set_project_design_system = AsyncMock(return_value=True)
|
|
from scribe.mcp.tools.design_systems import set_project_design_system
|
|
result = await set_project_design_system(project_id=2, design_system_id=-1)
|
|
assert svc.set_project_design_system.await_args.args[2] is None
|
|
assert result["design_system_id"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_returns_serialised_tokens_with_their_provenance():
|
|
"""The payload has to carry the shadowed entries, not just the winner —
|
|
dropping them at the serialisation boundary would discard the one thing
|
|
resolution was built to preserve."""
|
|
from scribe.services.design_cascade import resolve_tokens
|
|
|
|
class _T:
|
|
def __init__(self, name, value_by_mode):
|
|
self.name, self.value_by_mode = name, value_by_mode
|
|
self.group_name = self.purpose = None
|
|
self.order_index = 0
|
|
|
|
resolved = resolve_tokens(
|
|
2, {1: None, 2: 1},
|
|
{1: [_T("--fs-accent", {"base": "#6b2118"})],
|
|
2: [_T("--fs-accent", {"base": "#5b4a8a"})]},
|
|
)
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.resolve_design_system = AsyncMock(return_value=resolved)
|
|
from scribe.mcp.tools.design_systems import resolve_design_system
|
|
payload = await resolve_design_system(design_system_id=2)
|
|
|
|
token = payload["tokens"][0]
|
|
assert token["value_by_mode"] == {"base": "#5b4a8a"}
|
|
assert token["origin_by_mode"] == {"base": 2}
|
|
assert token["contributions"]["base"] == [
|
|
{"system_id": 2, "value": "#5b4a8a"},
|
|
{"system_id": 1, "value": "#6b2118"},
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_token_can_clear_supersedes_with_an_empty_list():
|
|
"""`[]` means "this token replaces nothing after all" — a real edit. Guarded
|
|
on `is not None` so it isn't swallowed as "unchanged", the same trap #2077
|
|
recorded for update_snippet."""
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_token = AsyncMock(return_value=_fake_token())
|
|
from scribe.mcp.tools.design_systems import update_design_token
|
|
await update_design_token(token_id=9, supersedes=[])
|
|
assert svc.update_token.await_args.kwargs["supersedes"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_token_leaves_supersedes_alone_when_omitted():
|
|
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
|
svc.update_token = AsyncMock(return_value=_fake_token())
|
|
from scribe.mcp.tools.design_systems import update_design_token
|
|
await update_design_token(token_id=9, purpose="text on action surfaces")
|
|
assert "supersedes" not in svc.update_token.await_args.kwargs
|