"""Every typed record kind gets the same doors (#4164). WHY THIS IS ONE GUARD AND NOT FIFTEEN Milestone 385 shipped `lesson` across four steps and it still arrived with three of five tools, no way to list it, and no duplicate report. Nothing was red, because every test asked "does what I built work" and none asked "is the kind finished". A per-tool test cannot catch a MISSING tool. So this asserts the property directly — a kind with its own `note_type` has a full create/read/update/delete plus a listing, each one registered and each one classified for auth — and derives the expectation from the kinds themselves. A fourth kind added later inherits the bar without anyone remembering to. #2250 is the same failure one kind earlier: processes could always be deleted through `delete_note`, but nothing said so, and "a kind whose own tools offer create/read/update reads as one you cannot retire". """ from __future__ import annotations import importlib import pytest from scribe.mcp.server import ( _DELIBERATELY_WRITE_SCOPED, _READ_ONLY_TOOLS, _WRITE_TOOLS, ) from scribe.services.dedup import _REPORT_KINDS # singular -> the tools module, which is also the plural used by the listing. KINDS = {"snippet": "snippets", "process": "processes", "lesson": "lessons"} def _module(plural: str): return importlib.import_module(f"scribe.mcp.tools.{plural}") def _expected_tools(singular: str, plural: str) -> list[str]: return [ f"list_{plural}", f"create_{singular}", f"get_{singular}", f"update_{singular}", f"delete_{singular}", ] @pytest.mark.parametrize("singular,plural", sorted(KINDS.items())) def test_a_kind_has_all_five_doors(singular, plural): """THE guard. A kind that can be created and not listed, or updated and not retired, is one whose gaps are invisible until someone needs the missing half.""" module = _module(plural) missing = [ name for name in _expected_tools(singular, plural) if not callable(getattr(module, name, None)) ] assert not missing, f"{plural}.py is missing {missing}" @pytest.mark.parametrize("singular,plural", sorted(KINDS.items())) def test_every_door_is_actually_registered(singular, plural): """Defining the function is not offering it. `register(mcp)` is what puts a tool on the surface, and a handler left out of that list is dead code that every other test still exercises directly.""" module = _module(plural) registered: list[str] = [] class _Recorder: def tool(self, name): registered.append(name) return lambda fn: fn module.register(_Recorder()) missing = [n for n in _expected_tools(singular, plural) if n not in registered] assert not missing, f"{plural}.register() does not offer {missing}" @pytest.mark.parametrize("singular,plural", sorted(KINDS.items())) def test_every_door_is_classified_for_auth(singular, plural): """`test_mcp_auth` requires every registered tool to sit in exactly one of the three sets. Asserted per KIND as well, because that test enumerates what is registered — so a whole module wired up with none of its tools classified is caught here by the kind that owns them.""" classified = _READ_ONLY_TOOLS | _WRITE_TOOLS | _DELIBERATELY_WRITE_SCOPED missing = [n for n in _expected_tools(singular, plural) if n not in classified] assert not missing, f"unclassified in server.py: {missing}" @pytest.mark.parametrize("singular", sorted(KINDS)) def test_every_kind_can_be_asked_whether_it_holds_duplicates(singular): """A kind with no report is one whose duplicates are found by accident. It matters most where the write-path gate is deliberately permissive: a lesson's bar sits above the band where two genuinely different lessons would block each other, and the report is where that tolerance is meant to be reviewed. """ assert singular in _REPORT_KINDS def test_the_surface_guards_can_fail(): """Rule 167: falsify the shape these assert against, so a guard that has quietly stopped describing anything cannot pass by describing nothing.""" assert _expected_tools("lesson", "lessons") == [ "list_lessons", "create_lesson", "get_lesson", "update_lesson", "delete_lesson", ] # A kind that does not exist has no module — the lookup is real, not a # getattr that returns None for everything. with pytest.raises(ModuleNotFoundError): _module("widgets")