"""Tests for CLI commands.""" import tempfile from pathlib import Path from unittest.mock import patch, MagicMock from io import StringIO import pytest from smarttools.cli import main from smarttools.tool import Tool, ToolArgument, PromptStep class TestCLIBasics: """Basic CLI tests.""" def test_help_flag(self, capsys): """--help should show usage.""" with pytest.raises(SystemExit) as exc_info: with patch('sys.argv', ['smarttools', '--help']): main() assert exc_info.value.code == 0 captured = capsys.readouterr() assert 'usage' in captured.out.lower() or 'smarttools' in captured.out.lower() def test_version_flag(self, capsys): """--version should show version.""" with pytest.raises(SystemExit) as exc_info: with patch('sys.argv', ['smarttools', '--version']): main() assert exc_info.value.code == 0 class TestListCommand: """Tests for 'smarttools list' command.""" @pytest.fixture def temp_tools_dir(self, tmp_path): with patch('smarttools.tool.TOOLS_DIR', tmp_path / ".smarttools"): with patch('smarttools.tool.BIN_DIR', tmp_path / ".local" / "bin"): yield tmp_path def test_list_empty(self, temp_tools_dir, capsys): """List with no tools should show message.""" with patch('sys.argv', ['smarttools', 'list']): result = main() assert result == 0 captured = capsys.readouterr() assert 'no tools' in captured.out.lower() or '0' in captured.out def test_list_with_tools(self, temp_tools_dir, capsys): """List should show available tools.""" from smarttools.tool import save_tool save_tool(Tool(name="test-tool", description="A test tool")) save_tool(Tool(name="another-tool", description="Another tool")) with patch('sys.argv', ['smarttools', 'list']): result = main() assert result == 0 captured = capsys.readouterr() assert 'test-tool' in captured.out assert 'another-tool' in captured.out class TestCreateCommand: """Tests for 'smarttools create' command.""" @pytest.fixture def temp_tools_dir(self, tmp_path): with patch('smarttools.tool.TOOLS_DIR', tmp_path / ".smarttools"): with patch('smarttools.tool.BIN_DIR', tmp_path / ".local" / "bin"): yield tmp_path def test_create_minimal(self, temp_tools_dir, capsys): """Create a minimal tool.""" with patch('sys.argv', ['smarttools', 'create', 'my-tool']): result = main() assert result == 0 from smarttools.tool import tool_exists assert tool_exists('my-tool') def test_create_with_description(self, temp_tools_dir): """Create tool with description.""" with patch('sys.argv', ['smarttools', 'create', 'described-tool', '-d', 'A helpful description']): result = main() assert result == 0 from smarttools.tool import load_tool tool = load_tool('described-tool') assert tool.description == 'A helpful description' def test_create_duplicate_fails(self, temp_tools_dir, capsys): """Creating duplicate tool should fail.""" from smarttools.tool import save_tool save_tool(Tool(name="existing")) with patch('sys.argv', ['smarttools', 'create', 'existing']): result = main() assert result != 0 captured = capsys.readouterr() assert 'exists' in captured.err.lower() or 'exists' in captured.out.lower() @pytest.mark.skip(reason="CLI does not yet validate tool names - potential enhancement") def test_create_invalid_name(self, temp_tools_dir, capsys): """Invalid tool name should fail.""" with patch('sys.argv', ['smarttools', 'create', 'invalid/name']): result = main() assert result != 0 class TestDeleteCommand: """Tests for 'smarttools delete' command.""" @pytest.fixture def temp_tools_dir(self, tmp_path): with patch('smarttools.tool.TOOLS_DIR', tmp_path / ".smarttools"): with patch('smarttools.tool.BIN_DIR', tmp_path / ".local" / "bin"): yield tmp_path def test_delete_existing(self, temp_tools_dir, capsys): """Delete an existing tool.""" from smarttools.tool import save_tool, tool_exists save_tool(Tool(name="to-delete")) assert tool_exists("to-delete") with patch('sys.argv', ['smarttools', 'delete', 'to-delete', '-f']): result = main() assert result == 0 assert not tool_exists("to-delete") def test_delete_nonexistent(self, temp_tools_dir, capsys): """Deleting nonexistent tool should fail.""" with patch('sys.argv', ['smarttools', 'delete', 'nonexistent', '-f']): result = main() assert result != 0 class TestRunCommand: """Tests for 'smarttools run' command.""" @pytest.fixture def temp_tools_dir(self, tmp_path): with patch('smarttools.tool.TOOLS_DIR', tmp_path / ".smarttools"): with patch('smarttools.tool.BIN_DIR', tmp_path / ".local" / "bin"): yield tmp_path def test_run_simple_tool(self, temp_tools_dir, capsys): """Run a simple tool without AI calls.""" from smarttools.tool import save_tool tool = Tool( name="echo", output="Echo: {input}" ) save_tool(tool) with patch('sys.argv', ['smarttools', 'run', 'echo']): with patch('sys.stdin', StringIO("Hello")): with patch('sys.stdin.isatty', return_value=False): result = main() assert result == 0 captured = capsys.readouterr() assert 'Echo: Hello' in captured.out def test_run_with_mock_provider(self, temp_tools_dir, capsys): """Run tool with mock provider.""" from smarttools.tool import save_tool tool = Tool( name="summarize", steps=[ PromptStep(prompt="Summarize: {input}", provider="mock", output_var="summary") ], output="{summary}" ) save_tool(tool) with patch('sys.argv', ['smarttools', 'run', 'summarize']): with patch('sys.stdin', StringIO("Some text")): with patch('sys.stdin.isatty', return_value=False): result = main() assert result == 0 captured = capsys.readouterr() assert 'MOCK' in captured.out def test_run_nonexistent_tool(self, temp_tools_dir, capsys): """Running nonexistent tool should fail.""" with patch('sys.argv', ['smarttools', 'run', 'nonexistent']): result = main() assert result != 0 class TestTestCommand: """Tests for 'smarttools test' command.""" @pytest.fixture def temp_tools_dir(self, tmp_path): with patch('smarttools.tool.TOOLS_DIR', tmp_path / ".smarttools"): with patch('smarttools.tool.BIN_DIR', tmp_path / ".local" / "bin"): yield tmp_path def test_test_tool(self, temp_tools_dir, capsys): """Test command should run with mock provider.""" from smarttools.tool import save_tool tool = Tool( name="test-me", steps=[ PromptStep(prompt="Test: {input}", provider="claude", output_var="result") ], output="{result}" ) save_tool(tool) # Provide stdin input for the test command with patch('sys.argv', ['smarttools', 'test', 'test-me']): with patch('sys.stdin', StringIO("test input")): result = main() # Test command uses mock, so should succeed assert result == 0 captured = capsys.readouterr() assert 'MOCK' in captured.out or 'mock' in captured.out.lower() class TestProvidersCommand: """Tests for 'smarttools providers' command.""" @pytest.fixture def temp_providers_file(self, tmp_path): providers_file = tmp_path / ".smarttools" / "providers.yaml" with patch('smarttools.providers.PROVIDERS_FILE', providers_file): yield providers_file def test_providers_list(self, temp_providers_file, capsys): """List providers.""" with patch('sys.argv', ['smarttools', 'providers']): result = main() assert result == 0 captured = capsys.readouterr() # Should show some default providers assert 'mock' in captured.out.lower() or 'claude' in captured.out.lower() def test_providers_add(self, temp_providers_file, capsys): """Add a custom provider.""" with patch('sys.argv', ['smarttools', 'providers', 'add', 'custom', 'my-ai --prompt']): result = main() assert result == 0 from smarttools.providers import get_provider provider = get_provider('custom') assert provider is not None assert provider.command == 'my-ai --prompt' def test_providers_remove(self, temp_providers_file, capsys): """Remove a provider.""" from smarttools.providers import add_provider, Provider add_provider(Provider('removeme', 'cmd')) with patch('sys.argv', ['smarttools', 'providers', 'remove', 'removeme']): result = main() assert result == 0 from smarttools.providers import get_provider assert get_provider('removeme') is None class TestRefreshCommand: """Tests for 'smarttools refresh' command.""" @pytest.fixture def temp_tools_dir(self, tmp_path): with patch('smarttools.tool.TOOLS_DIR', tmp_path / ".smarttools"): with patch('smarttools.tool.BIN_DIR', tmp_path / ".local" / "bin"): yield tmp_path def test_refresh_creates_wrappers(self, temp_tools_dir, capsys): """Refresh should create wrapper scripts.""" from smarttools.tool import save_tool, get_bin_dir save_tool(Tool(name="wrapper-test")) with patch('sys.argv', ['smarttools', 'refresh']): result = main() assert result == 0 wrapper = get_bin_dir() / "wrapper-test" assert wrapper.exists() class TestDocsCommand: """Tests for 'smarttools docs' command.""" @pytest.fixture def temp_tools_dir(self, tmp_path): with patch('smarttools.tool.TOOLS_DIR', tmp_path / ".smarttools"): with patch('smarttools.tool.BIN_DIR', tmp_path / ".local" / "bin"): yield tmp_path def test_docs_for_tool_with_readme(self, temp_tools_dir, capsys): """Docs should show README content when it exists.""" from smarttools.tool import save_tool, get_tools_dir tool = Tool( name="documented", description="A well-documented tool", ) save_tool(tool) # Create a README.md for the tool readme_path = get_tools_dir() / "documented" / "README.md" readme_path.write_text("# Documented Tool\n\nThis is the documentation.") with patch('sys.argv', ['smarttools', 'docs', 'documented']): result = main() assert result == 0 captured = capsys.readouterr() assert 'Documented Tool' in captured.out assert 'documentation' in captured.out.lower() def test_docs_no_readme(self, temp_tools_dir, capsys): """Docs without README should prompt to create one.""" from smarttools.tool import save_tool tool = Tool(name="no-docs") save_tool(tool) with patch('sys.argv', ['smarttools', 'docs', 'no-docs']): result = main() assert result == 1 # Returns 1 when no README captured = capsys.readouterr() assert 'No documentation' in captured.out or '--edit' in captured.out