tests.test_skills module

Conformance of bundled skills to the Agent Skills specification.

Every bundled SKILL.md is checked against the Agent Skills spec, so a new or edited skill cannot silently drift out of it.

Note

The spec allows six frontmatter fields, and argument-hint is the single deliberate deviation (see CLAUDE_CODE_EXTENSIONS). Anything Claude Code could express through another of its own extensions belongs in a spec field instead: the recommended model rides in compatibility rather than a model: key, which is why test_skill_compatibility_carries_the_model_hint() exists. Pinning the extension set to exactly one entry is what stops that budget creeping back, and it catches a misspelled field name (user_invocable for user-invocable) that would otherwise parse as valid YAML and be silently ignored. The same pin keeps disable-model-invocation: out: every skill is model-invocable by design, and the permission layer, not frontmatter, decides what it may do. Argue a new field here before adding it to a skill.

tests.test_skills.SKILLS_PAGE = PosixPath('/home/runner/work/repomatic/repomatic/docs/agent-skills.md')

The hand-maintained page whose table must mirror the skill registry.

tests.test_skills.SKILL_TABLE_ROW_RE = re.compile('^\\|[^|]*\\|\\s*\\[`/([a-z0-9-]+)`\\]', re.MULTILINE)

The skill name out of a table row, read from the linked /name cell.

tests.test_skills.SPEC_FIELDS = frozenset({'allowed-tools', 'compatibility', 'description', 'license', 'metadata', 'name'})

The six frontmatter fields the Agent Skills spec defines.

tests.test_skills.CLAUDE_CODE_EXTENSIONS = frozenset({'argument-hint'})

The only non-spec frontmatter field bundled skills are allowed to carry.

Kept because no spec field expresses an autocomplete hint, and it degrades to a no-op wherever it is not understood. Do not grow this set: reach for a spec field first, the way compatibility now carries what model: used to.

tests.test_skills.MAX_COMPATIBILITY_LENGTH = 500

Spec ceiling on the compatibility field.

tests.test_skills.MAX_DESCRIPTION_LENGTH = 1024

Spec ceiling on the description field.

tests.test_skills.MAX_NAME_LENGTH = 64

Spec ceiling on the name field.

tests.test_skills.SKILL_NAME_RE = re.compile('^[a-z0-9]+(-[a-z0-9]+)*$')

Spec charset for name: lowercase alphanumerics and single inner hyphens.

The grouping enforces the three separate prohibitions at once: no leading hyphen, no trailing hyphen, and no consecutive hyphens.

tests.test_skills.frontmatter(entry)[source]

Parse the frontmatter of a bundled skill registry entry.

tests.test_skills.exported_skill(tmp_path)[source]

A synthetic bundled skill, already exported once to its destination.

Stands in for a downstream repo right after repomatic init skills: the destination holds a pristine copy of the bundle.

Returns:

The (source, dest) folder pair.

tests.test_skills.test_skill_is_a_folder_holding_a_skill_md(entry)[source]

The spec’s unit of distribution: a folder entered through SKILL.md.

Registering the folder rather than the file is what lets a skill grow scripts/, references/ or assets/ without touching the registry.

tests.test_skills.test_skill_name_matches_directory(entry)[source]

name is required, spec-shaped, and equal to the parent directory.

tests.test_skills.test_skill_description_within_spec_limit(entry)[source]

description is required and fits the spec ceiling.

tests.test_skills.test_skill_compatibility_carries_the_model_hint(entry)[source]

Every skill names its recommended model in compatibility.

model: is a Claude Code extension the spec does not define, so the hint rides in a spec field instead of a bespoke key or a line of body prose: structured, greppable, and portable to any client that reads the spec.

tests.test_skills.test_skill_optional_spec_fields_are_well_formed(entry)[source]

compatibility and metadata hold what the spec says they hold.

No bundled skill sets metadata today, so that half guards the shape a future one would have to honour rather than an invariant already in play.

tests.test_skills.test_skill_allowed_tools_are_space_separated(entry)[source]

allowed-tools uses the spec’s space-separated string.

Claude Code also accepts a comma-separated string and a YAML list, so a comma here still works locally while failing the spec.

tests.test_skills.test_skill_frontmatter_fields_are_known(entry)[source]

No field beyond the spec’s six and the extensions we opted into.

tests.test_skills.test_skill_resource_folders_are_copied_verbatim(tmp_path)[source]

scripts/, references/ and assets/ travel with a skill.

No bundled skill ships resources yet, so this exercises the contract on a synthetic folder: whatever sits beside SKILL.md is copied as-is, with no per-file registration, and re-running changes nothing.

tests.test_skills.test_drifted_skill_is_overwritten(exported_skill)[source]

A downstream copy that fell behind the bundle is reconciled.

Skills are copied verbatim, with no user-modified heuristic: content that differs from the bundle is rewritten, so a downstream repo picks up new frontmatter on its next repomatic init. Adding a leave-what-is-there guard here would strand every downstream repo on whichever revision it first received, which is why this is pinned rather than left to the idempotency check above (identical content exercises neither branch).

tests.test_skills.test_local_files_beside_a_skill_survive(exported_skill)[source]

Re-exporting creates and overwrites, but never prunes.

Wiping whatever is not bundled would be an easy way to reconcile a drifted folder, and would take a downstream repo’s own files with it. Retiring a bundled asset goes through a REMOVED_ASSETS tombstone instead.

tests.test_skills.test_docs_table_lists_every_bundled_skill()[source]

docs/agent-skills.md tabulates the registry, and nothing else.

The page renders list-skills live right above the table, so a skill added to the registry shows up in the rendered output while the hand-maintained table below it keeps the old roster. Adding repomatic-test-matrix left exactly that gap, and only a reader comparing the two halves of one page would have caught it. Names only: the column padding belongs to mdformat.

The launcher init prints spells the model the skill recommends.

Read off compatibility rather than hard-coded, so a skill moving from Opus to Sonnet moves its launcher with it.

tests.test_skills.test_skill_launcher_passes_arguments_through()[source]

Extra words land in the prompt, where the skill reads them as $ARGUMENTS.