mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
196 lines
8.3 KiB
Python
196 lines
8.3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from plyngent.config.models import SkillsConfig
|
|
from plyngent.skills import SKILL_FILE, Skill, SkillStore, build_roots, validate_skill_name
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
def _write_skill(root: Path, name: str, *, description: str | None = "A test skill", body: str = "Body.") -> Path:
|
|
directory = root / name
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
front = "---\n"
|
|
if description is not None:
|
|
front += f"name: {name}\ndescription: {description}\n"
|
|
front += "---\n\n"
|
|
_ = (directory / SKILL_FILE).write_text(front + body + "\n", encoding="utf-8")
|
|
return directory
|
|
|
|
|
|
def test_build_roots_orders_explicit_paths_first(tmp_path: Path) -> None:
|
|
config = SkillsConfig(paths=["~/team-skills", "/srv/skills"])
|
|
roots = build_roots(
|
|
config,
|
|
workspace=tmp_path / "proj",
|
|
user_config_dir=tmp_path / "config",
|
|
home=tmp_path / "home",
|
|
)
|
|
assert [(r.origin, r.scope, str(r.path)) for r in roots] == [
|
|
("config", "user", str(tmp_path / "home" / "team-skills")),
|
|
("config", "user", "/srv/skills"),
|
|
("plyngent", "user", str(tmp_path / "config" / "skills")),
|
|
("claude", "user", str(tmp_path / "home" / ".claude" / "skills")),
|
|
("claude", "project", str(tmp_path / "proj" / ".claude" / "skills")),
|
|
]
|
|
|
|
|
|
def test_build_roots_respects_discover_and_project_switch(tmp_path: Path) -> None:
|
|
config = SkillsConfig(discover=["plyngent"], include_project_roots=False)
|
|
roots = build_roots(config, workspace=tmp_path, user_config_dir=tmp_path / "cfg", home=tmp_path)
|
|
assert [r.origin for r in roots] == ["plyngent"]
|
|
|
|
|
|
def test_store_discovers_skills_and_labels_their_root(tmp_path: Path) -> None:
|
|
user = tmp_path / "config" / "skills"
|
|
_ = _write_skill(user, "pdf-processing", description="Extract text from PDFs")
|
|
store = SkillStore(
|
|
build_roots(SkillsConfig(discover=["plyngent"]), workspace=tmp_path, user_config_dir=tmp_path / "config")
|
|
)
|
|
skills = store.skills
|
|
assert [(s.name, s.description, s.root.label) for s in skills] == [
|
|
("pdf-processing", "Extract text from PDFs", "plyngent-user"),
|
|
]
|
|
assert skills[0].skill_file.name == SKILL_FILE
|
|
|
|
|
|
def test_store_lists_skills_from_several_roots_sorted(tmp_path: Path) -> None:
|
|
config_dir = tmp_path / "config"
|
|
_ = _write_skill(config_dir / "skills", "zeta")
|
|
_ = _write_skill(tmp_path / "home" / ".claude" / "skills", "alpha")
|
|
store = SkillStore(
|
|
build_roots(SkillsConfig(), workspace=tmp_path, user_config_dir=config_dir, home=tmp_path / "home")
|
|
)
|
|
assert [s.name for s in store.skills] == ["alpha", "zeta"]
|
|
assert [s.root.origin for s in store.skills] == ["claude", "plyngent"]
|
|
|
|
|
|
def test_explicit_path_wins_and_shadowed_copy_stays_listed(tmp_path: Path) -> None:
|
|
explicit = tmp_path / "team"
|
|
config_dir = tmp_path / "config"
|
|
_ = _write_skill(explicit, "shared", description="Team copy")
|
|
_ = _write_skill(config_dir / "skills", "shared", description="User copy")
|
|
config = SkillsConfig(paths=[str(explicit)], discover=["plyngent"])
|
|
store = SkillStore(build_roots(config, workspace=tmp_path, user_config_dir=config_dir))
|
|
|
|
found = store.find("shared")
|
|
assert found is not None
|
|
assert found.description == "Team copy"
|
|
assert [s.description for s in store.shadowed()["shared"]] == ["Team copy", "User copy"]
|
|
assert len(store.all_skills) == 2
|
|
|
|
|
|
def test_find_is_case_insensitive_fallback(tmp_path: Path) -> None:
|
|
_ = _write_skill(tmp_path / "skills", "pdf-processing")
|
|
store = SkillStore(build_roots(SkillsConfig(discover=["plyngent"]), workspace=tmp_path, user_config_dir=tmp_path))
|
|
assert store.find("PDF-Processing") is not None
|
|
assert store.find("nope") is None
|
|
|
|
|
|
def test_root_itself_may_be_a_skill(tmp_path: Path) -> None:
|
|
single = tmp_path / "my-skill"
|
|
single.mkdir()
|
|
_ = (single / SKILL_FILE).write_text("---\nname: my-skill\ndescription: One-off\n---\n\nBody.\n", encoding="utf-8")
|
|
store = SkillStore(build_roots(SkillsConfig(paths=[str(single)]), workspace=tmp_path, user_config_dir=tmp_path))
|
|
assert [s.name for s in store.skills] == ["my-skill"]
|
|
|
|
|
|
def test_missing_roots_are_not_an_error(tmp_path: Path) -> None:
|
|
store = SkillStore(
|
|
build_roots(SkillsConfig(), workspace=tmp_path, user_config_dir=tmp_path / "nope", home=tmp_path)
|
|
)
|
|
assert store.skills == []
|
|
assert store.shadowed() == {}
|
|
|
|
|
|
def test_description_falls_back_to_the_first_paragraph(tmp_path: Path) -> None:
|
|
directory = tmp_path / "skills" / "no-desc"
|
|
directory.mkdir(parents=True)
|
|
_ = (directory / SKILL_FILE).write_text(
|
|
"# Heading\n\nFirst paragraph of the skill.\n\nSecond paragraph.\n",
|
|
encoding="utf-8",
|
|
)
|
|
store = SkillStore(build_roots(SkillsConfig(discover=["plyngent"]), workspace=tmp_path, user_config_dir=tmp_path))
|
|
skill = store.skills[0]
|
|
assert skill.description == "First paragraph of the skill."
|
|
assert skill.declared_name is None
|
|
assert any("no frontmatter description" in issue for issue in skill.issues)
|
|
|
|
|
|
def test_unusable_declared_name_falls_back_to_the_directory(tmp_path: Path) -> None:
|
|
directory = tmp_path / "skills" / "dir-name"
|
|
directory.mkdir(parents=True)
|
|
_ = (directory / SKILL_FILE).write_text(
|
|
'---\nname: "PDF Skills"\ndescription: Mixed case\n---\n\nBody.\n',
|
|
encoding="utf-8",
|
|
)
|
|
store = SkillStore(build_roots(SkillsConfig(discover=["plyngent"]), workspace=tmp_path, user_config_dir=tmp_path))
|
|
skill = store.skills[0]
|
|
assert skill.name == "dir-name"
|
|
assert skill.declared_name == "PDF Skills"
|
|
assert any("not usable" in issue for issue in skill.issues)
|
|
|
|
|
|
def test_broken_frontmatter_still_yields_a_listable_skill(tmp_path: Path) -> None:
|
|
directory = tmp_path / "skills" / "broken"
|
|
directory.mkdir(parents=True)
|
|
_ = (directory / SKILL_FILE).write_text("---\nname: broken\nbroken line\n---\n\nBody text.\n", encoding="utf-8")
|
|
store = SkillStore(build_roots(SkillsConfig(discover=["plyngent"]), workspace=tmp_path, user_config_dir=tmp_path))
|
|
skill = store.skills[0]
|
|
assert skill.name == "broken"
|
|
assert skill.description # falls back to the body
|
|
assert any("frontmatter not parsed" in issue for issue in skill.issues)
|
|
|
|
|
|
def test_every_lookup_reads_the_filesystem(tmp_path: Path) -> None:
|
|
"""No cache: an edit or a skill added outside the chat is simply there."""
|
|
root = tmp_path / "skills"
|
|
directory = _write_skill(root, "live", description="Before")
|
|
store = SkillStore(build_roots(SkillsConfig(discover=["plyngent"]), workspace=tmp_path, user_config_dir=tmp_path))
|
|
assert store.skills[0].description == "Before"
|
|
|
|
_ = (directory / SKILL_FILE).write_text(
|
|
"---\nname: live\ndescription: After\n---\n\nBody.\n",
|
|
encoding="utf-8",
|
|
)
|
|
assert store.skills[0].description == "After"
|
|
|
|
_ = _write_skill(root, "added")
|
|
assert [s.name for s in store.skills] == ["added", "live"]
|
|
|
|
|
|
def test_default_write_root_prefers_our_own_directory(tmp_path: Path) -> None:
|
|
config_dir = tmp_path / "config"
|
|
store = SkillStore(build_roots(SkillsConfig(), workspace=tmp_path, user_config_dir=config_dir, home=tmp_path))
|
|
root = store.default_write_root()
|
|
assert root is not None
|
|
assert root.origin == "plyngent"
|
|
assert root.path == config_dir / "skills"
|
|
|
|
explicit = SkillStore(
|
|
build_roots(SkillsConfig(paths=["/srv/skills"], discover=[]), workspace=tmp_path, user_config_dir=config_dir)
|
|
)
|
|
fallback = explicit.default_write_root()
|
|
assert fallback is not None and fallback.origin == "config"
|
|
assert SkillStore([]).default_write_root() is None
|
|
|
|
|
|
def test_validate_skill_name() -> None:
|
|
assert validate_skill_name("pdf-processing") is None
|
|
assert validate_skill_name("a1") is None
|
|
assert validate_skill_name("") is not None
|
|
assert validate_skill_name("PDF") is not None
|
|
assert validate_skill_name("with_underscore") is not None
|
|
assert validate_skill_name("x" * 65) is not None
|
|
|
|
|
|
def test_skill_is_a_frozen_record(tmp_path: Path) -> None:
|
|
directory = _write_skill(tmp_path / "skills", "frozen")
|
|
store = SkillStore(build_roots(SkillsConfig(discover=["plyngent"]), workspace=tmp_path, user_config_dir=tmp_path))
|
|
skill = store.skills[0]
|
|
assert isinstance(skill, Skill)
|
|
assert skill.path == directory
|
|
assert skill.metadata["name"] == "frozen"
|