Files
OpenHands/tests/unit/storage/data_models/test_settings.py
openhands 3a12924bc8 refactor: add General/Security pages, remove SDK_LEGACY_FIELD_MAP, fix inferInitialView
- 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>
2026-03-17 01:46:11 +00:00

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]}'
)