371 lines
12 KiB
Python
371 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from yuxi.agents.skills import remote_install as svc
|
|
|
|
|
|
def test_parse_available_skills_from_cli_output() -> None:
|
|
output = """
|
|
\x1b[38;5;250m███████╗\x1b[0m
|
|
◇ Available Skills
|
|
Claude Api
|
|
|
|
claude-api
|
|
|
|
Build apps with the Claude API.
|
|
|
|
Example Skills
|
|
|
|
frontend-design
|
|
|
|
Create distinctive frontend interfaces.
|
|
|
|
└ Use --skill <name> to install specific skills
|
|
"""
|
|
|
|
skills = svc._parse_available_skills(output)
|
|
|
|
assert skills == [
|
|
{"name": "claude-api", "description": "Build apps with the Claude API."},
|
|
{"name": "frontend-design", "description": "Create distinctive frontend interfaces."},
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_remote_skills_uses_isolated_home(monkeypatch: pytest.MonkeyPatch):
|
|
captured: dict[str, object] = {}
|
|
|
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
|
captured["args"] = args
|
|
captured["home"] = env["HOME"]
|
|
captured["cwd"] = cwd
|
|
return """
|
|
◇ Available Skills
|
|
|
|
frontend-design
|
|
|
|
Create distinctive frontend interfaces.
|
|
|
|
└ Use --skill <name> to install specific skills
|
|
"""
|
|
|
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
|
|
|
items = await svc.list_remote_skills("anthropics/skills")
|
|
|
|
assert items == [{"name": "frontend-design", "description": "Create distinctive frontend interfaces."}]
|
|
assert captured["args"] == ["npx", "-y", "skills", "add", "anthropics/skills", "--list"]
|
|
assert str(captured["cwd"]).startswith(str(captured["home"]))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_remote_skill_imports_from_cli_output_dir(monkeypatch: pytest.MonkeyPatch):
|
|
calls: list[tuple[list[str], str]] = []
|
|
|
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
|
calls.append((args, env["HOME"]))
|
|
home = Path(env["HOME"])
|
|
if "--list" in args:
|
|
return """
|
|
◇ Available Skills
|
|
|
|
frontend-design
|
|
|
|
Create distinctive frontend interfaces.
|
|
|
|
└ Use --skill <name> to install specific skills
|
|
"""
|
|
skill_dir = home / ".agents" / "skills" / "frontend-design"
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: frontend-design\ndescription: demo\n---\n# Demo\n",
|
|
encoding="utf-8",
|
|
)
|
|
return "installed"
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
|
captured["source_dir"] = Path(source_dir)
|
|
captured["created_by"] = created_by
|
|
return {"slug": "frontend-design"}
|
|
|
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
|
|
|
item = await svc.install_remote_skill(
|
|
None,
|
|
source="anthropics/skills",
|
|
skill="frontend-design",
|
|
created_by="root",
|
|
)
|
|
|
|
assert item == {"slug": "frontend-design"}
|
|
assert calls[0][0] == ["npx", "-y", "skills", "add", "anthropics/skills", "--list"]
|
|
assert calls[1][0] == [
|
|
"npx",
|
|
"-y",
|
|
"skills",
|
|
"add",
|
|
"anthropics/skills",
|
|
"--skill",
|
|
"frontend-design",
|
|
"-g",
|
|
"-y",
|
|
"--copy",
|
|
]
|
|
assert captured["source_dir"] == Path(calls[1][1]) / ".agents" / "skills" / "frontend-design"
|
|
assert captured["created_by"] == "root"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_remote_skill_rejects_missing_remote_skill(monkeypatch: pytest.MonkeyPatch):
|
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
|
return """
|
|
◇ Available Skills
|
|
|
|
other-skill
|
|
|
|
Description
|
|
|
|
└ Use --skill <name> to install specific skills
|
|
"""
|
|
|
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
|
|
|
with pytest.raises(ValueError, match="不存在 skill"):
|
|
await svc.install_remote_skill(
|
|
None,
|
|
source="anthropics/skills",
|
|
skill="frontend-design",
|
|
created_by="root",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_remote_skills_batch_installs_all(monkeypatch: pytest.MonkeyPatch):
|
|
calls: list[tuple[list[str], str]] = []
|
|
imported_skills: list[str] = []
|
|
|
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
|
calls.append((args, env["HOME"]))
|
|
home = Path(env["HOME"])
|
|
skill_dir_base = home / ".agents" / "skills"
|
|
for skill_name in ("frontend-design", "claude-api", "code-review"):
|
|
(skill_dir_base / skill_name).mkdir(parents=True, exist_ok=True)
|
|
(skill_dir_base / skill_name / "SKILL.md").write_text(
|
|
f"---\nname: {skill_name}\ndescription: demo\n---\n# {skill_name}\n",
|
|
encoding="utf-8",
|
|
)
|
|
return "installed"
|
|
|
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
|
imported_skills.append(source_dir.name)
|
|
return SimpleNamespace(slug=source_dir.name)
|
|
|
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
|
|
|
results = await svc.install_remote_skills_batch(
|
|
None,
|
|
source="anthropics/skills",
|
|
skills=["frontend-design", "claude-api", "code-review"],
|
|
created_by="root",
|
|
)
|
|
|
|
# Should only have 1 CLI call (no --list, direct batch install)
|
|
assert len(calls) == 1
|
|
assert calls[0][0] == [
|
|
"npx",
|
|
"-y",
|
|
"skills",
|
|
"add",
|
|
"anthropics/skills",
|
|
"--skill",
|
|
"frontend-design",
|
|
"--skill",
|
|
"claude-api",
|
|
"--skill",
|
|
"code-review",
|
|
"-g",
|
|
"-y",
|
|
"--copy",
|
|
]
|
|
|
|
assert len(results) == 3
|
|
assert all(r["success"] for r in results)
|
|
assert [r["slug"] for r in results] == ["frontend-design", "claude-api", "code-review"]
|
|
assert imported_skills == ["frontend-design", "claude-api", "code-review"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_remote_skills_batch_skips_missing(monkeypatch: pytest.MonkeyPatch):
|
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
|
home = Path(env["HOME"])
|
|
skill_dir_base = home / ".agents" / "skills"
|
|
(skill_dir_base / "frontend-design").mkdir(parents=True, exist_ok=True)
|
|
(skill_dir_base / "frontend-design" / "SKILL.md").write_text(
|
|
"---\nname: frontend-design\ndescription: demo\n---\n# Demo\n",
|
|
encoding="utf-8",
|
|
)
|
|
return "installed"
|
|
|
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
|
return SimpleNamespace(slug=source_dir.name)
|
|
|
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
|
|
|
results = await svc.install_remote_skills_batch(
|
|
None,
|
|
source="anthropics/skills",
|
|
skills=["frontend-design", "nonexistent-skill"],
|
|
created_by="root",
|
|
)
|
|
|
|
assert len(results) == 2
|
|
assert results[0] == {"slug": "frontend-design", "success": True}
|
|
assert results[1] == {"slug": "nonexistent-skill", "success": False, "error": "skills CLI 未生成预期的技能目录"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_remote_skills_batch_partial_failure(monkeypatch: pytest.MonkeyPatch):
|
|
calls: list[tuple[list[str], str]] = []
|
|
|
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
|
calls.append((args, env["HOME"]))
|
|
home = Path(env["HOME"])
|
|
skill_dir_base = home / ".agents" / "skills"
|
|
(skill_dir_base / "skill-a").mkdir(parents=True, exist_ok=True)
|
|
(skill_dir_base / "skill-a" / "SKILL.md").write_text(
|
|
"---\nname: skill-a\ndescription: demo\n---\n# A\n",
|
|
encoding="utf-8",
|
|
)
|
|
# skill-b directory missing (simulate install failure from CLI side)
|
|
(skill_dir_base / "skill-c").mkdir(parents=True, exist_ok=True)
|
|
(skill_dir_base / "skill-c" / "SKILL.md").write_text(
|
|
"---\nname: skill-c\ndescription: demo\n---\n# C\n",
|
|
encoding="utf-8",
|
|
)
|
|
return "installed"
|
|
|
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
|
return SimpleNamespace(slug=source_dir.name)
|
|
|
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
|
|
|
results = await svc.install_remote_skills_batch(
|
|
None,
|
|
source="test/repo",
|
|
skills=["skill-a", "skill-b", "skill-c"],
|
|
created_by="root",
|
|
)
|
|
|
|
assert len(results) == 3
|
|
assert results[0] == {"slug": "skill-a", "success": True}
|
|
assert results[1] == {"slug": "skill-b", "success": False, "error": "skills CLI 未生成预期的技能目录"}
|
|
assert results[2] == {"slug": "skill-c", "success": True}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_remote_skills_batch_handles_invalid_names(monkeypatch: pytest.MonkeyPatch):
|
|
calls: list[tuple[list[str], str]] = []
|
|
|
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
|
calls.append((args, env["HOME"]))
|
|
home = Path(env["HOME"])
|
|
skill_dir_base = home / ".agents" / "skills"
|
|
(skill_dir_base / "valid-skill").mkdir(parents=True, exist_ok=True)
|
|
(skill_dir_base / "valid-skill" / "SKILL.md").write_text(
|
|
"---\nname: valid-skill\ndescription: demo\n---\n# Valid\n",
|
|
encoding="utf-8",
|
|
)
|
|
return "installed"
|
|
|
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
|
return SimpleNamespace(slug=source_dir.name)
|
|
|
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
|
|
|
results = await svc.install_remote_skills_batch(
|
|
None,
|
|
source="test/repo",
|
|
skills=["valid-skill", "Bad Name", "another-valid"],
|
|
created_by="root",
|
|
)
|
|
|
|
assert len(results) == 3
|
|
assert results[0] == {"slug": "valid-skill", "success": True}
|
|
assert results[1]["success"] is False
|
|
assert "不合法" in results[1]["error"]
|
|
assert results[2] == {"slug": "another-valid", "success": False, "error": "skills CLI 未生成预期的技能目录"}
|
|
|
|
# Only valid skills passed to the CLI
|
|
assert len(calls) == 1
|
|
assert "--skill" in str(calls[0][0])
|
|
assert "valid-skill" in str(calls[0][0])
|
|
assert "Bad" not in str(calls[0][0])
|
|
|
|
|
|
def test_parse_search_skills() -> None:
|
|
output = """
|
|
Install with npx skills add <owner/repo@skill>
|
|
|
|
vercel-labs/agent-skills@web-design-guidelines 339.3K installs
|
|
└ https://skills.sh/vercel-labs/agent-skills/web-design-guidelines
|
|
|
|
xixu-me/skills@secure-linux-web-hosting 158.6K installs
|
|
└ https://skills.sh/xixu-me/skills/secure-linux-web-hosting
|
|
|
|
anthropics/skills@webapp-testing
|
|
└ https://skills.sh/anthropics/skills/webapp-testing
|
|
"""
|
|
|
|
results = svc._parse_search_skills(output)
|
|
assert results == [
|
|
{
|
|
"source": "vercel-labs/agent-skills",
|
|
"name": "web-design-guidelines",
|
|
"installs": "339.3K installs",
|
|
},
|
|
{
|
|
"source": "xixu-me/skills",
|
|
"name": "secure-linux-web-hosting",
|
|
"installs": "158.6K installs",
|
|
},
|
|
{
|
|
"source": "anthropics/skills",
|
|
"name": "webapp-testing",
|
|
"installs": "",
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_remote_skills(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured: dict[str, object] = {}
|
|
|
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
|
captured["args"] = args
|
|
return """
|
|
vercel-labs/agent-skills@web-design-guidelines 339.3K installs
|
|
"""
|
|
|
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
|
|
|
items = await svc.search_remote_skills("web")
|
|
assert items == [
|
|
{
|
|
"source": "vercel-labs/agent-skills",
|
|
"name": "web-design-guidelines",
|
|
"installs": "339.3K installs",
|
|
}
|
|
]
|
|
assert captured["args"] == ["npx", "-y", "skills", "find", "web"]
|