Files
FabledGrimoire/tests/test_skill_moments.py
T
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

85 lines
3.4 KiB
Python

"""The bundled skills declare the moments they are for.
Loading the reporting procedure is a report; loading the planning procedure
is planning. Each SKILL.md says which moment it is for in its frontmatter
(`metadata: moments:`), where an author editing the skill sees it — but the
server that resolves a skill load to its moments never sees the plugin's
files, so the same declaration ships as defaults in
`moment_actions.BUNDLED_SKILL_MOMENTS`. Two copies drift unless something
holds them together; this does, in both directions, and every skill has to
make the choice rather than inherit silence.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from grimoire.services import moment_actions as ma
from grimoire.services import moments
ROOT = Path(__file__).resolve().parents[1]
SKILLS = ROOT / "plugin" / "skills"
MANIFEST = ROOT / "plugin" / ".claude-plugin" / "plugin.json"
_FRONT = re.compile(r"\A---\n(.*?)\n---\n", re.S)
_METADATA = re.compile(r"^metadata:[ \t]*\n((?:[ \t]+\S.*(?:\n|\Z))*)", re.M)
_MOMENTS = re.compile(r"^[ \t]+moments:[ \t]*(.*)$", re.M)
def declared(text: str) -> tuple[str, ...]:
"""The moments a SKILL.md's frontmatter declares; () when it declares none.
Agent Skills metadata maps strings to strings, so several moments are one
comma-separated value.
"""
front = _FRONT.match(text)
meta = _METADATA.search(front.group(1)) if front else None
line = _MOMENTS.search(meta.group(1)) if meta else None
if not line:
return ()
return tuple(m.strip() for m in line.group(1).split(",") if m.strip())
def _on_disk() -> dict[str, tuple[str, ...]]:
return {p.parent.name: declared(p.read_text()) for p in sorted(SKILLS.glob("*/SKILL.md"))}
def test_the_reader_finds_a_declaration_and_only_in_the_frontmatter():
"""The guard below is only as good as this reader, so it must be able to
say no: a declaration in the body, or none at all, reads as nothing."""
assert declared(
"---\nname: x\ndescription: y\nmetadata:\n moments: work.plan, reply.report\n---\nbody\n"
) == ("work.plan", "reply.report")
assert declared("---\nname: x\ndescription: y\n---\nmetadata:\n moments: work.plan\n") == ()
assert declared("---\nname: x\nmetadata:\n author: someone\n---\n") == ()
assert declared("no frontmatter\n") == ()
def test_every_bundled_skill_declares_the_moments_it_is_for():
silent = [name for name, found in _on_disk().items() if not found]
assert not silent, (
f"{silent} declare no moment. Add `metadata:` / ` moments: <moment>` to "
f"the frontmatter (list_moments has the catalog) and the same entry to "
f"moment_actions.BUNDLED_SKILL_MOMENTS"
)
def test_the_frontmatter_and_the_shipped_defaults_agree():
assert _on_disk() == ma.BUNDLED_SKILL_MOMENTS, (
"a SKILL.md's `metadata: moments:` and moment_actions.BUNDLED_SKILL_MOMENTS "
"disagree — the server reads only the latter, so change both together"
)
def test_every_declared_moment_is_in_the_catalog():
bad = {name: found for name, found in _on_disk().items()
if any(m not in moments.MOMENTS for m in found)}
assert not bad, bad
def test_the_defaults_qualify_skills_by_the_plugins_own_name():
"""The harness names a plugin's skill `<plugin>:<skill>`, so the defaults
match only while this agrees with the manifest."""
assert json.loads(MANIFEST.read_text())["name"] == ma.BUNDLED_SKILL_PLUGIN