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 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 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 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 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 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"]