Files
collection-claude-code-sour…/nano-claude-code/tests/test_skills.py
chauncygu 1d4ffa964d Update README.MD and add nano-claude-code v3.0 + original-source-code/src
- README.MD: add original-source-code and nano-claude-code sections, update
  overview table (4 subprojects), add v3.0 news entry, expand comparison table
  with memory/multi-agent/skills dimensions
- nano-claude-code v3.0: multi-agent package (multi_agent/), memory package
  (memory/), skill package (skill/) with built-in /commit and /review skills,
  context compression (compaction.py), tool registry plugin system, diff view,
  17 slash commands, 18 built-in tools, 101 tests (~5000 lines total)
- original-source-code/src: add raw TypeScript source tree (1884 files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 10:26:29 -07:00

235 lines
7.1 KiB
Python

from __future__ import annotations
import pytest
from pathlib import Path
import skill.loader as _loader
from skill.loader import _parse_skill_file, _parse_list_field, find_skill, SkillDef
from skill import load_skills, substitute_arguments
COMMIT_MD = """\
---
name: commit
description: Create a git commit
triggers: [/commit, commit changes]
tools: [Bash, Read]
---
Review staged changes and create a commit with a descriptive message.
"""
REVIEW_MD = """\
---
name: review
description: Review a pull request
triggers: [/review, /review-pr]
tools: [Bash, Read, Grep]
---
Analyze the PR diff and provide constructive feedback.
"""
ARGS_MD = """\
---
name: deploy
description: Deploy to an environment
triggers: [/deploy]
tools: [Bash]
argument-hint: [env] [version]
arguments: [env, version]
---
Deploy $VERSION to $ENV environment. Full args: $ARGUMENTS
"""
@pytest.fixture()
def skill_dir(tmp_path, monkeypatch):
"""Create a temp skill directory with sample skills and patch _get_skill_paths."""
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
(skills_dir / "commit.md").write_text(COMMIT_MD, encoding="utf-8")
(skills_dir / "review.md").write_text(REVIEW_MD, encoding="utf-8")
monkeypatch.setattr(_loader, "_get_skill_paths", lambda: [skills_dir])
# Also patch the builtin list to be empty so tests are predictable
monkeypatch.setattr(_loader, "_BUILTIN_SKILLS", [])
return skills_dir
# ------------------------------------------------------------------
# _parse_list_field
# ------------------------------------------------------------------
def test_parse_list_field_bracket():
assert _parse_list_field("[a, b, c]") == ["a", "b", "c"]
def test_parse_list_field_plain():
assert _parse_list_field("a, b, c") == ["a", "b", "c"]
def test_parse_list_field_single():
assert _parse_list_field("solo") == ["solo"]
# ------------------------------------------------------------------
# _parse_skill_file
# ------------------------------------------------------------------
def test_parse_skill_file(skill_dir):
path = skill_dir / "commit.md"
skill = _parse_skill_file(path)
assert skill is not None
assert skill.name == "commit"
assert skill.description == "Create a git commit"
assert "/commit" in skill.triggers
assert "commit changes" in skill.triggers
assert "Bash" in skill.tools
assert "Read" in skill.tools
assert "commit" in skill.prompt.lower()
assert skill.file_path == str(path)
def test_parse_skill_file_review(skill_dir):
path = skill_dir / "review.md"
skill = _parse_skill_file(path)
assert skill is not None
assert skill.name == "review"
assert "/review" in skill.triggers
assert "/review-pr" in skill.triggers
def test_parse_skill_file_invalid(tmp_path):
bad = tmp_path / "bad.md"
bad.write_text("no frontmatter here", encoding="utf-8")
assert _parse_skill_file(bad) is None
def test_parse_skill_file_no_name(tmp_path):
no_name = tmp_path / "noname.md"
no_name.write_text("---\ndescription: test\n---\nbody\n", encoding="utf-8")
assert _parse_skill_file(no_name) is None
def test_parse_skill_file_context_fork(tmp_path):
fork_md = tmp_path / "fork.md"
fork_md.write_text("---\nname: fork-task\ndescription: test\ncontext: fork\n---\nbody\n")
skill = _parse_skill_file(fork_md)
assert skill is not None
assert skill.context == "fork"
def test_parse_skill_file_allowed_tools(tmp_path):
md = tmp_path / "t.md"
md.write_text("---\nname: myskill\ndescription: d\nallowed-tools: [Bash, Read]\n---\nbody\n")
skill = _parse_skill_file(md)
assert skill is not None
assert "Bash" in skill.tools
assert "Read" in skill.tools
# ------------------------------------------------------------------
# load_skills
# ------------------------------------------------------------------
def test_load_skills(skill_dir):
skills = load_skills()
assert len(skills) == 2
names = {s.name for s in skills}
assert names == {"commit", "review"}
def test_load_skills_empty_dir(tmp_path, monkeypatch):
empty = tmp_path / "empty_skills"
empty.mkdir()
monkeypatch.setattr(_loader, "_get_skill_paths", lambda: [empty])
monkeypatch.setattr(_loader, "_BUILTIN_SKILLS", [])
assert load_skills() == []
def test_load_skills_nonexistent_dir(tmp_path, monkeypatch):
monkeypatch.setattr(_loader, "_get_skill_paths", lambda: [tmp_path / "does_not_exist"])
monkeypatch.setattr(_loader, "_BUILTIN_SKILLS", [])
assert load_skills() == []
def test_load_skills_builtins_present(monkeypatch):
"""Without patching, builtins (commit, review) should be present."""
monkeypatch.setattr(_loader, "_get_skill_paths", lambda: [])
skills = load_skills()
names = {s.name for s in skills}
assert "commit" in names
assert "review" in names
def test_load_skills_project_overrides_builtin(tmp_path, monkeypatch):
"""A project skill with the same name overrides the builtin."""
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
# project-level "commit" with different description
(skills_dir / "commit.md").write_text(
"---\nname: commit\ndescription: OVERRIDDEN\n---\ncustom commit prompt\n"
)
monkeypatch.setattr(_loader, "_get_skill_paths", lambda: [skills_dir])
skills = load_skills()
commit = next(s for s in skills if s.name == "commit")
assert commit.description == "OVERRIDDEN"
# ------------------------------------------------------------------
# find_skill
# ------------------------------------------------------------------
def test_find_skill_commit(skill_dir):
skill = find_skill("/commit")
assert skill is not None
assert skill.name == "commit"
def test_find_skill_review(skill_dir):
skill = find_skill("/review")
assert skill is not None
assert skill.name == "review"
def test_find_skill_review_pr(skill_dir):
skill = find_skill("/review-pr some-pr-url")
assert skill is not None
assert skill.name == "review"
def test_find_skill_nonexistent(skill_dir):
result = find_skill("/nonexistent")
assert result is None
# ------------------------------------------------------------------
# substitute_arguments
# ------------------------------------------------------------------
def test_substitute_arguments_placeholder():
result = substitute_arguments("Deploy $ARGUMENTS please", "v1.2 prod", [])
assert result == "Deploy v1.2 prod please"
def test_substitute_named_args(tmp_path):
result = substitute_arguments(
"Deploy $VERSION to $ENV. Full args: $ARGUMENTS",
"1.0 staging",
["env", "version"],
)
# arg_names are positional: env=1.0, version=staging
assert "$VERSION" not in result
assert "$ENV" not in result
assert "$ARGUMENTS" not in result
def test_substitute_missing_arg():
# If user provides fewer args than named slots, missing ones become ""
result = substitute_arguments("Hello $NAME!", "", ["name"])
assert result == "Hello !"
def test_substitute_no_placeholders():
result = substitute_arguments("just a plain prompt", "some args", [])
assert result == "just a plain prompt"