mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
- Add /settings/general and /settings/security sidebar pages rendering their respective SDK schema sections - Reorder nav: General above LLM, Security below LLM (both SAAS + OSS) - Remove SDK_LEGACY_FIELD_MAP and all legacy field bridging — the only canonical store for SDK settings is now sdk_settings_values - Simplify to_agent_settings(), _extract_sdk_settings_values(), and _apply_settings_payload() to read/write sdk_settings_values only - Fix inferInitialView to accept an optional schemaOverride so SdkSectionPage passes filteredSchema (prevents cross-section minor-value overrides from elevating the view tier on unrelated pages) - Add SETTINGS$NAV_GENERAL and SETTINGS$NAV_SECURITY i18n keys with translations for all 14 languages - Use lock.svg for Security icon and settings.svg for General icon Co-authored-by: openhands <openhands@all-hands.dev>
161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
import warnings
|
|
from unittest.mock import patch
|
|
|
|
from pydantic import SecretStr
|
|
|
|
from openhands.core.config.llm_config import LLMConfig
|
|
from openhands.core.config.openhands_config import OpenHandsConfig
|
|
from openhands.core.config.sandbox_config import SandboxConfig
|
|
from openhands.core.config.security_config import SecurityConfig
|
|
from openhands.storage.data_models.settings import Settings
|
|
|
|
|
|
def test_settings_from_config():
|
|
# Mock configuration
|
|
mock_app_config = OpenHandsConfig(
|
|
default_agent='test-agent',
|
|
max_iterations=100,
|
|
security=SecurityConfig(
|
|
security_analyzer='test-analyzer',
|
|
confirmation_mode=True,
|
|
),
|
|
llms={
|
|
'llm': LLMConfig(
|
|
model='test-model',
|
|
api_key=SecretStr('test-key'),
|
|
base_url='https://test.example.com',
|
|
)
|
|
},
|
|
sandbox=SandboxConfig(remote_runtime_resource_factor=2),
|
|
)
|
|
|
|
with patch(
|
|
'openhands.storage.data_models.settings.load_openhands_config',
|
|
return_value=mock_app_config,
|
|
):
|
|
settings = Settings.from_config()
|
|
|
|
assert settings is not None
|
|
assert settings.language == 'en'
|
|
assert settings.agent == 'test-agent'
|
|
assert settings.max_iterations == 100
|
|
assert settings.security_analyzer == 'test-analyzer'
|
|
assert settings.confirmation_mode is True
|
|
assert settings.llm_model == 'test-model'
|
|
assert settings.llm_api_key.get_secret_value() == 'test-key'
|
|
assert settings.llm_base_url == 'https://test.example.com'
|
|
assert settings.remote_runtime_resource_factor == 2
|
|
assert not settings.secrets_store.provider_tokens
|
|
|
|
|
|
def test_settings_from_config_no_api_key():
|
|
# Mock configuration without API key
|
|
mock_app_config = OpenHandsConfig(
|
|
default_agent='test-agent',
|
|
max_iterations=100,
|
|
security=SecurityConfig(
|
|
security_analyzer='test-analyzer',
|
|
confirmation_mode=True,
|
|
),
|
|
llms={
|
|
'llm': LLMConfig(
|
|
model='test-model', api_key=None, base_url='https://test.example.com'
|
|
)
|
|
},
|
|
sandbox=SandboxConfig(remote_runtime_resource_factor=2),
|
|
)
|
|
|
|
with patch(
|
|
'openhands.storage.data_models.settings.load_openhands_config',
|
|
return_value=mock_app_config,
|
|
):
|
|
settings = Settings.from_config()
|
|
assert settings is None
|
|
|
|
|
|
def test_settings_handles_sensitive_data():
|
|
settings = Settings(
|
|
language='en',
|
|
agent='test-agent',
|
|
max_iterations=100,
|
|
security_analyzer='test-analyzer',
|
|
confirmation_mode=True,
|
|
llm_model='test-model',
|
|
llm_api_key='test-key',
|
|
llm_base_url='https://test.example.com',
|
|
remote_runtime_resource_factor=2,
|
|
)
|
|
|
|
assert str(settings.llm_api_key) == '**********'
|
|
assert settings.llm_api_key.get_secret_value() == 'test-key'
|
|
|
|
|
|
def test_settings_preserve_sdk_settings_values():
|
|
settings = Settings(
|
|
llm_api_key='test-key',
|
|
sdk_settings_values={
|
|
'critic.enabled': True,
|
|
'critic.mode': 'all_actions',
|
|
'llm.litellm_extra_body': {'metadata': {'tier': 'pro'}},
|
|
},
|
|
)
|
|
|
|
assert settings.llm_api_key.get_secret_value() == 'test-key'
|
|
assert settings.sdk_settings_values == {
|
|
'critic.enabled': True,
|
|
'critic.mode': 'all_actions',
|
|
'llm.litellm_extra_body': {'metadata': {'tier': 'pro'}},
|
|
}
|
|
|
|
|
|
def test_settings_to_agent_settings_uses_sdk_values():
|
|
settings = Settings(
|
|
sdk_settings_values={
|
|
'llm.model': 'sdk-model',
|
|
'llm.base_url': 'https://sdk.example.com',
|
|
'llm.litellm_extra_body': {'metadata': {'tier': 'enterprise'}},
|
|
'condenser.enabled': False,
|
|
'condenser.max_size': 88,
|
|
'critic.enabled': True,
|
|
'critic.mode': 'all_actions',
|
|
},
|
|
)
|
|
|
|
agent_settings = settings.to_agent_settings()
|
|
|
|
assert agent_settings.llm.model == 'sdk-model'
|
|
assert agent_settings.llm.base_url == 'https://sdk.example.com'
|
|
assert agent_settings.llm.litellm_extra_body == {'metadata': {'tier': 'enterprise'}}
|
|
assert agent_settings.condenser.enabled is False
|
|
assert agent_settings.condenser.max_size == 88
|
|
assert agent_settings.critic.enabled is True
|
|
assert agent_settings.critic.mode == 'all_actions'
|
|
|
|
|
|
def test_settings_no_pydantic_frozen_field_warning():
|
|
"""Test that Settings model does not trigger Pydantic UnsupportedFieldAttributeWarning.
|
|
|
|
This test ensures that the 'frozen' parameter is not incorrectly used in Field()
|
|
definitions, which would cause warnings in Pydantic v2 for union types.
|
|
See: https://github.com/All-Hands-AI/infra/issues/860
|
|
"""
|
|
with warnings.catch_warnings(record=True) as w:
|
|
warnings.simplefilter('always')
|
|
|
|
# Re-import to trigger any warnings during model definition
|
|
import importlib
|
|
|
|
import openhands.storage.data_models.settings
|
|
|
|
importlib.reload(openhands.storage.data_models.settings)
|
|
|
|
# Check for warnings containing 'frozen' which would indicate
|
|
# incorrect usage of frozen=True in Field()
|
|
frozen_warnings = [
|
|
warning for warning in w if 'frozen' in str(warning.message).lower()
|
|
]
|
|
|
|
assert len(frozen_warnings) == 0, (
|
|
f'Pydantic frozen field warnings found: {[str(w.message) for w in frozen_warnings]}'
|
|
)
|