Files
FabledGrimoire/tests/test_design_starter_roles.py
bvandeusenandClaude Opus 5.5 0f5ef0be63
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
rename(547): the server is grimoire — package, image, DB defaults, backups
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>
2026-10-22 10:50:45 -04:00

136 lines
5.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The starter role set.
The premise: a literal gets written when there is no role to reach for. A
house style with no "text on a filled colour" role collects `color: #fff` at
every call site that needs one. Naming the roles at creation removes the
occasion.
The tests that matter here are about the BOUNDARY, not the content. Roles ship
with the product; values never do. A default palette would be one operator's
taste shipped as product code, and it would be very easy to add by
accident while "being helpful".
"""
import re
import pytest
from grimoire.services import design_starter_roles as roles
def test_no_role_carries_a_VALUE():
"""THE rule-115 guard. Every seeded token is a named question with no
answer. The moment one ships a hex, the product is prescribing an install's
palette."""
for row in roles.starter_tokens():
assert row["value_by_mode"] == {}, f"{row['name']} shipped a value"
def _colour_literals(text: str) -> list[str]:
"""Hex colours in `text`, NOT counting issue references.
`#2275` is four hex-valid digits and also how an issue is cited — so the
naive pattern flags documentation. A real colour either contains a letter
a–f or is a full 6/8-digit value; an all-decimal 3- or 4-digit match is an
issue number.
"""
out = []
for m in re.findall(r"#[0-9a-fA-F]{3,8}\b", text):
digits = m[1:]
if len(digits) not in (3, 4, 6, 8):
continue
if len(digits) in (6, 8) or any(c in "abcdefABCDEF" for c in digits):
out.append(m)
return out
def test_the_module_contains_no_colour_literals_at_all():
"""Belt and braces on the above, and the stronger claim: not just that
tokens are blank, but that no palette hides in a comment or a docstring
waiting to be pasted in. Checks the SOURCE, not the output."""
import pathlib
src = pathlib.Path(roles.__file__).read_text()
hexes = _colour_literals(src)
assert not hexes, f"colour literals in product code: {hexes}"
def test_the_colour_check_does_not_flag_issue_references():
"""Pins the exclusion above, because without it this file fails on its own
citations and the obvious 'fix' is to delete the check."""
assert _colour_literals("see #2275 and #2349") == []
assert _colour_literals("color: #fff") == ["#fff"]
assert _colour_literals("#E8E4D8 on #14171A") == ["#E8E4D8", "#14171A"]
assert _colour_literals("#000000") == ["#000000"]
def test_every_group_is_individually_selectable():
"""One flat list, all skippable. An install that wants
three tokens must be able to get three."""
only_text = roles.starter_tokens(["text"])
assert {r["group_name"] for r in only_text} == {"text"}
assert len(only_text) == 4
def test_empty_selection_yields_nothing_and_is_not_the_same_as_None():
"""`[]` is a real answer — "none of them" — and must not be read as
"unspecified, so give me everything". Getting this backwards would seed 40
rows into a system whose creator explicitly declined."""
assert roles.starter_tokens([]) == []
assert len(roles.starter_tokens(None)) > 30
def test_unknown_group_names_are_ignored_not_fatal():
"""This feeds a checkbox list. A stale name from an older client should not
fail an otherwise-fine creation."""
out = roles.starter_tokens(["text", "not-a-real-group"])
assert {r["group_name"] for r in out} == {"text"}
def test_the_prefix_is_the_installs_choice():
"""`--fs-` is FabledSword's convention, not the product's. Baking it in
would put one family's naming into every install."""
assert all(r["name"].startswith("--ds-") for r in roles.starter_tokens(["text"]))
custom = roles.starter_tokens(["text"], prefix="--acme-")
assert all(r["name"].startswith("--acme-") for r in custom)
assert "--acme-text-primary" in {r["name"] for r in custom}
def test_text_on_action_is_in_the_starter_set():
"""The specific role whose absence produced 76 literals. It is separate
from text-primary on purpose: the surfaces it sits on do not change with
the mode, while the page does — so reusing text-primary there passes in
dark and fails contrast in light."""
names = {r["name"] for r in roles.starter_tokens(["text"])}
assert "--ds-text-on-action" in names
assert "--ds-text-primary" in names
def test_names_are_valid_custom_properties():
"""They go straight into a stylesheet; an invalid name is a silent no-op
rather than an error, which is the worst failure mode available."""
valid = re.compile(r"^--[A-Za-z0-9_-]+$")
for row in roles.starter_tokens():
assert valid.match(row["name"]), row["name"]
def test_no_duplicate_names_across_the_whole_set():
"""A design system has a partial-unique index on (system, name); a
duplicate in the starter set would make creation fail at the DB with a
constraint error rather than anything legible."""
names = [r["name"] for r in roles.starter_tokens()]
assert len(names) == len(set(names))
def test_every_role_states_a_purpose():
"""An unfilled role is only useful if it says what belongs there. "Colour
1" is a blank with extra steps."""
for row in roles.starter_tokens():
assert row["purpose"].strip(), row["name"]
def test_describe_groups_matches_what_starter_tokens_produces():
"""The catalogue a UI renders and the rows creation writes must not drift —
a checklist offering a group that seeds nothing is a lie in the UI."""
described = {g["group"]: g["token_count"] for g in roles.describe_groups()}
for group, count in described.items():
assert len(roles.starter_tokens([group])) == count