CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
Step 2 of the Scribe -> Grimoire rename. - src/scribe -> src/grimoire, and every import, patch target, module path, hypercorn entry point and `python -m grimoire.restore` with it; pyproject, uv.lock and the frontend package are named grimoire. - Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service runs as grimoire / grimoire_test. - Compose files default the DB user, database and (prod) volume to grimoire, each overridable from .env, so an install created as Scribe keeps its data by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME. - Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar. Archives named scribe-* with the scribe-backup-tar manifest still restore and are still counted at the destination (folder and S3), with tests. - Exports, the MCP auth realm, the ASGI scope key and the persistent logger carry the new name. Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1", because every stored secret was derived under it. Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is step 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
4.9 KiB
Python
120 lines
4.9 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
|
|
from tests.helpers import FakeMCP
|
|
|
|
|
|
def test_design_systems_blueprint_registered():
|
|
from grimoire.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 grimoire.app import create_app
|
|
app = create_app()
|
|
assert "design_systems" in app.blueprints
|
|
|
|
|
|
def test_route_handlers_callable():
|
|
from grimoire.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",
|
|
"get_design_system_stylesheet",
|
|
"check_snippets_against_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 grimoire.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",
|
|
# Static segment, declared before the <int:...> rule reads it — Quart's
|
|
# int converter will not match "starter-roles", so the two cannot
|
|
# collide. Worth stating: a static-vs-dynamic sibling on the same prefix
|
|
# is exactly where a silently-shadowed route hides.
|
|
"/api/design-systems/starter-roles",
|
|
"/api/design-systems/<int:design_system_id>",
|
|
"/api/design-systems/<int:design_system_id>/resolved",
|
|
"/api/design-systems/<int:design_system_id>/stylesheet",
|
|
"/api/design-systems/<int:design_system_id>/snippet-check",
|
|
"/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 grimoire.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",
|
|
"stylesheet_for_system", "check_snippets_against_system",
|
|
):
|
|
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.
|
|
|
|
Two surfaces drift apart without failing anything — one door with no
|
|
delete, the other with no near-duplicate gate — until someone goes
|
|
looking.
|
|
"""
|
|
from grimoire.mcp.tools import design_systems as tools
|
|
from grimoire.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",
|
|
"get_design_system_stylesheet", "list_starter_role_groups",
|
|
):
|
|
assert callable(getattr(tools, name)), f"MCP tool missing: {name}"
|
|
assert callable(getattr(routes, name)), f"REST route missing: {name}"
|
|
|
|
# The snippet check is the one verb whose handler names differ between the
|
|
# surfaces (the tool says what it checks AGAINST; 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.check_snippets_against_design_system)
|
|
assert callable(routes.check_snippets_against_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 grimoire.mcp.tools import design_systems as tools
|
|
|
|
mcp = FakeMCP()
|
|
|
|
tools.register(mcp)
|
|
|
|
public = {
|
|
name for name, obj in vars(tools).items()
|
|
if inspect.iscoroutinefunction(obj) and not name.startswith("_")
|
|
}
|
|
assert set(mcp.names) == public
|