AI Agent Factory with Claude Code Subagents

This commit is contained in:
Cole Medin
2025-08-22 21:01:17 -05:00
parent 4e1240a0b3
commit 8d9f46ecfa
104 changed files with 24521 additions and 0 deletions

View File

@@ -0,0 +1,665 @@
"""Test CLI functionality."""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
import asyncio
from click.testing import CliRunner
from rich.console import Console
import sys
from ..cli import cli, search_cmd, interactive, info, display_results, display_welcome, interactive_mode
from ..agent import SearchResponse
class TestCLICommands:
"""Test CLI command functionality."""
def test_cli_without_subcommand(self):
"""Test CLI runs interactive mode when no subcommand provided."""
runner = CliRunner()
with patch('..cli.interactive_mode') as mock_interactive:
mock_interactive.return_value = asyncio.run(asyncio.sleep(0)) # Mock async function
result = runner.invoke(cli, [], input='\n')
# Should attempt to run interactive mode
assert result.exit_code == 0 or 'KeyboardInterrupt' in str(result.exception)
def test_search_command_basic(self):
"""Test basic search command functionality."""
runner = CliRunner()
mock_response = SearchResponse(
summary="Test search results found",
key_findings=["Finding 1", "Finding 2"],
sources=["Source 1", "Source 2"],
search_strategy="semantic",
result_count=2
)
with patch('..cli.search') as mock_search:
mock_search.return_value = mock_response
result = runner.invoke(search_cmd, [
'--query', 'test query',
'--type', 'semantic',
'--count', '5'
])
# Should complete successfully
assert result.exit_code == 0
mock_search.assert_called_once()
# Verify search was called with correct parameters
call_args = mock_search.call_args
assert call_args[1]['query'] == 'test query'
assert call_args[1]['search_type'] == 'semantic'
assert call_args[1]['match_count'] == 5
def test_search_command_with_text_weight(self):
"""Test search command with text weight parameter."""
runner = CliRunner()
mock_response = SearchResponse(
summary="Hybrid search results",
key_findings=[],
sources=[],
search_strategy="hybrid",
result_count=10
)
with patch('..cli.search') as mock_search:
mock_search.return_value = mock_response
result = runner.invoke(search_cmd, [
'--query', 'test query',
'--type', 'hybrid',
'--text-weight', '0.7'
])
assert result.exit_code == 0
call_args = mock_search.call_args
assert call_args[1]['text_weight'] == 0.7
def test_search_command_error_handling(self):
"""Test search command handles errors gracefully."""
runner = CliRunner()
with patch('..cli.search') as mock_search:
mock_search.side_effect = Exception("Search failed")
result = runner.invoke(search_cmd, [
'--query', 'test query'
])
# Should exit with error code 1
assert result.exit_code == 1
assert "Error:" in result.output
assert "Search failed" in result.output
def test_interactive_command(self):
"""Test interactive command invokes interactive mode."""
runner = CliRunner()
with patch('..cli.interactive_mode') as mock_interactive:
mock_interactive.return_value = asyncio.run(asyncio.sleep(0))
result = runner.invoke(interactive, [])
# Should attempt to run interactive mode
assert result.exit_code == 0 or 'KeyboardInterrupt' in str(result.exception)
def test_info_command_success(self):
"""Test info command displays system information."""
runner = CliRunner()
mock_settings = MagicMock()
mock_settings.llm_model = "gpt-4o-mini"
mock_settings.embedding_model = "text-embedding-3-small"
mock_settings.embedding_dimension = 1536
mock_settings.default_match_count = 10
mock_settings.max_match_count = 50
mock_settings.default_text_weight = 0.3
mock_settings.db_pool_min_size = 10
mock_settings.db_pool_max_size = 20
with patch('..cli.load_settings', return_value=mock_settings):
result = runner.invoke(info, [])
assert result.exit_code == 0
assert "System Configuration" in result.output
assert "gpt-4o-mini" in result.output
assert "text-embedding-3-small" in result.output
def test_info_command_error_handling(self):
"""Test info command handles settings loading errors."""
runner = CliRunner()
with patch('..cli.load_settings') as mock_load_settings:
mock_load_settings.side_effect = Exception("Settings load failed")
result = runner.invoke(info, [])
assert result.exit_code == 1
assert "Error loading settings:" in result.output
assert "Settings load failed" in result.output
class TestDisplayFunctions:
"""Test CLI display functions."""
def test_display_welcome(self, capsys):
"""Test welcome message display."""
console = Console(file=sys.stdout, force_terminal=False)
with patch('..cli.console', console):
display_welcome()
captured = capsys.readouterr()
assert "Semantic Search Agent" in captured.out
assert "Welcome" in captured.out
assert "search" in captured.out.lower()
assert "interactive" in captured.out.lower()
def test_display_results_basic(self, capsys):
"""Test basic results display."""
console = Console(file=sys.stdout, force_terminal=False)
response = {
'summary': 'This is a test summary of the search results.',
'key_findings': ['Finding 1', 'Finding 2', 'Finding 3'],
'sources': [
{'title': 'Document 1', 'source': 'doc1.pdf'},
{'title': 'Document 2', 'source': 'doc2.pdf'}
],
'search_strategy': 'hybrid',
'result_count': 10
}
with patch('..cli.console', console):
display_results(response)
captured = capsys.readouterr()
assert "Summary:" in captured.out
assert "This is a test summary" in captured.out
assert "Key Findings:" in captured.out
assert "Finding 1" in captured.out
assert "Sources:" in captured.out
assert "Document 1" in captured.out
assert "Search Strategy: hybrid" in captured.out
assert "Results Found: 10" in captured.out
def test_display_results_minimal(self, capsys):
"""Test results display with minimal data."""
console = Console(file=sys.stdout, force_terminal=False)
response = {
'summary': 'Minimal response',
'search_strategy': 'semantic',
'result_count': 0
}
with patch('..cli.console', console):
display_results(response)
captured = capsys.readouterr()
assert "Summary:" in captured.out
assert "Minimal response" in captured.out
assert "Search Strategy: semantic" in captured.out
assert "Results Found: 0" in captured.out
def test_display_results_no_summary(self, capsys):
"""Test results display when summary is missing."""
console = Console(file=sys.stdout, force_terminal=False)
response = {
'search_strategy': 'auto',
'result_count': 5
}
with patch('..cli.console', console):
display_results(response)
captured = capsys.readouterr()
assert "Summary:" in captured.out
assert "No summary available" in captured.out
assert "Search Strategy: auto" in captured.out
class TestInteractiveMode:
"""Test interactive mode functionality."""
@pytest.mark.asyncio
async def test_interactive_mode_initialization(self):
"""Test interactive mode initializes properly."""
with patch('..cli.interactive_search') as mock_interactive_search:
with patch('..cli.display_welcome') as mock_display_welcome:
with patch('..cli.Prompt.ask') as mock_prompt:
with patch('..cli.Confirm.ask') as mock_confirm:
mock_deps = AsyncMock()
mock_interactive_search.return_value = mock_deps
mock_prompt.side_effect = ['test query', 'exit']
mock_confirm.return_value = True
await interactive_mode()
mock_display_welcome.assert_called_once()
mock_interactive_search.assert_called_once()
@pytest.mark.asyncio
async def test_interactive_mode_search_query(self):
"""Test interactive mode handles search queries."""
mock_response = SearchResponse(
summary="Interactive search results",
key_findings=["Finding 1"],
sources=["Source 1"],
search_strategy="auto",
result_count=1
)
with patch('..cli.interactive_search') as mock_interactive_search:
with patch('..cli.display_welcome'):
with patch('..cli.display_results') as mock_display_results:
with patch('..cli.search') as mock_search:
with patch('..cli.Prompt.ask') as mock_prompt:
with patch('..cli.Confirm.ask') as mock_confirm:
mock_deps = AsyncMock()
mock_interactive_search.return_value = mock_deps
mock_search.return_value = mock_response
mock_prompt.side_effect = ['Python tutorial', 'exit']
mock_confirm.return_value = True
await interactive_mode()
# Should perform search
mock_search.assert_called()
call_args = mock_search.call_args
assert call_args[1]['query'] == 'Python tutorial'
# Should display results
mock_display_results.assert_called()
@pytest.mark.asyncio
async def test_interactive_mode_help_command(self):
"""Test interactive mode handles help command."""
with patch('..cli.interactive_search') as mock_interactive_search:
with patch('..cli.display_welcome') as mock_display_welcome:
with patch('..cli.Prompt.ask') as mock_prompt:
with patch('..cli.Confirm.ask') as mock_confirm:
mock_deps = AsyncMock()
mock_interactive_search.return_value = mock_deps
mock_prompt.side_effect = ['help', 'exit']
mock_confirm.return_value = True
await interactive_mode()
# Should display welcome twice (initial + help)
assert mock_display_welcome.call_count == 2
@pytest.mark.asyncio
async def test_interactive_mode_clear_command(self):
"""Test interactive mode handles clear command."""
with patch('..cli.interactive_search') as mock_interactive_search:
with patch('..cli.display_welcome'):
with patch('..cli.console') as mock_console:
with patch('..cli.Prompt.ask') as mock_prompt:
with patch('..cli.Confirm.ask') as mock_confirm:
mock_deps = AsyncMock()
mock_interactive_search.return_value = mock_deps
mock_prompt.side_effect = ['clear', 'exit']
mock_confirm.return_value = True
await interactive_mode()
# Should clear console
mock_console.clear.assert_called_once()
@pytest.mark.asyncio
async def test_interactive_mode_set_preference(self):
"""Test interactive mode handles preference setting."""
with patch('..cli.interactive_search') as mock_interactive_search:
with patch('..cli.display_welcome'):
with patch('..cli.Prompt.ask') as mock_prompt:
with patch('..cli.Confirm.ask') as mock_confirm:
with patch('..cli.console') as mock_console:
mock_deps = AsyncMock()
mock_interactive_search.return_value = mock_deps
mock_prompt.side_effect = ['set search_type=semantic', 'exit']
mock_confirm.return_value = True
await interactive_mode()
# Should set preference on deps
mock_deps.set_user_preference.assert_called_once_with('search_type', 'semantic')
@pytest.mark.asyncio
async def test_interactive_mode_invalid_set_command(self):
"""Test interactive mode handles invalid set commands."""
with patch('..cli.interactive_search') as mock_interactive_search:
with patch('..cli.display_welcome'):
with patch('..cli.Prompt.ask') as mock_prompt:
with patch('..cli.Confirm.ask') as mock_confirm:
with patch('..cli.console') as mock_console:
mock_deps = AsyncMock()
mock_interactive_search.return_value = mock_deps
mock_prompt.side_effect = ['set invalid_format', 'exit']
mock_confirm.return_value = True
await interactive_mode()
# Should not set preference
mock_deps.set_user_preference.assert_not_called()
# Should print error message
mock_console.print.assert_called()
@pytest.mark.asyncio
async def test_interactive_mode_exit_confirmation(self):
"""Test interactive mode handles exit confirmation."""
with patch('..cli.interactive_search') as mock_interactive_search:
with patch('..cli.display_welcome'):
with patch('..cli.Prompt.ask') as mock_prompt:
with patch('..cli.Confirm.ask') as mock_confirm:
mock_deps = AsyncMock()
mock_interactive_search.return_value = mock_deps
mock_prompt.side_effect = ['exit', 'quit']
# First time say no, second time say yes
mock_confirm.side_effect = [False, True]
await interactive_mode()
# Should ask for confirmation twice
assert mock_confirm.call_count == 2
# Should cleanup dependencies
mock_deps.cleanup.assert_called_once()
@pytest.mark.asyncio
async def test_interactive_mode_search_error(self):
"""Test interactive mode handles search errors."""
with patch('..cli.interactive_search') as mock_interactive_search:
with patch('..cli.display_welcome'):
with patch('..cli.search') as mock_search:
with patch('..cli.Prompt.ask') as mock_prompt:
with patch('..cli.Confirm.ask') as mock_confirm:
with patch('..cli.console') as mock_console:
mock_deps = AsyncMock()
mock_interactive_search.return_value = mock_deps
mock_search.side_effect = Exception("Search failed")
mock_prompt.side_effect = ['test query', 'exit']
mock_confirm.return_value = True
await interactive_mode()
# Should print error message
error_calls = [call for call in mock_console.print.call_args_list
if 'Error:' in str(call)]
assert len(error_calls) > 0
class TestCLIInputValidation:
"""Test CLI input validation."""
def test_search_command_empty_query(self):
"""Test search command with empty query."""
runner = CliRunner()
result = runner.invoke(search_cmd, ['--query', ''])
# Should still accept empty query (might be valid use case)
assert result.exit_code == 0 or result.exit_code == 1 # May fail due to missing search function
def test_search_command_invalid_type(self):
"""Test search command with invalid search type."""
runner = CliRunner()
result = runner.invoke(search_cmd, [
'--query', 'test',
'--type', 'invalid_type'
])
# Should reject invalid type
assert result.exit_code != 0
assert "Invalid value" in result.output or "Usage:" in result.output
def test_search_command_invalid_count(self):
"""Test search command with invalid count."""
runner = CliRunner()
result = runner.invoke(search_cmd, [
'--query', 'test',
'--count', 'not_a_number'
])
# Should reject non-numeric count
assert result.exit_code != 0
assert ("Invalid value" in result.output or
"Usage:" in result.output or
"not_a_number is not a valid integer" in result.output)
def test_search_command_negative_count(self):
"""Test search command with negative count."""
runner = CliRunner()
mock_response = SearchResponse(
summary="Test results",
key_findings=[],
sources=[],
search_strategy="auto",
result_count=0
)
with patch('..cli.search') as mock_search:
mock_search.return_value = mock_response
result = runner.invoke(search_cmd, [
'--query', 'test',
'--count', '-5'
])
# Click accepts negative integers, but our code should handle it
assert result.exit_code == 0
call_args = mock_search.call_args
assert call_args[1]['match_count'] == -5 # Passed through
def test_search_command_invalid_text_weight(self):
"""Test search command with invalid text weight."""
runner = CliRunner()
result = runner.invoke(search_cmd, [
'--query', 'test',
'--text-weight', 'not_a_float'
])
# Should reject non-numeric text weight
assert result.exit_code != 0
assert ("Invalid value" in result.output or
"Usage:" in result.output or
"not_a_float is not a valid" in result.output)
class TestCLIIntegration:
"""Test CLI integration scenarios."""
def test_cli_with_all_parameters(self):
"""Test CLI with all possible parameters."""
runner = CliRunner()
mock_response = SearchResponse(
summary="Complete search results",
key_findings=["Finding 1", "Finding 2"],
sources=["Source 1", "Source 2"],
search_strategy="hybrid",
result_count=15
)
with patch('..cli.search') as mock_search:
mock_search.return_value = mock_response
result = runner.invoke(search_cmd, [
'--query', 'comprehensive search test',
'--type', 'hybrid',
'--count', '15',
'--text-weight', '0.6'
])
assert result.exit_code == 0
# Verify all parameters passed correctly
call_args = mock_search.call_args
assert call_args[1]['query'] == 'comprehensive search test'
assert call_args[1]['search_type'] == 'hybrid'
assert call_args[1]['match_count'] == 15
assert call_args[1]['text_weight'] == 0.6
def test_cli_search_output_format(self):
"""Test CLI search output formatting."""
runner = CliRunner()
mock_response = SearchResponse(
summary="Formatted output test results with detailed information.",
key_findings=[
"Key finding number one with details",
"Second important finding",
"Third critical insight"
],
sources=[
"Python Documentation",
"Machine Learning Guide",
"API Reference Manual"
],
search_strategy="semantic",
result_count=25
)
with patch('..cli.search') as mock_search:
mock_search.return_value = mock_response
result = runner.invoke(search_cmd, [
'--query', 'formatting test'
])
assert result.exit_code == 0
# Check that output contains expected formatted content
output = result.output
assert "Searching for:" in output
assert "formatting test" in output
assert "Summary:" in output
assert "Formatted output test results" in output
assert "Key Findings:" in output
assert "Key finding number one" in output
assert "Sources:" in output
assert "Python Documentation" in output
assert "Search Strategy: semantic" in output
assert "Results Found: 25" in output
class TestCLIErrorScenarios:
"""Test CLI error handling scenarios."""
def test_cli_keyboard_interrupt(self):
"""Test CLI handles keyboard interrupt gracefully."""
runner = CliRunner()
with patch('..cli.search') as mock_search:
mock_search.side_effect = KeyboardInterrupt()
result = runner.invoke(search_cmd, ['--query', 'test'])
# Should handle KeyboardInterrupt without crashing
assert result.exit_code != 0
def test_cli_system_exit(self):
"""Test CLI handles system exit gracefully."""
runner = CliRunner()
with patch('..cli.search') as mock_search:
mock_search.side_effect = SystemExit(1)
result = runner.invoke(search_cmd, ['--query', 'test'])
# Should handle SystemExit
assert result.exit_code == 1
def test_cli_unexpected_exception(self):
"""Test CLI handles unexpected exceptions."""
runner = CliRunner()
with patch('..cli.search') as mock_search:
mock_search.side_effect = RuntimeError("Unexpected error occurred")
result = runner.invoke(search_cmd, ['--query', 'test'])
assert result.exit_code == 1
assert "Error:" in result.output
assert "Unexpected error occurred" in result.output
class TestCLIUsability:
"""Test CLI usability features."""
def test_cli_help_messages(self):
"""Test CLI provides helpful help messages."""
runner = CliRunner()
# Test main CLI help
result = runner.invoke(cli, ['--help'])
assert result.exit_code == 0
assert "Semantic Search Agent CLI" in result.output
# Test search command help
result = runner.invoke(search_cmd, ['--help'])
assert result.exit_code == 0
assert "Perform a one-time search" in result.output
assert "--query" in result.output
assert "--type" in result.output
assert "--count" in result.output
assert "--text-weight" in result.output
# Test interactive command help
result = runner.invoke(interactive, ['--help'])
assert result.exit_code == 0
assert "interactive search session" in result.output
# Test info command help
result = runner.invoke(info, ['--help'])
assert result.exit_code == 0
assert "system information" in result.output
def test_cli_command_suggestions(self):
"""Test CLI provides command suggestions for typos."""
runner = CliRunner()
# Test with typo in command name
result = runner.invoke(cli, ['searc']) # Missing 'h'
# Should suggest correct command or show usage
assert result.exit_code != 0
assert ("Usage:" in result.output or
"No such command" in result.output or
"Did you mean" in result.output)
def test_cli_default_values(self):
"""Test CLI uses appropriate default values."""
runner = CliRunner()
mock_response = SearchResponse(
summary="Default values test",
key_findings=[],
sources=[],
search_strategy="auto",
result_count=10
)
with patch('..cli.search') as mock_search:
mock_search.return_value = mock_response
result = runner.invoke(search_cmd, ['--query', 'test with defaults'])
assert result.exit_code == 0
# Check default values were used
call_args = mock_search.call_args
assert call_args[1]['search_type'] == 'auto' # Default type
assert call_args[1]['match_count'] == 10 # Default count
assert call_args[1]['text_weight'] is None # No default text weight