"""The contract every `skills/*/SKILL.md` has to hold (plan Steps 9 and 11). A skill is a document, and a document has no unit tests -- which is exactly why it needs a contract test. What can be checked mechanically is checked here: the frontmatter parses, the name matches the directory, the trigger phrases the operator would actually type are declared and unique, the body is short enough to be loaded without crowding the context, every intra-plugin path is written as `${CLAUDE_PLUGIN_ROOT}`, no absolute home path is baked in, and every `references/` file is linked from the skill that owns it. Two checks in the plan's Step 9 list read as behavioural, and they are implemented as far as a zero-network test honestly can, with the boundary stated rather than blurred: * *"invoking the skill against a non-existent workspace scaffolds it and reports the created tree"* -- the invocation is a model turn and cannot run here. What runs is the mechanism the skill delegates to, `jobbsok_lib.paths.scaffold`, against a path that does not exist, plus the assertion that SKILL.md actually names it. A skill that stopped calling scaffold would fail the second half. * *"the version echo prints both the build stamp and the manifest version"* -- `BUILD_STAMP` is created in Step 13. So the check is that the skill names both the stamp path and the manifest version, and that `.claude-plugin/plugin.json` really carries a version to echo. The file's existence becomes assertable at Step 13 and not before; claiming otherwise here would be asserting a fact about a file this repository does not have. The trigger-phrase rule deserves its own note. Build-brief 7 requires the description to name the trigger phrases in Norwegian, and the plan requires them "in both accented and ASCII spellings" -- because an operator typing in a hurry writes `lonn` as often as `lønn`. So every accented phrase must have an ASCII twin, folded through `paths.slug`'s rules so the fold is defined in one place rather than twice. Style note: this file follows tests/test_kandidatprofil_schema.py. """ import json import os import re import pytest from jobbsok_lib import frontmatter, paths REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SKILLS = os.path.join(REPO, "skills") MANIFEST = os.path.join(REPO, ".claude-plugin", "plugin.json") NAME_RE = re.compile(r"^[a-z][a-z0-9-]*$") PLUGIN_ROOT = "${CLAUDE_PLUGIN_ROOT}" #: A line that names one of these is naming a file inside the plugin, and has #: to say so with the variable rather than a bare relative path. INTRA_PLUGIN = (".py", "references/", "scripts/", "assets/", "templates/") #: Absolute or home-anchored paths. `paths.py` refuses to guess at a home #: directory; a skill that hardcodes one would put the guess back. HOME_PATHS = ("/Users/", "/home/", "$HOME", "~/") #: First and second person, word-bounded. Build-brief 7 wants a description #: that says what the skill does, not one that addresses the operator. PERSON_RE = re.compile( r"(?