mirror of
https://github.com/coleam00/context-engineering-intro.git
synced 2025-12-18 10:15:27 +00:00
665 lines
26 KiB
Python
665 lines
26 KiB
Python
|
|
"""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
|