356 lines
12 KiB
Python
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
|