"""The moment catalog — the vocabulary rules mount on (milestone 458 step 1). What this pins is what a mount depends on: every name is well-formed and resolvable, an unknown name is refused rather than stored, the catalog speaks for any kind of work rather than only software, and both doors — the MCP tool and the REST read — hand out the same catalog. """ import re import pytest from scribe.services import moments from tests.helpers import FakeMCP _NAME = re.compile(r"^[a-z]+\.[a-z]+$") def test_the_catalog_is_not_empty(): """The sweeps below are vacuous over an empty catalog (rule 167).""" assert len(moments.MOMENTS) >= 10 def test_every_moment_is_keyed_by_its_own_well_formed_name(): for key, m in moments.MOMENTS.items(): assert key == m.name, f"{key!r} is filed under a name it does not carry" assert _NAME.match(key), f"{key!r} is not ." def test_every_moment_says_what_it_means_and_how_it_is_reached(): for m in moments.MOMENTS.values(): assert m.means.strip() and m.reached_by.strip(), m.name assert "\n" not in m.means, f"{m.name}: `means` is one line" def test_the_skill_family_is_not_a_catalog_entry(): """A family is a shape, not a moment — no rule mounts on `skill.` literally, and a catalog key starting with the prefix would shadow it.""" assert not any(k.startswith(moments.SKILL_PREFIX) for k in moments.MOMENTS) # Software-only vocabulary, the same guard the completion query carries. The # moments are named for any work a person drives through an agent; the actions # particular to one kind of work belong in the mappings, not in the meaning. _DEV_ONLY = (r"\bCI\b", r"\bcommit", r"\bpull request", r"\bcode\b", r"\btest", r"\bgit\b", r"\brepo(s|sitor\w*)?\b", r"\bbranch", r"\bcompil", r"\bbuild\b") @pytest.mark.parametrize("field", ["means", "reached_by"]) def test_the_catalog_assumes_no_particular_domain(field): found = { m.name: hits for m in [*moments.MOMENTS.values(), moments.SKILL_FAMILY] if (hits := [w for w in _DEV_ONLY if re.search(w, getattr(m, field), re.IGNORECASE)]) } assert not found, f"software-only vocabulary in `{field}`: {found}" def test_the_guard_can_fail(): assert re.search(_DEV_ONLY[1], "after the commit lands", re.IGNORECASE) @pytest.mark.parametrize("name", list(moments.MOMENTS)) def test_every_catalog_moment_is_mountable(name): assert moments.require_moment(name) == name @pytest.mark.parametrize("raw,clean", [ (" Work.Finish ", "work.finish"), ("skill.verification", "skill.verification"), ("skill.scribe:writing-plans", "skill.scribe:writing-plans"), ("skill.scribe-proc-release_notes", "skill.scribe-proc-release_notes"), ]) def test_a_name_is_normalised_before_it_is_judged(raw, clean): assert moments.require_moment(raw) == clean @pytest.mark.parametrize("bad", [ "work.finished", "finish", "", "skill.", "skill.two words", "work.", ]) def test_an_unknown_moment_is_refused_with_the_catalog(bad): """A typo stored as a mount never fires — the silent failure moments end.""" with pytest.raises(ValueError) as exc: moments.require_moment(bad) assert "work.finish" in str(exc.value) assert moments.SKILL_FAMILY.name in str(exc.value) def test_the_catalog_payload_carries_every_moment_and_the_family(): data = moments.catalog() assert [m["name"] for m in data["moments"]] == list(moments.MOMENTS) assert data["total"] == len(moments.MOMENTS) assert data["families"][0]["prefix"] == moments.SKILL_PREFIX async def test_the_tool_returns_the_service_catalog(): from scribe.mcp.tools import moments as tool assert await tool.list_moments() == moments.catalog() def test_the_tool_is_registered_and_read_only(): from scribe.mcp.server import _READ_ONLY_TOOLS from scribe.mcp.tools import moments as tool mcp = FakeMCP() tool.register(mcp) assert mcp.names == ["list_moments"] assert "list_moments" in _READ_ONLY_TOOLS def test_both_doors_read_one_catalog(): """Rule 33 parity: the tool and the route call the same service, so the session and the Settings view cannot name different moments.""" from scribe.mcp.tools import moments as tool from scribe.routes import retrieval as routes assert tool.moments_svc is moments assert routes.moments_svc is moments