smarttools/tests/test_cli.py

356 lines
12 KiB
Python

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