mirror of
https://fastgit.cc/github.com/HKUDS/CLI-Anything
synced 2026-04-21 13:21:02 +08:00
Three harnesses shipped without the SKILL.md skill definition introduced in Phase 6.5, and their setup.py files lacked the package_data entry needed for pip install to include the skill file. This means agents cannot discover these CLIs through the standard skill system. Changes: - Add skills/SKILL.md for adguardhome (12 command groups, 36+ commands) - Add skills/SKILL.md for comfyui (5 command groups: workflow, queue, models, images, system) - Add skills/SKILL.md for mermaid (4 command groups: project, diagram, export, session) - Fix adguardhome/setup.py: add package_data and include_package_data - Fix mermaid/setup.py: add package_data and include_package_data - Fix comfyui/setup.py: add package_data for skills (was missing despite include_package_data=True already being set) - Add comfyui to .gitignore allow-list (was tracked before the gitignore was tightened, but new files could not be added) - Expand test_skill_path.py HARNESSES list from 11 to all 18 harnesses - Fix test assertion to accept mubu-style explicit SKILL.md reference alongside the glob pattern used by other harnesses Test results: 79 passed (was 51 tests covering only 11 harnesses)
199 lines
7.1 KiB
Python
199 lines
7.1 KiB
Python
"""Tests that SKILL.md is discoverable after pip install.
|
|
|
|
Simulates the installed package layout and verifies:
|
|
1. ReplSkin auto-detects the skill file from its __file__ location
|
|
2. The banner output includes the absolute skill path
|
|
3. Missing skill file results in skill_path=None
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import tempfile
|
|
import textwrap
|
|
from pathlib import Path
|
|
from io import StringIO
|
|
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _build_package_tree(root: Path, software: str = "demo") -> Path:
|
|
"""Create a minimal cli_anything/<software>/ layout with repl_skin + SKILL.md.
|
|
|
|
Returns the path to the utils/ directory (where repl_skin.py lives).
|
|
"""
|
|
pkg = root / "cli_anything" / software
|
|
utils = pkg / "utils"
|
|
skills = pkg / "skills"
|
|
utils.mkdir(parents=True)
|
|
skills.mkdir(parents=True)
|
|
|
|
# Copy the canonical repl_skin.py from the plugin
|
|
src = Path(__file__).resolve().parent.parent.parent / "cli-anything-plugin" / "repl_skin.py"
|
|
shutil.copy(src, utils / "repl_skin.py")
|
|
|
|
# Write a minimal SKILL.md
|
|
(skills / "SKILL.md").write_text(textwrap.dedent("""\
|
|
---
|
|
name: "cli-anything-demo"
|
|
description: "Demo skill"
|
|
---
|
|
# cli-anything-demo
|
|
"""))
|
|
|
|
return utils
|
|
|
|
|
|
def _load_repl_skin(utils_dir: Path):
|
|
"""Import ReplSkin from the given utils directory (simulating installed path)."""
|
|
import importlib.util
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
"repl_skin", utils_dir / "repl_skin.py"
|
|
)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
# Set __file__ so auto-detection resolves relative to this location
|
|
mod.__file__ = str(utils_dir / "repl_skin.py")
|
|
spec.loader.exec_module(mod)
|
|
return mod.ReplSkin
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSkillPathAutoDetect:
|
|
"""ReplSkin should auto-detect skills/SKILL.md relative to its own location."""
|
|
|
|
def test_auto_detects_skill_path(self, tmp_path):
|
|
utils = _build_package_tree(tmp_path)
|
|
ReplSkin = _load_repl_skin(utils)
|
|
|
|
skin = ReplSkin("demo", version="1.0.0")
|
|
|
|
expected = str(tmp_path / "cli_anything" / "demo" / "skills" / "SKILL.md")
|
|
assert skin.skill_path == expected
|
|
|
|
def test_skill_path_is_absolute(self, tmp_path):
|
|
utils = _build_package_tree(tmp_path)
|
|
ReplSkin = _load_repl_skin(utils)
|
|
|
|
skin = ReplSkin("demo", version="1.0.0")
|
|
|
|
assert os.path.isabs(skin.skill_path)
|
|
|
|
def test_skill_file_exists_at_detected_path(self, tmp_path):
|
|
utils = _build_package_tree(tmp_path)
|
|
ReplSkin = _load_repl_skin(utils)
|
|
|
|
skin = ReplSkin("demo", version="1.0.0")
|
|
|
|
assert Path(skin.skill_path).is_file()
|
|
|
|
def test_none_when_skill_missing(self, tmp_path):
|
|
utils = _build_package_tree(tmp_path)
|
|
# Remove the SKILL.md
|
|
(tmp_path / "cli_anything" / "demo" / "skills" / "SKILL.md").unlink()
|
|
|
|
ReplSkin = _load_repl_skin(utils)
|
|
skin = ReplSkin("demo", version="1.0.0")
|
|
|
|
assert skin.skill_path is None
|
|
|
|
def test_explicit_skill_path_overrides_auto(self, tmp_path):
|
|
utils = _build_package_tree(tmp_path)
|
|
ReplSkin = _load_repl_skin(utils)
|
|
|
|
skin = ReplSkin("demo", version="1.0.0", skill_path="/custom/SKILL.md")
|
|
|
|
assert skin.skill_path == "/custom/SKILL.md"
|
|
|
|
|
|
class TestSkillPathInBanner:
|
|
"""The REPL banner should display the skill path when present."""
|
|
|
|
def test_banner_shows_skill_path(self, tmp_path, capsys):
|
|
utils = _build_package_tree(tmp_path)
|
|
ReplSkin = _load_repl_skin(utils)
|
|
|
|
skin = ReplSkin("demo", version="1.0.0")
|
|
skin.print_banner()
|
|
|
|
output = capsys.readouterr().out
|
|
assert "Skill:" in output
|
|
assert "SKILL.md" in output
|
|
|
|
def test_banner_omits_skill_when_missing(self, tmp_path, capsys):
|
|
utils = _build_package_tree(tmp_path)
|
|
(tmp_path / "cli_anything" / "demo" / "skills" / "SKILL.md").unlink()
|
|
|
|
ReplSkin = _load_repl_skin(utils)
|
|
skin = ReplSkin("demo", version="1.0.0")
|
|
skin.print_banner()
|
|
|
|
output = capsys.readouterr().out
|
|
assert "Skill:" not in output
|
|
|
|
|
|
class TestInstalledHarnesses:
|
|
"""Verify each real harness has SKILL.md in the correct package location."""
|
|
|
|
HARNESSES = [
|
|
("adguardhome", "adguardhome"),
|
|
("anygen", "anygen"),
|
|
("audacity", "audacity"),
|
|
("blender", "blender"),
|
|
("comfyui", "comfyui"),
|
|
("drawio", "drawio"),
|
|
("gimp", "gimp"),
|
|
("inkscape", "inkscape"),
|
|
("kdenlive", "kdenlive"),
|
|
("libreoffice", "libreoffice"),
|
|
("mermaid", "mermaid"),
|
|
("mubu", "mubu"),
|
|
("notebooklm", "notebooklm"),
|
|
("novita", "novita"),
|
|
("obs-studio", "obs_studio"),
|
|
("ollama", "ollama"),
|
|
("shotcut", "shotcut"),
|
|
("zoom", "zoom"),
|
|
]
|
|
|
|
@pytest.mark.parametrize("dir_name,pkg_name", HARNESSES)
|
|
def test_skill_md_exists_in_package(self, dir_name, pkg_name):
|
|
repo_root = Path(__file__).resolve().parent.parent.parent
|
|
skill_path = repo_root / dir_name / "agent-harness" / "cli_anything" / pkg_name / "skills" / "SKILL.md"
|
|
assert skill_path.is_file(), f"Missing: {skill_path}"
|
|
|
|
@pytest.mark.parametrize("dir_name,pkg_name", HARNESSES)
|
|
def test_skill_md_has_yaml_frontmatter(self, dir_name, pkg_name):
|
|
repo_root = Path(__file__).resolve().parent.parent.parent
|
|
skill_path = repo_root / dir_name / "agent-harness" / "cli_anything" / pkg_name / "skills" / "SKILL.md"
|
|
content = skill_path.read_text()
|
|
assert content.startswith("---"), f"Missing YAML frontmatter in {skill_path}"
|
|
# Must have closing ---
|
|
assert content.count("---") >= 2
|
|
|
|
@pytest.mark.parametrize("dir_name,pkg_name", HARNESSES)
|
|
def test_skill_md_has_command_groups(self, dir_name, pkg_name):
|
|
repo_root = Path(__file__).resolve().parent.parent.parent
|
|
skill_path = repo_root / dir_name / "agent-harness" / "cli_anything" / pkg_name / "skills" / "SKILL.md"
|
|
content = skill_path.read_text()
|
|
assert "## Command Groups" in content
|
|
# Must have at least one filled command row
|
|
assert "| `" in content, f"Empty command tables in {skill_path}"
|
|
|
|
@pytest.mark.parametrize("dir_name,pkg_name", HARNESSES)
|
|
def test_setup_py_includes_package_data(self, dir_name, pkg_name):
|
|
repo_root = Path(__file__).resolve().parent.parent.parent
|
|
setup_path = repo_root / dir_name / "agent-harness" / "setup.py"
|
|
content = setup_path.read_text()
|
|
assert "package_data" in content, f"Missing package_data in {setup_path}"
|
|
# Accept both glob style ("skills/*.md") and explicit style ("SKILL.md" in a .skills key)
|
|
has_skill_ref = "skills/*.md" in content or ("skills" in content and "SKILL.md" in content)
|
|
assert has_skill_ref, f"Missing skills/*.md or SKILL.md in package_data: {setup_path}"
|