Files
FabledScribe/tests/test_routes_design_systems.py
T
bvandeusen 4dc57f8ab2
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 21s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Successful in 38s
feat(design-systems): import a design system out of a rulebook's prose
Milestone #254 step 3 (#2288). Reuses #251's prose extractor as the reader and
adds the part that makes it an import rather than a list of claims.

**The join is the whole trick.** A rulebook states a design system in two places
and neither half is a token: one rule names the colours ("Obsidian #14171A (page
bg, deepest surface)"), another names the custom properties
(`--fs-obsidian/iron/slate`). The import pairs them on the word — `--fs-obsidian`
ends with `obsidian` — which is the only reason it produces something usable
instead of seventy empty names. The parenthetical becomes the token's purpose,
which is the field a bare hex could never carry.

**Prohibitions arrive as replacements, per the operator's reframe.** Rule 52
declares Parchment and forbids pure white in one breath, so the import emits
"write --fs-parchment instead of #ffffff" — the same fact stated forwards. It
attaches to the FIRST token that rule supplied a value for, not to every token
of that rule, because claiming Vellum is also the replacement for white would be
putting words in the rulebook's mouth.

**A token the rulebook names but states no readable value for is still
proposed, with an empty value.** Radius steps and type sizes are prose ("Small
4px") and nothing here parses them; inventing a parse per shape would be
guessing. The name is real and the value needs a human, so the proposal says
exactly that — and the UI leads with the COUNT of those, because an import that
hid them would look more complete than it is.

Preview is the default on both surfaces and in the UI. An import is a proposal:
rulebooks are written aspirationally and some of what they describe was never
built, so every entry carries the rule id and the sentence it came from and a
reviewer can check the claim rather than trust it.

Existing token names are never overwritten. A value already in the record was
put there deliberately — most likely correcting this importer — so a re-run
fills gaps and lists the rest as skipped, which also makes it safe to repeat.

Colours the rulebook names but never exposes as a custom property produce no
token: it never asked for one, and inventing a name would put something in the
record no rule sanctions.
2026-07-30 21:23:32 -04:00

114 lines
4.5 KiB
Python

"""Structural tests for the design-systems blueprint + the parity guard.
Modelled on tests/test_routes_snippets.py. The enumerations below are
deliberate: relaxing one to a pattern would stop it catching the thing it was
written for, which is a capability landing on one surface and not the other.
"""
import inspect
def test_design_systems_blueprint_registered():
from scribe.routes.design_systems import design_systems_bp
assert design_systems_bp.name == "design_systems"
assert design_systems_bp.url_prefix == "/api"
def test_design_systems_blueprint_registered_in_app():
from scribe.app import create_app
app = create_app()
assert "design_systems" in app.blueprints
def test_route_handlers_callable():
from scribe.routes import design_systems as routes
for name in (
"list_design_systems", "create_design_system", "get_design_system",
"update_design_system", "delete_design_system", "resolve_design_system",
"import_design_system", "list_design_tokens", "create_design_token",
"update_design_token", "delete_design_token", "set_project_design_system",
):
assert callable(getattr(routes, name))
def test_every_endpoint_is_reachable_on_the_app():
"""The handlers existing is not the same as them being routed. This catches
a decorator that was copied without its path changing — two handlers on one
rule, where the second silently never runs."""
from scribe.app import create_app
app = create_app()
rules = {
str(r.rule) for r in app.url_map.iter_rules()
if r.endpoint.startswith("design_systems.")
}
assert rules == {
"/api/design-systems",
"/api/design-systems/<int:design_system_id>",
"/api/design-systems/<int:design_system_id>/resolved",
"/api/design-systems/<int:design_system_id>/import",
"/api/design-systems/<int:design_system_id>/tokens",
"/api/design-tokens/<int:token_id>",
"/api/projects/<int:project_id>/design-system",
}
def test_service_functions_take_user_id():
"""Routes must call the services with user_id — verify the contract (#33)."""
from scribe.services import design_systems as svc
for fn_name in (
"create_design_system", "list_design_systems", "get_design_system",
"update_design_system", "delete_design_system", "resolve_design_system",
"create_token", "list_tokens", "update_token", "delete_token",
"set_project_design_system", "import_from_rulebook",
):
fn = getattr(svc, fn_name)
assert callable(fn)
assert "user_id" in inspect.signature(fn).parameters
def test_agent_and_web_surfaces_stay_at_parity():
"""The MCP tools and the REST routes are two callers of one service; a
capability on one has to exist on the other (rule #33).
This guard exists because the snippet surfaces drifted apart once — MCP had
no delete, the web side had no near-duplicate gate — and neither failed
anything until someone went looking.
"""
from scribe.mcp.tools import design_systems as tools
from scribe.routes import design_systems as routes
for name in (
"create_design_system", "list_design_systems", "get_design_system",
"resolve_design_system", "update_design_system", "delete_design_system",
"create_design_token", "list_design_tokens", "update_design_token",
"delete_design_token", "set_project_design_system",
):
assert callable(getattr(tools, name)), f"MCP tool missing: {name}"
assert callable(getattr(routes, name)), f"REST route missing: {name}"
# Import is the one verb whose handler names differ between the surfaces
# (the tool says what it reads FROM; the route is already under the system),
# so the loop above can't pair it. It still has to exist on both.
assert callable(tools.import_design_system_from_rulebook)
assert callable(routes.import_design_system)
def test_every_mcp_tool_in_the_module_is_registered():
"""A tool written but never registered is invisible to an agent, and nothing
else in the codebase would notice."""
from scribe.mcp.tools import design_systems as tools
registered = []
class _Recorder:
def tool(self, name):
registered.append(name)
return lambda fn: fn
tools.register(_Recorder())
public = {
name for name, obj in vars(tools).items()
if inspect.iscoroutinefunction(obj) and not name.startswith("_")
}
assert set(registered) == public