Second batch of comprehensive linting fixes: Unused Arguments/Variables (136 errors): - ARG002/ARG001 (91 errors): Prefixed unused method/function arguments with '_' - Interface methods in adaptors (base.py, gemini.py, markdown.py) - AST analyzer methods maintaining signatures (code_analyzer.py) - Test fixtures and hooks (conftest.py) - Added noqa: ARG001/ARG002 for pytest hooks requiring exact names - F841 (45 errors): Prefixed unused local variables with '_' - Tuple unpacking where some values aren't needed - Variables assigned but not referenced Loop & Boolean Quality (28 errors): - B007 (18 errors): Prefixed unused loop control variables with '_' - enumerate() loops where index not used - for-in loops where loop variable not referenced - E712 (10 errors): Simplified boolean comparisons - Changed '== True' to direct boolean check - Changed '== False' to 'not' expression - Improved test readability Code Quality (24 errors): - SIM201 (4 errors): Already fixed in previous commit - SIM118 (2 errors): Already fixed in previous commit - E741 (4 errors): Already fixed in previous commit - Config manager loop variable fix (1 error) All Tests Passing: - test_scraper_features.py: 42 passed - test_integration.py: 51 passed - test_architecture_scenarios.py: 11 passed - test_real_world_fastmcp.py: 19 passed, 1 skipped Note: Some SIM errors (nested if, multiple with) remain unfixed as they would require non-trivial refactoring. Focus was on functional correctness. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
494 lines
19 KiB
Python
494 lines
19 KiB
Python
"""
|
|
Tests for install_agent CLI tool.
|
|
|
|
Tests cover:
|
|
- Agent path mapping and resolution
|
|
- Agent name validation with fuzzy matching
|
|
- Skill directory validation
|
|
- Installation to single agent
|
|
- Installation to all agents
|
|
- CLI interface
|
|
"""
|
|
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
# Add src to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from skill_seekers.cli.install_agent import (
|
|
get_agent_path,
|
|
get_available_agents,
|
|
install_to_agent,
|
|
install_to_all_agents,
|
|
main,
|
|
validate_agent_name,
|
|
validate_skill_directory,
|
|
)
|
|
|
|
|
|
class TestAgentPathMapping:
|
|
"""Test agent path resolution and mapping."""
|
|
|
|
def test_get_agent_path_home_expansion(self):
|
|
"""Test that ~ expands to home directory for global agents."""
|
|
# Test claude (global agent with ~)
|
|
path = get_agent_path("claude")
|
|
assert path.is_absolute()
|
|
assert ".claude" in str(path)
|
|
assert str(path).startswith(str(Path.home()))
|
|
|
|
def test_get_agent_path_project_relative(self):
|
|
"""Test that project-relative paths use current directory."""
|
|
# Test cursor (project-relative agent)
|
|
path = get_agent_path("cursor")
|
|
assert path.is_absolute()
|
|
assert ".cursor" in str(path)
|
|
# Should be relative to current directory
|
|
assert str(Path.cwd()) in str(path)
|
|
|
|
def test_get_agent_path_project_relative_with_custom_root(self):
|
|
"""Test project-relative paths with custom project root."""
|
|
custom_root = Path("/tmp/test-project")
|
|
path = get_agent_path("cursor", project_root=custom_root)
|
|
assert path.is_absolute()
|
|
assert str(custom_root) in str(path)
|
|
assert ".cursor" in str(path)
|
|
|
|
def test_get_agent_path_invalid_agent(self):
|
|
"""Test that invalid agent raises ValueError."""
|
|
with pytest.raises(ValueError, match="Unknown agent"):
|
|
get_agent_path("invalid_agent")
|
|
|
|
def test_get_available_agents(self):
|
|
"""Test that all 11 agents are listed."""
|
|
agents = get_available_agents()
|
|
assert len(agents) == 11
|
|
assert "claude" in agents
|
|
assert "cursor" in agents
|
|
assert "vscode" in agents
|
|
assert "amp" in agents
|
|
assert "goose" in agents
|
|
assert "neovate" in agents
|
|
assert sorted(agents) == agents # Should be sorted
|
|
|
|
def test_agent_path_case_insensitive(self):
|
|
"""Test that agent names are case-insensitive."""
|
|
path_lower = get_agent_path("claude")
|
|
path_upper = get_agent_path("CLAUDE")
|
|
path_mixed = get_agent_path("Claude")
|
|
assert path_lower == path_upper == path_mixed
|
|
|
|
|
|
class TestAgentNameValidation:
|
|
"""Test agent name validation and fuzzy matching."""
|
|
|
|
def test_validate_valid_agent(self):
|
|
"""Test that valid agent names pass validation."""
|
|
is_valid, error = validate_agent_name("claude")
|
|
assert is_valid is True
|
|
assert error is None
|
|
|
|
def test_validate_invalid_agent_suggests_similar(self):
|
|
"""Test that similar agent names are suggested for typos."""
|
|
is_valid, error = validate_agent_name("courser")
|
|
assert is_valid is False
|
|
assert "cursor" in error.lower() # Should suggest 'cursor'
|
|
|
|
def test_validate_special_all(self):
|
|
"""Test that 'all' is a valid special agent name."""
|
|
is_valid, error = validate_agent_name("all")
|
|
assert is_valid is True
|
|
assert error is None
|
|
|
|
def test_validate_case_insensitive(self):
|
|
"""Test that validation is case-insensitive."""
|
|
for name in ["Claude", "CLAUDE", "claude", "cLaUdE"]:
|
|
is_valid, error = validate_agent_name(name)
|
|
assert is_valid is True
|
|
assert error is None
|
|
|
|
def test_validate_shows_available_agents(self):
|
|
"""Test that error message shows available agents."""
|
|
is_valid, error = validate_agent_name("invalid")
|
|
assert is_valid is False
|
|
assert "available agents" in error.lower()
|
|
assert "claude" in error.lower()
|
|
assert "cursor" in error.lower()
|
|
|
|
|
|
class TestSkillDirectoryValidation:
|
|
"""Test skill directory validation."""
|
|
|
|
def test_validate_valid_skill_directory(self):
|
|
"""Test that valid skill directory passes validation."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
skill_dir = Path(tmpdir) / "test-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text("# Test Skill")
|
|
|
|
is_valid, error = validate_skill_directory(skill_dir)
|
|
assert is_valid is True
|
|
assert error is None
|
|
|
|
def test_validate_missing_directory(self):
|
|
"""Test that missing directory fails validation."""
|
|
skill_dir = Path("/nonexistent/directory")
|
|
is_valid, error = validate_skill_directory(skill_dir)
|
|
assert is_valid is False
|
|
assert "does not exist" in error
|
|
|
|
def test_validate_not_a_directory(self):
|
|
"""Test that file (not directory) fails validation."""
|
|
with tempfile.NamedTemporaryFile(delete=False) as tmpfile:
|
|
try:
|
|
is_valid, error = validate_skill_directory(Path(tmpfile.name))
|
|
assert is_valid is False
|
|
assert "not a directory" in error
|
|
finally:
|
|
Path(tmpfile.name).unlink()
|
|
|
|
def test_validate_missing_skill_md(self):
|
|
"""Test that directory without SKILL.md fails validation."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
skill_dir = Path(tmpdir) / "test-skill"
|
|
skill_dir.mkdir()
|
|
|
|
is_valid, error = validate_skill_directory(skill_dir)
|
|
assert is_valid is False
|
|
assert "SKILL.md not found" in error
|
|
|
|
|
|
class TestInstallToAgent:
|
|
"""Test installation to single agent."""
|
|
|
|
def setup_method(self):
|
|
"""Create test skill directory before each test."""
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.skill_dir = Path(self.tmpdir) / "test-skill"
|
|
self.skill_dir.mkdir()
|
|
|
|
# Create SKILL.md
|
|
(self.skill_dir / "SKILL.md").write_text("# Test Skill\n\nThis is a test skill.")
|
|
|
|
# Create references directory with files
|
|
refs_dir = self.skill_dir / "references"
|
|
refs_dir.mkdir()
|
|
(refs_dir / "index.md").write_text("# Index")
|
|
(refs_dir / "getting_started.md").write_text("# Getting Started")
|
|
|
|
# Create empty directories
|
|
(self.skill_dir / "scripts").mkdir()
|
|
(self.skill_dir / "assets").mkdir()
|
|
|
|
def teardown_method(self):
|
|
"""Clean up after each test."""
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def test_install_creates_skill_subdirectory(self):
|
|
"""Test that installation creates {agent_path}/{skill_name}/ directory."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
agent_path = Path(agent_tmpdir) / ".claude" / "skills"
|
|
|
|
with patch("skill_seekers.cli.install_agent.get_agent_path", return_value=agent_path):
|
|
success, message = install_to_agent(self.skill_dir, "claude", force=True)
|
|
|
|
assert success is True
|
|
target_path = agent_path / "test-skill"
|
|
assert target_path.exists()
|
|
assert target_path.is_dir()
|
|
|
|
def test_install_preserves_structure(self):
|
|
"""Test that installation preserves SKILL.md, references/, scripts/, assets/."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
agent_path = Path(agent_tmpdir) / ".claude" / "skills"
|
|
|
|
with patch("skill_seekers.cli.install_agent.get_agent_path", return_value=agent_path):
|
|
success, message = install_to_agent(self.skill_dir, "claude", force=True)
|
|
|
|
assert success is True
|
|
target_path = agent_path / "test-skill"
|
|
|
|
# Check structure
|
|
assert (target_path / "SKILL.md").exists()
|
|
assert (target_path / "references").exists()
|
|
assert (target_path / "references" / "index.md").exists()
|
|
assert (target_path / "references" / "getting_started.md").exists()
|
|
assert (target_path / "scripts").exists()
|
|
assert (target_path / "assets").exists()
|
|
|
|
def test_install_excludes_backups(self):
|
|
"""Test that .backup files are excluded from installation."""
|
|
# Create backup file
|
|
(self.skill_dir / "SKILL.md.backup").write_text("# Backup")
|
|
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
agent_path = Path(agent_tmpdir) / ".claude" / "skills"
|
|
|
|
with patch("skill_seekers.cli.install_agent.get_agent_path", return_value=agent_path):
|
|
success, message = install_to_agent(self.skill_dir, "claude", force=True)
|
|
|
|
assert success is True
|
|
target_path = agent_path / "test-skill"
|
|
|
|
# Backup should NOT be copied
|
|
assert not (target_path / "SKILL.md.backup").exists()
|
|
# Main file should be copied
|
|
assert (target_path / "SKILL.md").exists()
|
|
|
|
def test_install_existing_directory_no_force(self):
|
|
"""Test that existing directory without --force fails with clear message."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
agent_path = Path(agent_tmpdir) / ".claude" / "skills"
|
|
target_path = agent_path / "test-skill"
|
|
target_path.mkdir(parents=True)
|
|
|
|
with patch("skill_seekers.cli.install_agent.get_agent_path", return_value=agent_path):
|
|
success, message = install_to_agent(self.skill_dir, "claude", force=False)
|
|
|
|
assert success is False
|
|
assert "already installed" in message.lower()
|
|
assert "--force" in message
|
|
|
|
def test_install_existing_directory_with_force(self):
|
|
"""Test that existing directory with --force overwrites successfully."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
agent_path = Path(agent_tmpdir) / ".claude" / "skills"
|
|
target_path = agent_path / "test-skill"
|
|
target_path.mkdir(parents=True)
|
|
(target_path / "old_file.txt").write_text("old content")
|
|
|
|
with patch("skill_seekers.cli.install_agent.get_agent_path", return_value=agent_path):
|
|
success, message = install_to_agent(self.skill_dir, "claude", force=True)
|
|
|
|
assert success is True
|
|
# Old file should be gone
|
|
assert not (target_path / "old_file.txt").exists()
|
|
# New structure should exist
|
|
assert (target_path / "SKILL.md").exists()
|
|
|
|
def test_install_invalid_skill_directory(self):
|
|
"""Test that installation fails for invalid skill directory."""
|
|
invalid_dir = Path("/nonexistent/directory")
|
|
|
|
success, message = install_to_agent(invalid_dir, "claude")
|
|
|
|
assert success is False
|
|
assert "does not exist" in message
|
|
|
|
def test_install_missing_skill_md(self):
|
|
"""Test that installation fails if SKILL.md is missing."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
bad_skill_dir = Path(tmpdir) / "bad-skill"
|
|
bad_skill_dir.mkdir()
|
|
|
|
success, message = install_to_agent(bad_skill_dir, "claude")
|
|
|
|
assert success is False
|
|
assert "SKILL.md not found" in message
|
|
|
|
def test_install_dry_run(self):
|
|
"""Test that dry-run mode previews without making changes."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
agent_path = Path(agent_tmpdir) / ".claude" / "skills"
|
|
|
|
with patch("skill_seekers.cli.install_agent.get_agent_path", return_value=agent_path):
|
|
success, message = install_to_agent(self.skill_dir, "claude", dry_run=True)
|
|
|
|
assert success is True
|
|
assert "DRY RUN" in message
|
|
# Directory should NOT be created
|
|
assert not (agent_path / "test-skill").exists()
|
|
|
|
|
|
class TestInstallToAllAgents:
|
|
"""Test installation to all agents."""
|
|
|
|
def setup_method(self):
|
|
"""Create test skill directory before each test."""
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.skill_dir = Path(self.tmpdir) / "test-skill"
|
|
self.skill_dir.mkdir()
|
|
(self.skill_dir / "SKILL.md").write_text("# Test Skill")
|
|
(self.skill_dir / "references").mkdir()
|
|
|
|
def teardown_method(self):
|
|
"""Clean up after each test."""
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def test_install_to_all_success(self):
|
|
"""Test that install_to_all_agents attempts all 11 agents."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
|
|
def mock_get_agent_path(agent_name, _project_root=None):
|
|
return Path(agent_tmpdir) / f".{agent_name}" / "skills"
|
|
|
|
with patch(
|
|
"skill_seekers.cli.install_agent.get_agent_path", side_effect=mock_get_agent_path
|
|
):
|
|
results = install_to_all_agents(self.skill_dir, force=True)
|
|
|
|
assert len(results) == 11
|
|
assert "claude" in results
|
|
assert "cursor" in results
|
|
|
|
def test_install_to_all_partial_success(self):
|
|
"""Test that install_to_all collects both successes and failures."""
|
|
# This is hard to test without complex mocking, so we'll do dry-run
|
|
results = install_to_all_agents(self.skill_dir, dry_run=True)
|
|
|
|
# All should succeed in dry-run mode
|
|
assert len(results) == 11
|
|
for _agent_name, (success, message) in results.items():
|
|
assert success is True
|
|
assert "DRY RUN" in message
|
|
|
|
def test_install_to_all_with_force(self):
|
|
"""Test that install_to_all respects force flag."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
# Create existing directories for all agents
|
|
for agent in get_available_agents():
|
|
agent_dir = Path(agent_tmpdir) / f".{agent}" / "skills" / "test-skill"
|
|
agent_dir.mkdir(parents=True)
|
|
|
|
def mock_get_agent_path(agent_name, _project_root=None):
|
|
return Path(agent_tmpdir) / f".{agent_name}" / "skills"
|
|
|
|
with patch(
|
|
"skill_seekers.cli.install_agent.get_agent_path", side_effect=mock_get_agent_path
|
|
):
|
|
# Without force - should fail
|
|
results_no_force = install_to_all_agents(self.skill_dir, force=False)
|
|
# All should fail because directories exist
|
|
for _agent_name, (success, message) in results_no_force.items():
|
|
assert success is False
|
|
assert "already installed" in message.lower()
|
|
|
|
# With force - should succeed
|
|
results_with_force = install_to_all_agents(self.skill_dir, force=True)
|
|
for _agent_name, (success, message) in results_with_force.items():
|
|
assert success is True
|
|
|
|
def test_install_to_all_returns_results(self):
|
|
"""Test that install_to_all returns dict with all results."""
|
|
results = install_to_all_agents(self.skill_dir, dry_run=True)
|
|
|
|
assert isinstance(results, dict)
|
|
assert len(results) == 11
|
|
|
|
for agent_name, (success, message) in results.items():
|
|
assert isinstance(success, bool)
|
|
assert isinstance(message, str)
|
|
assert agent_name in get_available_agents()
|
|
|
|
|
|
class TestInstallAgentCLI:
|
|
"""Test CLI interface."""
|
|
|
|
def setup_method(self):
|
|
"""Create test skill directory before each test."""
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.skill_dir = Path(self.tmpdir) / "test-skill"
|
|
self.skill_dir.mkdir()
|
|
(self.skill_dir / "SKILL.md").write_text("# Test Skill")
|
|
(self.skill_dir / "references").mkdir()
|
|
|
|
def teardown_method(self):
|
|
"""Clean up after each test."""
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def test_cli_help_output(self):
|
|
"""Test that --help shows usage information."""
|
|
with (
|
|
pytest.raises(SystemExit) as exc_info,
|
|
patch("sys.argv", ["install_agent.py", "--help"]),
|
|
):
|
|
main()
|
|
|
|
# --help exits with code 0
|
|
assert exc_info.value.code == 0
|
|
|
|
def test_cli_requires_agent_flag(self):
|
|
"""Test that CLI fails without --agent flag."""
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
with patch("sys.argv", ["install_agent.py", str(self.skill_dir)]):
|
|
main()
|
|
|
|
# Missing required argument exits with code 2
|
|
assert exc_info.value.code == 2
|
|
|
|
def test_cli_dry_run(self):
|
|
"""Test that --dry-run flag works correctly."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
|
|
def mock_get_agent_path(agent_name, _project_root=None):
|
|
return Path(agent_tmpdir) / f".{agent_name}" / "skills"
|
|
|
|
with patch(
|
|
"skill_seekers.cli.install_agent.get_agent_path", side_effect=mock_get_agent_path
|
|
):
|
|
with patch(
|
|
"sys.argv",
|
|
["install_agent.py", str(self.skill_dir), "--agent", "claude", "--dry-run"],
|
|
):
|
|
exit_code = main()
|
|
|
|
assert exit_code == 0
|
|
# Directory should NOT be created
|
|
assert not (Path(agent_tmpdir) / ".claude" / "skills" / "test-skill").exists()
|
|
|
|
def test_cli_integration(self):
|
|
"""Test end-to-end CLI execution."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
|
|
def mock_get_agent_path(agent_name, _project_root=None):
|
|
return Path(agent_tmpdir) / f".{agent_name}" / "skills"
|
|
|
|
with patch(
|
|
"skill_seekers.cli.install_agent.get_agent_path", side_effect=mock_get_agent_path
|
|
):
|
|
with patch(
|
|
"sys.argv",
|
|
["install_agent.py", str(self.skill_dir), "--agent", "claude", "--force"],
|
|
):
|
|
exit_code = main()
|
|
|
|
assert exit_code == 0
|
|
# Directory should be created
|
|
target = Path(agent_tmpdir) / ".claude" / "skills" / "test-skill"
|
|
assert target.exists()
|
|
assert (target / "SKILL.md").exists()
|
|
|
|
def test_cli_install_to_all(self):
|
|
"""Test CLI with --agent all."""
|
|
with tempfile.TemporaryDirectory() as agent_tmpdir:
|
|
|
|
def mock_get_agent_path(agent_name, _project_root=None):
|
|
return Path(agent_tmpdir) / f".{agent_name}" / "skills"
|
|
|
|
with patch(
|
|
"skill_seekers.cli.install_agent.get_agent_path", side_effect=mock_get_agent_path
|
|
):
|
|
with patch(
|
|
"sys.argv",
|
|
["install_agent.py", str(self.skill_dir), "--agent", "all", "--force"],
|
|
):
|
|
exit_code = main()
|
|
|
|
assert exit_code == 0
|
|
|
|
# All agent directories should be created
|
|
for agent in get_available_agents():
|
|
target = Path(agent_tmpdir) / f".{agent}" / "skills" / "test-skill"
|
|
assert target.exists(), f"Directory not created for {agent}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run tests with pytest
|
|
pytest.main([__file__, "-v"])
|