"""Tests for the ``jarvis skill`` CLI commands.""" from __future__ import annotations import textwrap from pathlib import Path from unittest.mock import patch from click.testing import CliRunner from openjarvis.cli import cli class TestSkillCmd: def test_skill_list_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "list", "--help"]) assert result.exit_code == 0 def test_skill_install_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "install", "--help"]) assert result.exit_code == 0 def test_skill_search_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "search", "--help"]) assert result.exit_code == 0 def test_skill_group_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "--help"]) assert result.exit_code == 0 assert "list" in result.output assert "install" in result.output assert "remove" in result.output assert "search" in result.output def test_skill_run_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "run", "--help"]) assert result.exit_code == 0 def test_skill_info_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "info", "--help"]) assert result.exit_code == 0 def test_skill_update_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "update", "--help"]) assert result.exit_code == 0 def test_skill_list_shows_discovered_skills(self, tmp_path: Path) -> None: skill_dir = tmp_path / "my_skill" skill_dir.mkdir() (skill_dir / "skill.toml").write_text( textwrap.dedent("""\ [skill] name = "my_skill" description = "A test skill" [[skill.steps]] tool_name = "echo" output_key = "x" """) ) with patch( "openjarvis.cli.skill_cmd._get_skill_paths", return_value=[tmp_path], ): result = CliRunner().invoke(cli, ["skill", "list"]) assert result.exit_code == 0 assert "my_skill" in result.output def test_skill_info_shows_details(self, tmp_path: Path) -> None: skill_dir = tmp_path / "info_skill" skill_dir.mkdir() (skill_dir / "skill.toml").write_text( textwrap.dedent("""\ [skill] name = "info_skill" description = "Detailed skill" author = "test_author" tags = ["research", "test"] required_capabilities = ["network:fetch"] [[skill.steps]] tool_name = "echo" output_key = "x" """) ) with patch( "openjarvis.cli.skill_cmd._get_skill_paths", return_value=[tmp_path], ): result = CliRunner().invoke(cli, ["skill", "info", "info_skill"]) assert result.exit_code == 0 assert "info_skill" in result.output assert "test_author" in result.output class TestSkillRemoveCommand: def test_remove_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "remove", "--help"]) assert result.exit_code == 0 def test_remove_missing_skill(self, tmp_path: Path) -> None: with patch( "openjarvis.cli.skill_cmd._get_skill_paths", return_value=[tmp_path], ): result = CliRunner().invoke(cli, ["skill", "remove", "ghost", "--yes"]) assert result.exit_code != 0 assert "no installed skill" in result.output.lower() def test_remove_deletes_directory(self, tmp_path: Path) -> None: skill_dir = tmp_path / "to_remove" skill_dir.mkdir() (skill_dir / "skill.toml").write_text( textwrap.dedent("""\ [skill] name = "to_remove" description = "doomed" [[skill.steps]] tool_name = "echo" output_key = "x" """) ) with patch( "openjarvis.cli.skill_cmd._get_skill_paths", return_value=[tmp_path], ): result = CliRunner().invoke(cli, ["skill", "remove", "to_remove", "--yes"]) assert result.exit_code == 0, result.output assert "Removed" in result.output assert not skill_dir.exists() class TestSkillSearchCommand: def test_search_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "search", "--help"]) assert result.exit_code == 0 def test_search_no_sources_configured(self) -> None: from openjarvis.core.config import JarvisConfig, SkillsConfig cfg = JarvisConfig() cfg.skills = SkillsConfig(sources=[]) with patch("openjarvis.cli.skill_cmd.load_config", return_value=cfg): result = CliRunner().invoke(cli, ["skill", "search", "anything"]) assert result.exit_code != 0 assert "no skill sources" in result.output.lower() def test_search_filters_results(self) -> None: from pathlib import Path as _P from openjarvis.core.config import ( JarvisConfig, SkillsConfig, SkillSourceConfig, ) from openjarvis.skills.sources.base import ResolvedSkill class _FakeResolver: def sync(self) -> None: return None def list_skills(self): return [ ResolvedSkill( name="apple-notes", source="hermes", path=_P("/tmp"), category="apple", description="Take notes on macOS", commit="abc", ), ResolvedSkill( name="github-prs", source="hermes", path=_P("/tmp"), category="dev", description="List pull requests", commit="def", ), ] cfg = JarvisConfig() cfg.skills = SkillsConfig(sources=[SkillSourceConfig(source="hermes")]) with patch("openjarvis.cli.skill_cmd.load_config", return_value=cfg): with patch( "openjarvis.cli.skill_cmd._get_resolver", return_value=_FakeResolver(), ): result = CliRunner().invoke(cli, ["skill", "search", "notes"]) assert result.exit_code == 0, result.output assert "apple-notes" in result.output assert "github-prs" not in result.output class TestSkillInstallCommand: def test_install_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "install", "--help"]) assert result.exit_code == 0 assert "source" in result.output.lower() def test_install_invalid_source_format(self) -> None: result = CliRunner().invoke(cli, ["skill", "install", "invalid"]) assert result.exit_code != 0 assert "format" in result.output.lower() or "source" in result.output.lower() class TestSkillSyncCommand: def test_sync_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "sync", "--help"]) assert result.exit_code == 0 class TestSkillSourcesCommand: def test_sources_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "sources", "--help"]) assert result.exit_code == 0 def test_sources_lists_configured_sources(self, tmp_path: Path) -> None: from unittest.mock import patch from openjarvis.core.config import ( JarvisConfig, SkillsConfig, SkillSourceConfig, ) cfg = JarvisConfig() cfg.skills = SkillsConfig( sources=[ SkillSourceConfig(source="hermes"), SkillSourceConfig(source="openclaw"), ] ) with patch("openjarvis.cli.skill_cmd.load_config", return_value=cfg): result = CliRunner().invoke(cli, ["skill", "sources"]) assert result.exit_code == 0 assert "hermes" in result.output assert "openclaw" in result.output class TestSkillDiscoverCommand: def test_discover_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "discover", "--help"]) assert result.exit_code == 0 def test_discover_dry_run_no_traces(self, tmp_path: Path) -> None: from unittest.mock import patch # Patch the trace store to return nothing class _EmptyStore: def list_traces(self, *, limit: int = 100, **kwargs): return [] with patch( "openjarvis.cli.skill_cmd._get_trace_store", return_value=_EmptyStore(), ): result = CliRunner().invoke(cli, ["skill", "discover", "--dry-run"]) assert result.exit_code == 0 def test_discover_writes_when_not_dry_run(self, tmp_path: Path) -> None: from unittest.mock import patch from openjarvis.core.types import StepType, Trace, TraceStep def _trace(): return Trace( query="q", steps=[ TraceStep( step_type=StepType.TOOL_CALL, timestamp=0.0, input={"tool": "web_search", "arguments": {}}, output={"success": True, "result": "ok"}, metadata={}, ), TraceStep( step_type=StepType.TOOL_CALL, timestamp=0.0, input={"tool": "calculator", "arguments": {}}, output={"success": True, "result": "ok"}, metadata={}, ), ], outcome="success", feedback=1.0, ) class _Store: def list_traces(self, *, limit: int = 100, **kwargs): return [_trace() for _ in range(5)] output_dir = tmp_path / "discovered" with patch( "openjarvis.cli.skill_cmd._get_trace_store", return_value=_Store(), ): with patch( "openjarvis.cli.skill_cmd._get_discovered_dir", return_value=output_dir, ): result = CliRunner().invoke( cli, ["skill", "discover", "--min-frequency", "3"] ) assert result.exit_code == 0 assert output_dir.exists() # At least one manifest should have been written assert any(output_dir.rglob("skill.toml")) class TestSkillShowOverlayCommand: def test_show_overlay_help(self) -> None: result = CliRunner().invoke(cli, ["skill", "show-overlay", "--help"]) assert result.exit_code == 0 def test_show_overlay_missing(self, tmp_path: Path) -> None: from unittest.mock import patch with patch( "openjarvis.cli.skill_cmd._get_overlay_dir", return_value=tmp_path / "no-such", ): result = CliRunner().invoke(cli, ["skill", "show-overlay", "nonexistent"]) assert result.exit_code != 0 def test_show_overlay_displays_optimized(self, tmp_path: Path) -> None: from unittest.mock import patch from openjarvis.skills.overlay import SkillOverlay, write_overlay write_overlay( SkillOverlay( skill_name="research-skill", optimizer="dspy", optimized_at="2026-04-08T14:30:00Z", trace_count=42, description="Better description", few_shot=[{"input": "q", "output": "a"}], ), tmp_path, ) with patch( "openjarvis.cli.skill_cmd._get_overlay_dir", return_value=tmp_path, ): result = CliRunner().invoke( cli, ["skill", "show-overlay", "research-skill"] ) assert result.exit_code == 0 assert "research-skill" in result.output assert "Better description" in result.output assert "42" in result.output assert "dspy" in result.output