Spaces:
Build error
Build error
from unittest.mock import AsyncMock, MagicMock, patch | |
import pytest | |
from prompt_toolkit.formatted_text import HTML | |
from pydantic import SecretStr | |
from openhands.cli.settings import ( | |
display_settings, | |
modify_llm_settings_advanced, | |
modify_llm_settings_basic, | |
) | |
from openhands.cli.tui import UserCancelledError | |
from openhands.core.config import OpenHandsConfig | |
from openhands.storage.data_models.settings import Settings | |
from openhands.storage.settings.file_settings_store import FileSettingsStore | |
# Mock classes for condensers | |
class MockLLMSummarizingCondenserConfig: | |
def __init__(self, llm_config, type): | |
self.llm_config = llm_config | |
self.type = type | |
class MockNoOpCondenserConfig: | |
def __init__(self, type): | |
self.type = type | |
class TestDisplaySettings: | |
def app_config(self): | |
config = MagicMock(spec=OpenHandsConfig) | |
llm_config = MagicMock() | |
llm_config.base_url = None | |
llm_config.model = 'openai/gpt-4' | |
llm_config.api_key = SecretStr('test-api-key') | |
config.get_llm_config.return_value = llm_config | |
config.default_agent = 'test-agent' | |
# Set up security as a separate mock | |
security_mock = MagicMock() | |
security_mock.confirmation_mode = True | |
config.security = security_mock | |
config.enable_default_condenser = True | |
return config | |
def advanced_app_config(self): | |
config = MagicMock(spec=OpenHandsConfig) | |
llm_config = MagicMock() | |
llm_config.base_url = 'https://custom-api.com' | |
llm_config.model = 'custom-model' | |
llm_config.api_key = SecretStr('test-api-key') | |
config.get_llm_config.return_value = llm_config | |
config.default_agent = 'test-agent' | |
# Set up security as a separate mock | |
security_mock = MagicMock() | |
security_mock.confirmation_mode = True | |
config.security = security_mock | |
config.enable_default_condenser = True | |
return config | |
def test_display_settings_standard_config(self, mock_print_container, app_config): | |
display_settings(app_config) | |
mock_print_container.assert_called_once() | |
# Verify the container was created with the correct settings | |
container = mock_print_container.call_args[0][0] | |
text_area = container.body | |
# Check that the text area contains expected labels and values | |
settings_text = text_area.text | |
assert 'LLM Provider:' in settings_text | |
assert 'openai' in settings_text | |
assert 'LLM Model:' in settings_text | |
assert 'gpt-4' in settings_text | |
assert 'API Key:' in settings_text | |
assert '********' in settings_text | |
assert 'Agent:' in settings_text | |
assert 'test-agent' in settings_text | |
assert 'Confirmation Mode:' in settings_text | |
assert 'Enabled' in settings_text | |
assert 'Memory Condensation:' in settings_text | |
assert 'Enabled' in settings_text | |
def test_display_settings_advanced_config( | |
self, mock_print_container, advanced_app_config | |
): | |
display_settings(advanced_app_config) | |
mock_print_container.assert_called_once() | |
# Verify the container was created with the correct settings | |
container = mock_print_container.call_args[0][0] | |
text_area = container.body | |
# Check that the text area contains expected labels and values | |
settings_text = text_area.text | |
assert 'Custom Model:' in settings_text | |
assert 'custom-model' in settings_text | |
assert 'Base URL:' in settings_text | |
assert 'https://custom-api.com' in settings_text | |
assert 'API Key:' in settings_text | |
assert '********' in settings_text | |
assert 'Agent:' in settings_text | |
assert 'test-agent' in settings_text | |
class TestModifyLLMSettingsBasic: | |
def app_config(self): | |
config = MagicMock(spec=OpenHandsConfig) | |
llm_config = MagicMock() | |
llm_config.model = 'openai/gpt-4' | |
llm_config.api_key = SecretStr('test-api-key') | |
llm_config.base_url = None | |
config.get_llm_config.return_value = llm_config | |
config.set_llm_config = MagicMock() | |
config.set_agent_config = MagicMock() | |
agent_config = MagicMock() | |
config.get_agent_config.return_value = agent_config | |
# Set up security as a separate mock | |
security_mock = MagicMock() | |
security_mock.confirmation_mode = True | |
config.security = security_mock | |
return config | |
def settings_store(self): | |
store = MagicMock(spec=FileSettingsStore) | |
store.load = AsyncMock(return_value=Settings()) | |
store.store = AsyncMock() | |
return store | |
async def test_modify_llm_settings_basic_success( | |
self, | |
mock_confirm, | |
mock_session, | |
mock_organize, | |
mock_get_models, | |
app_config, | |
settings_store, | |
): | |
# Setup mocks | |
mock_get_models.return_value = ['openai/gpt-4', 'anthropic/claude-3-opus'] | |
mock_organize.return_value = { | |
'openai': {'models': ['gpt-4', 'gpt-3.5-turbo'], 'separator': '/'}, | |
'anthropic': { | |
'models': ['claude-3-opus', 'claude-3-sonnet'], | |
'separator': '/', | |
}, | |
} | |
session_instance = MagicMock() | |
session_instance.prompt_async = AsyncMock( | |
side_effect=[ | |
'openai', # Provider | |
'gpt-4', # Model | |
'new-api-key', # API Key | |
] | |
) | |
mock_session.return_value = session_instance | |
# Mock cli_confirm to select the second option (change provider/model) for the first two calls | |
# and then select the first option (save settings) for the last call | |
mock_confirm.side_effect = [1, 1, 0] | |
# Call the function | |
await modify_llm_settings_basic(app_config, settings_store) | |
# Verify LLM config was updated | |
app_config.set_llm_config.assert_called_once() | |
args, kwargs = app_config.set_llm_config.call_args | |
# The model name might be different based on the default model in the list | |
# Just check that it starts with 'openai/' | |
assert args[0].model.startswith('openai/') | |
assert args[0].api_key.get_secret_value() == 'new-api-key' | |
assert args[0].base_url is None | |
# Verify settings were saved | |
settings_store.store.assert_called_once() | |
args, kwargs = settings_store.store.call_args | |
settings = args[0] | |
# The model name might be different based on the default model in the list | |
# Just check that it starts with openai/ | |
assert settings.llm_model.startswith('openai/') | |
assert settings.llm_api_key.get_secret_value() == 'new-api-key' | |
assert settings.llm_base_url is None | |
async def test_modify_llm_settings_basic_user_cancels( | |
self, | |
mock_confirm, | |
mock_session, | |
mock_organize, | |
mock_get_models, | |
app_config, | |
settings_store, | |
): | |
# Setup mocks | |
mock_get_models.return_value = ['openai/gpt-4', 'anthropic/claude-3-opus'] | |
mock_organize.return_value = { | |
'openai': {'models': ['gpt-4', 'gpt-3.5-turbo'], 'separator': '/'} | |
} | |
session_instance = MagicMock() | |
session_instance.prompt_async = AsyncMock(side_effect=UserCancelledError()) | |
mock_session.return_value = session_instance | |
# Call the function | |
await modify_llm_settings_basic(app_config, settings_store) | |
# Verify settings were not changed | |
app_config.set_llm_config.assert_not_called() | |
settings_store.store.assert_not_called() | |
async def test_modify_llm_settings_basic_invalid_input( | |
self, | |
mock_print, | |
mock_confirm, | |
mock_session, | |
mock_organize, | |
mock_get_models, | |
app_config, | |
settings_store, | |
): | |
# Setup mocks | |
mock_get_models.return_value = ['openai/gpt-4', 'anthropic/claude-3-opus'] | |
mock_organize.return_value = { | |
'openai': {'models': ['gpt-4', 'gpt-3.5-turbo'], 'separator': '/'} | |
} | |
session_instance = MagicMock() | |
session_instance.prompt_async = AsyncMock( | |
side_effect=[ | |
'invalid-provider', # First invalid provider | |
'openai', # Valid provider | |
'invalid-model', # Invalid model | |
'gpt-4', # Valid model | |
'new-api-key', # API key | |
] | |
) | |
mock_session.return_value = session_instance | |
# Mock cli_confirm to select the second option (change provider/model) for the first two calls | |
# and then select the first option (save settings) for the last call | |
mock_confirm.side_effect = [1, 1, 0] | |
# Call the function | |
await modify_llm_settings_basic(app_config, settings_store) | |
# Verify error messages were shown for invalid inputs | |
assert ( | |
mock_print.call_count >= 2 | |
) # At least two error messages should be printed | |
# Check for invalid provider error | |
provider_error_found = False | |
model_error_found = False | |
for call in mock_print.call_args_list: | |
args, _ = call | |
if args and isinstance(args[0], HTML): | |
if 'Invalid provider selected' in args[0].value: | |
provider_error_found = True | |
if 'Invalid model selected' in args[0].value: | |
model_error_found = True | |
assert provider_error_found, 'No error message for invalid provider' | |
assert model_error_found, 'No error message for invalid model' | |
# Verify LLM config was updated with correct values | |
app_config.set_llm_config.assert_called_once() | |
# Verify settings were saved | |
settings_store.store.assert_called_once() | |
args, kwargs = settings_store.store.call_args | |
settings = args[0] | |
assert settings.llm_model == 'openai/gpt-4' | |
assert settings.llm_api_key.get_secret_value() == 'new-api-key' | |
assert settings.llm_base_url is None | |
class TestModifyLLMSettingsAdvanced: | |
def app_config(self): | |
config = MagicMock(spec=OpenHandsConfig) | |
llm_config = MagicMock() | |
llm_config.model = 'custom-model' | |
llm_config.api_key = SecretStr('test-api-key') | |
llm_config.base_url = 'https://custom-api.com' | |
config.get_llm_config.return_value = llm_config | |
config.set_llm_config = MagicMock() | |
config.set_agent_config = MagicMock() | |
agent_config = MagicMock() | |
config.get_agent_config.return_value = agent_config | |
# Set up security as a separate mock | |
security_mock = MagicMock() | |
security_mock.confirmation_mode = True | |
config.security = security_mock | |
return config | |
def settings_store(self): | |
store = MagicMock(spec=FileSettingsStore) | |
store.load = AsyncMock(return_value=Settings()) | |
store.store = AsyncMock() | |
return store | |
async def test_modify_llm_settings_advanced_success( | |
self, mock_confirm, mock_session, mock_list_agents, app_config, settings_store | |
): | |
# Setup mocks | |
mock_list_agents.return_value = ['default', 'test-agent'] | |
session_instance = MagicMock() | |
session_instance.prompt_async = AsyncMock( | |
side_effect=[ | |
'new-model', # Custom model | |
'https://new-url', # Base URL | |
'new-api-key', # API key | |
'default', # Agent | |
] | |
) | |
mock_session.return_value = session_instance | |
# Mock user confirmations | |
mock_confirm.side_effect = [ | |
0, # Enable confirmation mode | |
0, # Enable memory condensation | |
0, # Save settings | |
] | |
# Call the function | |
await modify_llm_settings_advanced(app_config, settings_store) | |
# Verify LLM config was updated | |
app_config.set_llm_config.assert_called_once() | |
args, kwargs = app_config.set_llm_config.call_args | |
assert args[0].model == 'new-model' | |
assert args[0].api_key.get_secret_value() == 'new-api-key' | |
assert args[0].base_url == 'https://new-url' | |
# Verify settings were saved | |
settings_store.store.assert_called_once() | |
args, kwargs = settings_store.store.call_args | |
settings = args[0] | |
assert settings.llm_model == 'new-model' | |
assert settings.llm_api_key.get_secret_value() == 'new-api-key' | |
assert settings.llm_base_url == 'https://new-url' | |
assert settings.agent == 'default' | |
assert settings.confirmation_mode is True | |
assert settings.enable_default_condenser is True | |
async def test_modify_llm_settings_advanced_user_cancels( | |
self, mock_confirm, mock_session, mock_list_agents, app_config, settings_store | |
): | |
# Setup mocks | |
mock_list_agents.return_value = ['default', 'test-agent'] | |
session_instance = MagicMock() | |
session_instance.prompt_async = AsyncMock(side_effect=UserCancelledError()) | |
mock_session.return_value = session_instance | |
# Call the function | |
await modify_llm_settings_advanced(app_config, settings_store) | |
# Verify settings were not changed | |
app_config.set_llm_config.assert_not_called() | |
settings_store.store.assert_not_called() | |
async def test_modify_llm_settings_advanced_invalid_agent( | |
self, | |
mock_print, | |
mock_confirm, | |
mock_session, | |
mock_list_agents, | |
app_config, | |
settings_store, | |
): | |
# Setup mocks | |
mock_list_agents.return_value = ['default', 'test-agent'] | |
session_instance = MagicMock() | |
session_instance.prompt_async = AsyncMock( | |
side_effect=[ | |
'new-model', # Custom model | |
'https://new-url', # Base URL | |
'new-api-key', # API key | |
'invalid-agent', # Invalid agent | |
'default', # Valid agent on retry | |
] | |
) | |
mock_session.return_value = session_instance | |
# Call the function | |
await modify_llm_settings_advanced(app_config, settings_store) | |
# Verify error message was shown | |
assert ( | |
mock_print.call_count == 3 | |
) # Called 3 times: empty line, error message, empty line | |
error_message_call = mock_print.call_args_list[ | |
1 | |
] # The second call contains the error message | |
args, kwargs = error_message_call | |
assert isinstance(args[0], HTML) | |
assert 'Invalid agent' in args[0].value | |
# Verify settings were not changed | |
app_config.set_llm_config.assert_not_called() | |
settings_store.store.assert_not_called() | |
async def test_modify_llm_settings_advanced_user_rejects_save( | |
self, mock_confirm, mock_session, mock_list_agents, app_config, settings_store | |
): | |
# Setup mocks | |
mock_list_agents.return_value = ['default', 'test-agent'] | |
session_instance = MagicMock() | |
session_instance.prompt_async = AsyncMock( | |
side_effect=[ | |
'new-model', # Custom model | |
'https://new-url', # Base URL | |
'new-api-key', # API key | |
'default', # Agent | |
] | |
) | |
mock_session.return_value = session_instance | |
# Mock user confirmations | |
mock_confirm.side_effect = [ | |
0, # Enable confirmation mode | |
0, # Enable memory condensation | |
1, # Reject saving settings | |
] | |
# Call the function | |
await modify_llm_settings_advanced(app_config, settings_store) | |
# Verify settings were not changed | |
app_config.set_llm_config.assert_not_called() | |
settings_store.store.assert_not_called() | |