# SPDX-License-Identifier: Apache-2.0 """Tests for omlx.settings module.""" import json import os import tempfile from argparse import Namespace from pathlib import Path from unittest.mock import patch import pytest from omlx.settings import ( BURST_DECODE_MODES, DEFAULT_BURST_DECODE_MODE, AuthSettings, CacheSettings, ClaudeCodeSettings, GlobalSettings, HuggingFaceSettings, IntegrationSettings, LoggingSettings, MCPSettings, MemorySettings, ModelSettings, NetworkSettings, SamplingSettings, SchedulerSettings, ServerSettings, burst_decode_env, get_settings, get_ssd_capacity, get_system_memory, init_settings, reset_settings, resolve_default_base_path, ) class TestServerSettings: """Tests for ServerSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = ServerSettings() assert settings.host == "127.0.0.1" assert settings.port == 8000 assert settings.log_level == "info" assert settings.cors_origins == ["*"] assert settings.sse_keepalive_mode == "chunk" assert settings.auto_start_on_launch is True assert settings.burst_decode_mode == "balanced" assert settings.preserve_mid_system_cache is True def test_custom_values(self): """Test custom values.""" settings = ServerSettings( host="0.0.0.0", port=9000, log_level="debug", cors_origins=["https://example.com"], ) assert settings.host == "0.0.0.0" assert settings.port == 9000 assert settings.log_level == "debug" assert settings.cors_origins == ["https://example.com"] def test_to_dict(self): """Test conversion to dictionary.""" settings = ServerSettings(host="127.0.0.1", port=8000, log_level="info") result = settings.to_dict() assert result == { "host": "127.0.0.1", "port": 8000, "log_level": "info", "cors_origins": ["*"], "server_aliases": [], "sse_keepalive_mode": "chunk", "auto_start_on_launch": True, "burst_decode_mode": "balanced", "preserve_mid_system_cache": True, } def test_from_dict_sse_keepalive_mode(self): """sse_keepalive_mode round-trips through from_dict / to_dict.""" for mode in ("chunk", "comment", "off"): settings = ServerSettings.from_dict({"sse_keepalive_mode": mode}) assert settings.sse_keepalive_mode == mode assert settings.to_dict()["sse_keepalive_mode"] == mode def test_from_dict_burst_decode_mode(self): """burst_decode_mode round-trips through from_dict / to_dict.""" for mode in BURST_DECODE_MODES: settings = ServerSettings.from_dict({"burst_decode_mode": mode}) assert settings.burst_decode_mode == mode assert settings.to_dict()["burst_decode_mode"] == mode def test_from_dict_burst_decode_mode_default(self): """A settings.json without burst_decode_mode falls back to the default.""" settings = ServerSettings.from_dict({}) assert settings.burst_decode_mode == DEFAULT_BURST_DECODE_MODE def test_from_dict_preserve_mid_system_cache(self): """preserve_mid_system_cache round-trips through from_dict / to_dict.""" settings = ServerSettings.from_dict({"preserve_mid_system_cache": False}) assert settings.preserve_mid_system_cache is False assert settings.to_dict()["preserve_mid_system_cache"] is False def test_from_dict_preserve_mid_system_cache_default(self): """Missing preserve_mid_system_cache keeps the cache-friendly default.""" settings = ServerSettings.from_dict({}) assert settings.preserve_mid_system_cache is True def test_from_dict_auto_start_on_launch(self): """auto_start_on_launch round-trips through from_dict / to_dict.""" settings = ServerSettings.from_dict({"auto_start_on_launch": False}) assert settings.auto_start_on_launch is False assert settings.to_dict()["auto_start_on_launch"] is False def test_from_dict(self): """Test creation from dictionary.""" data = {"host": "0.0.0.0", "port": 9000, "log_level": "debug"} settings = ServerSettings.from_dict(data) assert settings.host == "0.0.0.0" assert settings.port == 9000 assert settings.log_level == "debug" assert settings.cors_origins == ["*"] # default def test_from_dict_reads_bind_address_fallback(self): """bind_address is accepted only as a compatibility fallback.""" settings = ServerSettings.from_dict({"bind_address": "0.0.0.0"}) assert settings.host == "0.0.0.0" def test_from_dict_host_wins_over_bind_address(self): """host remains the canonical persisted/admin API key.""" settings = ServerSettings.from_dict( {"host": "127.0.0.1", "bind_address": "0.0.0.0"} ) assert settings.host == "127.0.0.1" def test_from_dict_with_cors_origins(self): """Test creation from dictionary with cors_origins.""" data = { "host": "0.0.0.0", "port": 9000, "cors_origins": ["https://chat.example.com", "http://localhost:3000"], } settings = ServerSettings.from_dict(data) assert settings.cors_origins == [ "https://chat.example.com", "http://localhost:3000", ] def test_from_dict_with_defaults(self): """Test creation from partial dictionary uses defaults.""" data = {"port": 9000} settings = ServerSettings.from_dict(data) assert settings.host == "127.0.0.1" # default assert settings.port == 9000 assert settings.log_level == "info" # default assert settings.cors_origins == ["*"] # default class TestBurstDecodeEnv: """Tests for the Burst Decode mode -> OMLX_DECODE_BURST_* env mapping.""" def test_off_disables_bursting(self): """'off' caps max_steps at 1, which disables bursting in _step_burst.""" assert burst_decode_env("off")["OMLX_DECODE_BURST_MAX_STEPS"] == "1" def test_levels_set_single_request_budget(self): """light / balanced / aggressive map to the documented budgets.""" assert burst_decode_env("light")["OMLX_DECODE_BURST_BUDGET_SINGLE_S"] == "0.05" assert ( burst_decode_env("balanced")["OMLX_DECODE_BURST_BUDGET_SINGLE_S"] == "0.1" ) assert ( burst_decode_env("aggressive")["OMLX_DECODE_BURST_BUDGET_SINGLE_S"] == "0.2" ) def test_on_levels_keep_burst_enabled(self): """The on-levels keep max_steps above the disable threshold.""" for mode in ("light", "balanced", "aggressive"): assert int(burst_decode_env(mode)["OMLX_DECODE_BURST_MAX_STEPS"]) > 1 def test_unknown_mode_falls_back_to_default(self): """An unknown mode never disables bursting; it uses the default.""" assert burst_decode_env("bogus") == burst_decode_env(DEFAULT_BURST_DECODE_MODE) def test_keys_match_engine_config_env_vars(self): """The mapping only sets the env vars EngineConfig actually reads.""" assert set(burst_decode_env("balanced")) == { "OMLX_DECODE_BURST_MAX_STEPS", "OMLX_DECODE_BURST_BUDGET_SINGLE_S", } class TestModelSettings: """Tests for ModelSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = ModelSettings() assert settings.model_dirs == [] assert settings.model_dir is None def test_get_model_dirs_default(self): """Test default model directories.""" settings = ModelSettings() base_path = Path("/tmp/omlx") assert settings.get_model_dirs(base_path) == [Path("/tmp/omlx/models")] assert settings.get_model_dir(base_path) == Path("/tmp/omlx/models") def test_get_model_dirs_custom(self): """Test custom model directories.""" settings = ModelSettings(model_dirs=["/custom/models"]) base_path = Path("/tmp/omlx") assert settings.get_model_dirs(base_path) == [Path("/custom/models")] def test_get_model_dirs_multiple(self): """Test multiple model directories.""" settings = ModelSettings(model_dirs=["/path/a", "/path/b"]) base_path = Path("/tmp/omlx") result = settings.get_model_dirs(base_path) assert len(result) == 2 assert result[0] == Path("/path/a") assert result[1] == Path("/path/b") # get_model_dir returns the first (primary) directory assert settings.get_model_dir(base_path) == Path("/path/a") def test_get_model_dirs_with_tilde(self): """Test model directory with tilde expansion.""" settings = ModelSettings(model_dirs=["~/models"]) base_path = Path("/tmp/omlx") result = settings.get_model_dirs(base_path) assert "~" not in str(result[0]) # Should be expanded def test_get_model_dirs_backward_compat(self): """Test backward compatibility: model_dir fallback when model_dirs is empty.""" settings = ModelSettings(model_dir="/legacy/models") base_path = Path("/tmp/omlx") assert settings.get_model_dirs(base_path) == [Path("/legacy/models")] def test_to_dict(self): """Test conversion to dictionary.""" settings = ModelSettings(model_dirs=["/models"]) result = settings.to_dict() assert result == { "model_dirs": ["/models"], "model_dir": "/models", "model_fallback": False, "hide_helper_models": False, } def test_from_dict(self): """Test creation from dictionary.""" data = {"model_dirs": ["/models"]} settings = ModelSettings.from_dict(data) assert settings.model_dirs == ["/models"] assert settings.model_fallback is False def test_from_dict_ignores_legacy_max_model_memory(self): """Legacy max_model_memory key in settings.json is silently ignored.""" data = {"model_dirs": ["/models"], "max_model_memory": "32GB"} settings = ModelSettings.from_dict(data) assert settings.model_dirs == ["/models"] assert not hasattr(settings, "max_model_memory") def test_model_fallback_default(self): """Test model_fallback defaults to False.""" settings = ModelSettings() assert settings.model_fallback is False def test_model_fallback_to_dict(self): """Test model_fallback is included in to_dict.""" settings = ModelSettings(model_dirs=["/models"], model_fallback=True) result = settings.to_dict() assert result["model_fallback"] is True def test_model_fallback_from_dict(self): """Test model_fallback is loaded from dict.""" data = {"model_dirs": ["/models"], "model_fallback": True} settings = ModelSettings.from_dict(data) assert settings.model_fallback is True def test_model_fallback_from_dict_missing(self): """Test model_fallback defaults to False when missing from dict.""" data = {"model_dirs": ["/models"]} settings = ModelSettings.from_dict(data) assert settings.model_fallback is False def test_from_dict_backward_compat(self): """Test from_dict migrates old model_dir to model_dirs.""" data = {"model_dir": "/legacy/models"} settings = ModelSettings.from_dict(data) assert settings.model_dirs == ["/legacy/models"] assert settings.model_dir == "/legacy/models" class TestSchedulerSettings: """Tests for SchedulerSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = SchedulerSettings() assert settings.max_concurrent_requests == 8 assert settings.embedding_batch_size == 32 def test_custom_values(self): """Test custom values.""" settings = SchedulerSettings( max_concurrent_requests=128, embedding_batch_size=16 ) assert settings.max_concurrent_requests == 128 assert settings.embedding_batch_size == 16 def test_to_dict(self): """Test conversion to dictionary.""" settings = SchedulerSettings() result = settings.to_dict() assert result == { "max_concurrent_requests": 8, "embedding_batch_size": 32, "chunked_prefill": False, } def test_from_dict(self): """Test creation from dictionary.""" data = {"max_concurrent_requests": 512} settings = SchedulerSettings.from_dict(data) assert settings.max_concurrent_requests == 512 assert settings.embedding_batch_size == 32 data = {"max_concurrent_requests": 512, "embedding_batch_size": 24} settings = SchedulerSettings.from_dict(data) assert settings.max_concurrent_requests == 512 assert settings.embedding_batch_size == 24 def test_from_dict_backwards_compat(self): """Test creation from dictionary with old keys.""" data = {"max_num_seqs": 64} settings = SchedulerSettings.from_dict(data) assert settings.max_concurrent_requests == 64 data = {"completion_batch_size": 32} settings = SchedulerSettings.from_dict(data) assert settings.max_concurrent_requests == 32 class TestCacheSettings: """Tests for CacheSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = CacheSettings() assert settings.enabled is True assert settings.ssd_cache_dir is None assert settings.ssd_cache_max_size == "auto" assert settings.initial_cache_blocks == 256 def test_get_ssd_cache_dir_default(self): """Test default SSD cache directory.""" settings = CacheSettings(ssd_cache_dir=None) base_path = Path("/tmp/omlx") assert settings.get_ssd_cache_dir(base_path) == Path("/tmp/omlx/cache") def test_get_ssd_cache_dir_custom(self): """Test custom SSD cache directory.""" settings = CacheSettings(ssd_cache_dir="/custom/cache") base_path = Path("/tmp/omlx") assert settings.get_ssd_cache_dir(base_path) == Path("/custom/cache") def test_get_ssd_cache_max_size_bytes_auto(self): """Test auto SSD cache size calculation.""" settings = CacheSettings(ssd_cache_max_size="auto") base_path = Path("/tmp/omlx") cache_dir = settings.get_ssd_cache_dir(base_path) expected = int(get_ssd_capacity(cache_dir) * 0.1) assert settings.get_ssd_cache_max_size_bytes(base_path) == expected def test_get_ssd_cache_max_size_bytes_explicit(self): """Test explicit SSD cache size.""" settings = CacheSettings(ssd_cache_max_size="100GB") base_path = Path("/tmp/omlx") assert settings.get_ssd_cache_max_size_bytes(base_path) == 100 * 1024**3 def test_to_dict(self): """Test conversion to dictionary.""" settings = CacheSettings( enabled=False, ssd_cache_dir="/cache", ssd_cache_max_size="50GB" ) result = settings.to_dict() assert result == { "enabled": False, "hot_cache_only": False, "ssd_cache_dir": "/cache", "ssd_cache_max_size": "50GB", "hot_cache_max_size": "0", "initial_cache_blocks": 256, } def test_from_dict(self): """Test creation from dictionary.""" data = { "enabled": False, "ssd_cache_dir": "/cache", "ssd_cache_max_size": "200GB", } settings = CacheSettings.from_dict(data) assert settings.enabled is False assert settings.ssd_cache_dir == "/cache" assert settings.ssd_cache_max_size == "200GB" assert settings.initial_cache_blocks == 256 # default def test_from_dict_with_initial_cache_blocks(self): """Test creation from dictionary with initial_cache_blocks.""" data = { "enabled": True, "initial_cache_blocks": 16384, } settings = CacheSettings.from_dict(data) assert settings.initial_cache_blocks == 16384 def test_from_dict_migrates_hot_cache_auto_to_disabled(self): """Legacy hot_cache_max_size=auto should load as disabled.""" settings = CacheSettings.from_dict({"hot_cache_max_size": "auto"}) assert settings.hot_cache_max_size == "0" def test_initial_cache_blocks_custom(self): """Test custom initial_cache_blocks value.""" settings = CacheSettings(initial_cache_blocks=8192) assert settings.initial_cache_blocks == 8192 result = settings.to_dict() assert result["initial_cache_blocks"] == 8192 class TestAuthSettings: """Tests for AuthSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = AuthSettings() assert settings.api_key is None assert settings.secret_key is None assert settings.sub_keys == [] def test_custom_values(self): """Test custom values.""" settings = AuthSettings(api_key="test-secret-key", secret_key="my-secret") assert settings.api_key == "test-secret-key" assert settings.secret_key == "my-secret" def test_to_dict(self): """Test conversion to dictionary.""" settings = AuthSettings(api_key="my-key") result = settings.to_dict() assert result == { "api_key": "my-key", "secret_key": None, "skip_api_key_verification": False, "sub_keys": [], } def test_to_dict_with_sub_keys(self): """Test conversion to dictionary with sub keys.""" from omlx.settings import SubKeyEntry settings = AuthSettings( api_key="my-key", sub_keys=[SubKeyEntry(key="sk1", name="Test", created_at="2024-01-01")], ) result = settings.to_dict() assert len(result["sub_keys"]) == 1 assert result["sub_keys"][0]["key"] == "sk1" assert result["sub_keys"][0]["name"] == "Test" def test_from_dict(self): """Test creation from dictionary.""" data = {"api_key": "loaded-key", "secret_key": "loaded-secret"} settings = AuthSettings.from_dict(data) assert settings.api_key == "loaded-key" assert settings.secret_key == "loaded-secret" assert settings.sub_keys == [] def test_from_dict_with_sub_keys(self): """Test creation from dictionary with sub keys.""" data = { "api_key": "loaded-key", "sub_keys": [ {"key": "sk1", "name": "My Key", "created_at": "2024-01-01"}, ], } settings = AuthSettings.from_dict(data) assert len(settings.sub_keys) == 1 assert settings.sub_keys[0].key == "sk1" assert settings.sub_keys[0].name == "My Key" def test_from_dict_backward_compat(self): """Test creation from dictionary without secret_key (backward compat).""" data = {"api_key": "loaded-key"} settings = AuthSettings.from_dict(data) assert settings.api_key == "loaded-key" assert settings.secret_key is None assert settings.sub_keys == [] class TestMCPSettings: """Tests for MCPSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = MCPSettings() assert settings.config_path is None def test_custom_values(self): """Test custom values.""" settings = MCPSettings(config_path="/path/to/mcp.json") assert settings.config_path == "/path/to/mcp.json" def test_to_dict(self): """Test conversion to dictionary.""" settings = MCPSettings(config_path="/mcp/config.json") result = settings.to_dict() assert result == {"config_path": "/mcp/config.json"} def test_from_dict(self): """Test creation from dictionary.""" data = {"config_path": "/some/path.json"} settings = MCPSettings.from_dict(data) assert settings.config_path == "/some/path.json" class TestHuggingFaceSettings: """Tests for HuggingFaceSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = HuggingFaceSettings() assert settings.endpoint == "" assert settings.hf_cache_enabled is True def test_custom_values(self): """Test custom values.""" settings = HuggingFaceSettings( endpoint="https://hf-mirror.com", hf_cache_enabled=False, ) assert settings.endpoint == "https://hf-mirror.com" assert settings.hf_cache_enabled is False def test_to_dict(self): """Test conversion to dictionary.""" settings = HuggingFaceSettings( endpoint="https://hf-mirror.com", hf_cache_enabled=False, ) result = settings.to_dict() assert result == { "endpoint": "https://hf-mirror.com", "hf_cache_enabled": False, } def test_to_dict_empty(self): """Test conversion to dictionary with empty endpoint.""" settings = HuggingFaceSettings() result = settings.to_dict() assert result == {"endpoint": "", "hf_cache_enabled": True} def test_from_dict(self): """Test creation from dictionary.""" data = {"endpoint": "https://hf-mirror.com", "hf_cache_enabled": False} settings = HuggingFaceSettings.from_dict(data) assert settings.endpoint == "https://hf-mirror.com" assert settings.hf_cache_enabled is False def test_from_dict_defaults(self): """Test creation from empty dictionary uses defaults.""" settings = HuggingFaceSettings.from_dict({}) assert settings.endpoint == "" assert settings.hf_cache_enabled is True class TestNetworkSettings: """Tests for NetworkSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = NetworkSettings() assert settings.http_proxy == "" assert settings.https_proxy == "" assert settings.no_proxy == "" assert settings.ca_bundle == "" def test_custom_values(self): """Test custom values.""" settings = NetworkSettings( http_proxy="http://proxy:8080", https_proxy="http://proxy:8080", no_proxy="localhost,127.0.0.1", ca_bundle="/tmp/corp-ca.pem", ) assert settings.http_proxy == "http://proxy:8080" assert settings.https_proxy == "http://proxy:8080" assert settings.no_proxy == "localhost,127.0.0.1" assert settings.ca_bundle == "/tmp/corp-ca.pem" def test_to_dict(self): """Test conversion to dictionary.""" settings = NetworkSettings( http_proxy="http://proxy:8080", https_proxy="http://proxy:8080", no_proxy="localhost", ca_bundle="/tmp/ca.pem", ) result = settings.to_dict() assert result == { "http_proxy": "http://proxy:8080", "https_proxy": "http://proxy:8080", "no_proxy": "localhost", "ca_bundle": "/tmp/ca.pem", } def test_from_dict(self): """Test creation from dictionary.""" data = { "http_proxy": "http://proxy:8080", "https_proxy": "http://proxy:8080", "no_proxy": "localhost", "ca_bundle": "/tmp/ca.pem", } settings = NetworkSettings.from_dict(data) assert settings.http_proxy == "http://proxy:8080" assert settings.https_proxy == "http://proxy:8080" assert settings.no_proxy == "localhost" assert settings.ca_bundle == "/tmp/ca.pem" class TestLoggingSettings: """Tests for LoggingSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = LoggingSettings() assert settings.log_dir is None assert settings.retention_days == 7 def test_custom_values(self): """Test custom values.""" settings = LoggingSettings(log_dir="/custom/logs", retention_days=14) assert settings.log_dir == "/custom/logs" assert settings.retention_days == 14 def test_get_log_dir_default(self): """Test default log directory.""" settings = LoggingSettings(log_dir=None) base_path = Path("/tmp/omlx") assert settings.get_log_dir(base_path) == Path("/tmp/omlx/logs") def test_get_log_dir_custom(self): """Test custom log directory.""" settings = LoggingSettings(log_dir="/custom/logs") base_path = Path("/tmp/omlx") assert settings.get_log_dir(base_path) == Path("/custom/logs") def test_get_log_dir_with_tilde(self): """Test log directory with tilde expansion.""" settings = LoggingSettings(log_dir="~/logs") base_path = Path("/tmp/omlx") result = settings.get_log_dir(base_path) assert "~" not in str(result) # Should be expanded def test_to_dict(self): """Test conversion to dictionary.""" settings = LoggingSettings(log_dir="/logs", retention_days=30) result = settings.to_dict() assert result == {"log_dir": "/logs", "retention_days": 30} def test_from_dict(self): """Test creation from dictionary.""" data = {"log_dir": "/my/logs", "retention_days": 14} settings = LoggingSettings.from_dict(data) assert settings.log_dir == "/my/logs" assert settings.retention_days == 14 def test_from_dict_with_defaults(self): """Test creation from partial dictionary uses defaults.""" data = {"log_dir": "/custom"} settings = LoggingSettings.from_dict(data) assert settings.log_dir == "/custom" assert settings.retention_days == 7 # default class TestMemorySettings: """Tests for MemorySettings dataclass.""" def test_defaults(self): """Memory guard defaults to balanced tier and guard on.""" settings = MemorySettings() assert settings.prefill_memory_guard is True assert settings.memory_guard_tier == "balanced" def test_to_dict(self): """Test serialization.""" settings = MemorySettings(memory_guard_tier="safe") d = settings.to_dict() assert d["memory_guard_tier"] == "safe" assert d["prefill_memory_guard"] is True assert d["soft_threshold"] == 0.85 assert d["hard_threshold"] == 0.95 # Removed fields must not be present. assert "max_process_memory" not in d assert "max_process_memory_is_explicit" not in d def test_to_dict_guard_disabled(self): """Test serialization with prefill guard disabled.""" settings = MemorySettings(prefill_memory_guard=False) d = settings.to_dict() assert d["prefill_memory_guard"] is False def test_from_dict(self): """Test deserialization picks up tier value.""" settings = MemorySettings.from_dict({"memory_guard_tier": "aggressive"}) assert settings.memory_guard_tier == "aggressive" assert settings.prefill_memory_guard is True # default def test_from_dict_defaults(self): """Test deserialization with empty dict uses defaults.""" settings = MemorySettings.from_dict({}) assert settings.memory_guard_tier == "balanced" assert settings.prefill_memory_guard is True def test_from_dict_invalid_tier_falls_back_to_balanced(self): """Unknown tier values silently degrade to balanced.""" settings = MemorySettings.from_dict({"memory_guard_tier": "wild"}) assert settings.memory_guard_tier == "balanced" def test_from_dict_ignores_legacy_keys(self): """Legacy max_process_memory / is_explicit keys in old settings.json are ignored.""" settings = MemorySettings.from_dict( { "max_process_memory": "80%", "max_process_memory_is_explicit": True, "memory_guard_tier": "safe", } ) assert settings.memory_guard_tier == "safe" assert not hasattr(settings, "max_process_memory") def test_from_dict_guard_disabled(self): """Test deserialization with prefill guard disabled.""" settings = MemorySettings.from_dict({"prefill_memory_guard": False}) assert settings.prefill_memory_guard is False class TestGlobalSettings: """Tests for GlobalSettings class.""" def test_defaults(self): """Test default values.""" with tempfile.TemporaryDirectory() as tmpdir: settings = GlobalSettings(base_path=Path(tmpdir)) assert settings.server.host == "127.0.0.1" assert settings.server.port == 8000 assert settings.memory.memory_guard_tier == "balanced" assert settings.scheduler.max_concurrent_requests == 8 assert settings.scheduler.embedding_batch_size == 32 assert settings.cache.enabled is True assert settings.auth.api_key is None assert settings.mcp.config_path is None def test_get_effective_model_dirs_includes_hf_cache_between_dirs( self, tmp_path, monkeypatch ): """HF cache is inserted between primary and additional model dirs.""" primary = tmp_path / "primary" additional = tmp_path / "additional" hf_cache = tmp_path / "hf" / "hub" primary.mkdir() additional.mkdir() hf_cache.mkdir(parents=True) monkeypatch.setenv("HF_HUB_CACHE", str(hf_cache)) settings = GlobalSettings(base_path=tmp_path / "omlx") settings.model.model_dirs = [str(primary), str(additional)] assert settings.get_effective_model_dirs() == [ primary.resolve(), hf_cache.resolve(), additional.resolve(), ] def test_get_effective_model_dirs_skips_disabled_hf_cache( self, tmp_path, monkeypatch ): """Disabled HF cache is not included in discovery dirs.""" primary = tmp_path / "primary" hf_cache = tmp_path / "hf" / "hub" primary.mkdir() hf_cache.mkdir(parents=True) monkeypatch.setenv("HF_HUB_CACHE", str(hf_cache)) settings = GlobalSettings(base_path=tmp_path / "omlx") settings.model.model_dirs = [str(primary)] settings.huggingface.hf_cache_enabled = False assert settings.get_effective_model_dirs() == [primary.resolve()] def test_cli_override_memory_guard_tier(self, tmp_path): """CLI memory guard tier should override loaded settings.""" args = Namespace(memory_guard="safe", memory_guard_gb=None) settings = GlobalSettings.load(base_path=tmp_path, cli_args=args) assert settings.memory.memory_guard_tier == "safe" assert settings.memory.memory_guard_custom_ceiling_gb == 0.0 def test_cli_override_memory_guard_gb_sets_custom_tier(self, tmp_path): """CLI memory guard GB should select custom tier automatically.""" args = Namespace(memory_guard=None, memory_guard_gb=48.0) settings = GlobalSettings.load(base_path=tmp_path, cli_args=args) assert settings.memory.memory_guard_tier == "custom" assert settings.memory.memory_guard_custom_ceiling_gb == 48.0 def test_load_from_file(self): """Test loading settings from JSON file.""" with tempfile.TemporaryDirectory() as tmpdir: # Create settings file settings_file = Path(tmpdir) / "settings.json" settings_file.write_text( json.dumps( { "version": "1.0", "server": {"port": 9000}, "auth": {"api_key": "test-key"}, } ) ) settings = GlobalSettings.load(base_path=tmpdir) assert settings.server.port == 9000 assert settings.auth.api_key == "test-key" def test_load_from_file_all_sections(self): """Test loading all settings sections from file.""" with tempfile.TemporaryDirectory() as tmpdir: settings_file = Path(tmpdir) / "settings.json" settings_file.write_text( json.dumps( { "version": "1.0", "server": { "host": "0.0.0.0", "port": 9000, "log_level": "debug", }, "model": {"model_dir": "/models"}, "memory": {"memory_guard_tier": "safe"}, "scheduler": { "max_concurrent_requests": 128, "embedding_batch_size": 24, }, "cache": { "enabled": False, "ssd_cache_dir": "/cache", "ssd_cache_max_size": "50GB", }, "auth": {"api_key": "secret"}, "mcp": {"config_path": "/mcp.json"}, } ) ) settings = GlobalSettings.load(base_path=tmpdir) assert settings.server.host == "0.0.0.0" assert settings.server.port == 9000 assert settings.server.log_level == "debug" assert settings.model.model_dirs == ["/models"] # Migrated from model_dir assert settings.model.model_dir == "/models" # Backward compat field assert settings.memory.memory_guard_tier == "safe" assert settings.scheduler.max_concurrent_requests == 128 assert settings.scheduler.embedding_batch_size == 24 assert settings.cache.enabled is False assert settings.cache.ssd_cache_dir == "/cache" assert settings.auth.api_key == "secret" assert settings.mcp.config_path == "/mcp.json" def test_load_nonexistent_file_uses_defaults(self): """Test loading with no settings file uses defaults.""" with tempfile.TemporaryDirectory() as tmpdir: settings = GlobalSettings.load(base_path=tmpdir) assert settings.server.host == "127.0.0.1" assert settings.server.port == 8000 def test_load_invalid_json_uses_defaults(self): """Test loading invalid JSON file logs warning and uses defaults.""" with tempfile.TemporaryDirectory() as tmpdir: settings_file = Path(tmpdir) / "settings.json" settings_file.write_text("{ invalid json }") settings = GlobalSettings.load(base_path=tmpdir) # Should use defaults due to parse error assert settings.server.port == 8000 def test_save(self): """Test saving settings to file.""" with tempfile.TemporaryDirectory() as tmpdir: settings = GlobalSettings(base_path=Path(tmpdir)) settings.server.port = 9001 settings.auth.api_key = "saved-key" settings.save() # Verify file was created settings_file = Path(tmpdir) / "settings.json" assert settings_file.exists() # Verify content data = json.loads(settings_file.read_text()) assert data["version"] == "1.0" assert data["server"]["port"] == 9001 assert data["auth"]["api_key"] == "saved-key" def test_save_and_load_cors_origins(self): """Test saving and loading cors_origins through settings file.""" with tempfile.TemporaryDirectory() as tmpdir: settings = GlobalSettings(base_path=Path(tmpdir)) settings.server.cors_origins = ["https://chat.example.com"] settings.save() loaded = GlobalSettings.load(base_path=tmpdir) assert loaded.server.cors_origins == ["https://chat.example.com"] def test_load_cors_origins_from_file(self): """Test loading cors_origins from settings.json.""" with tempfile.TemporaryDirectory() as tmpdir: settings_file = Path(tmpdir) / "settings.json" settings_file.write_text( json.dumps( { "version": "1.0", "server": { "port": 8000, "cors_origins": [ "https://chat.example.com", "http://localhost:3000", ], }, } ) ) settings = GlobalSettings.load(base_path=tmpdir) assert settings.server.cors_origins == [ "https://chat.example.com", "http://localhost:3000", ] def test_load_without_cors_origins_uses_default(self): """Test that missing cors_origins in settings.json uses default ['*'].""" with tempfile.TemporaryDirectory() as tmpdir: settings_file = Path(tmpdir) / "settings.json" settings_file.write_text( json.dumps({"version": "1.0", "server": {"port": 9000}}) ) settings = GlobalSettings.load(base_path=tmpdir) assert settings.server.cors_origins == ["*"] def test_save_creates_directory(self): """Test save creates base directory if needed.""" with tempfile.TemporaryDirectory() as tmpdir: base = Path(tmpdir) / "nested" / "omlx" settings = GlobalSettings(base_path=base) settings.save() assert base.exists() assert (base / "settings.json").exists() def test_ensure_directories(self): """Test directory creation.""" with tempfile.TemporaryDirectory() as tmpdir: base = Path(tmpdir) / "omlx" settings = GlobalSettings(base_path=base) settings.ensure_directories() assert base.exists() assert (base / "models").exists() assert (base / "cache").exists() assert (base / "logs").exists() def test_ensure_directories_custom_paths(self): """Test directory creation with custom paths.""" with tempfile.TemporaryDirectory() as tmpdir: base = Path(tmpdir) / "omlx" custom_models = Path(tmpdir) / "custom_models" custom_cache = Path(tmpdir) / "custom_cache" custom_logs = Path(tmpdir) / "custom_logs" settings = GlobalSettings(base_path=base) settings.model.model_dir = str(custom_models) settings.cache.ssd_cache_dir = str(custom_cache) settings.logging.log_dir = str(custom_logs) settings.ensure_directories() assert base.exists() assert custom_models.exists() assert custom_cache.exists() assert custom_logs.exists() def test_ensure_directories_unavailable_model_dir(self): """Test that unavailable model dirs are skipped instead of crashing.""" with tempfile.TemporaryDirectory() as tmpdir: base = Path(tmpdir) / "omlx" valid_models = Path(tmpdir) / "valid_models" unavailable = Path("/Volumes/NonExistentDrive/Models") settings = GlobalSettings(base_path=base) settings.model.model_dirs = [str(valid_models), str(unavailable)] settings.ensure_directories() assert base.exists() assert valid_models.exists() # Unavailable path should be removed from model_dirs resolved_dirs = settings.model.get_model_dirs(base) assert len(resolved_dirs) == 1 assert resolved_dirs[0] == valid_models.resolve() def test_ensure_directories_unreadable_model_dir(self, tmp_path, monkeypatch): """Test that existing but unreadable model dirs are skipped.""" base = tmp_path / "omlx" valid_models = tmp_path / "valid_models" unreadable = tmp_path / "unreadable_models" unreadable.mkdir() original_iterdir = Path.iterdir def fake_iterdir(path): if path == unreadable.resolve(): raise PermissionError("Operation not permitted") return original_iterdir(path) monkeypatch.setattr(Path, "iterdir", fake_iterdir) settings = GlobalSettings(base_path=base) settings.model.model_dirs = [str(valid_models), str(unreadable)] settings.ensure_directories() resolved_dirs = settings.model.get_model_dirs(base) assert resolved_dirs == [valid_models.resolve()] def test_validate_valid_settings(self): """Test validation with valid settings.""" settings = GlobalSettings() errors = settings.validate() assert errors == [] def test_validate_context_window_policy(self): """Sampling context policy must be positive when set.""" settings = GlobalSettings() settings.sampling.max_context_window_policy = 128000 assert settings.validate() == [] settings.sampling.max_context_window_policy = 0 errors = settings.validate() assert any("max_context_window_policy" in e for e in errors) def test_validate_invalid_port_low(self): """Test validation catches port below 1.""" settings = GlobalSettings() settings.server.port = 0 errors = settings.validate() assert any("port" in e.lower() for e in errors) def test_validate_invalid_port_high(self): """Test validation catches port above 65535.""" settings = GlobalSettings() settings.server.port = 70000 errors = settings.validate() assert any("port" in e.lower() for e in errors) def test_validate_invalid_log_level(self): """Test validation catches invalid log level.""" settings = GlobalSettings() settings.server.log_level = "invalid" errors = settings.validate() assert any("log_level" in e.lower() for e in errors) def test_validate_valid_log_levels(self): """Test validation accepts all valid log levels.""" for level in ["trace", "debug", "info", "warning", "error", "critical"]: settings = GlobalSettings() settings.server.log_level = level errors = settings.validate() assert errors == [] def test_validate_invalid_sse_keepalive_mode(self): """Validation rejects unknown sse_keepalive_mode values.""" settings = GlobalSettings() settings.server.sse_keepalive_mode = "bogus" errors = settings.validate() assert any("sse_keepalive_mode" in e for e in errors) def test_validate_valid_sse_keepalive_modes(self): """Validation accepts chunk / comment / off.""" for mode in ("chunk", "comment", "off"): settings = GlobalSettings() settings.server.sse_keepalive_mode = mode errors = settings.validate() assert errors == [] def test_validate_memory_guard_tier_valid(self): """Test validation accepts each known tier.""" for tier in ("safe", "balanced", "aggressive"): settings = GlobalSettings() settings.memory.memory_guard_tier = tier errors = settings.validate() assert not any("memory_guard_tier" in e for e in errors) settings = GlobalSettings() settings.memory.memory_guard_tier = "custom" settings.memory.memory_guard_custom_ceiling_gb = 48.0 errors = settings.validate() assert not any("memory_guard_tier" in e for e in errors) assert not any("memory_guard_custom_ceiling_gb" in e for e in errors) def test_validate_memory_guard_tier_invalid(self): """Test validation flags unknown tier values.""" settings = GlobalSettings() settings.memory.memory_guard_tier = "extreme" # type: ignore[assignment] errors = settings.validate() assert any("memory_guard_tier" in e for e in errors) def test_validate_invalid_scheduler_values(self): """Test validation catches invalid scheduler values.""" settings = GlobalSettings() settings.scheduler.max_concurrent_requests = 0 errors = settings.validate() assert any("max_concurrent_requests" in e.lower() for e in errors) settings = GlobalSettings() settings.scheduler.embedding_batch_size = 0 errors = settings.validate() assert any("embedding_batch_size" in e.lower() for e in errors) def test_validate_invalid_cache_size(self): """Test validation catches invalid cache size.""" settings = GlobalSettings() settings.cache.ssd_cache_max_size = "not-a-size" errors = settings.validate() assert any("ssd_cache_max_size" in e.lower() for e in errors) def test_validate_hot_cache_size(self): """Hot cache accepts explicit sizes only; auto is SSD-cache-only.""" settings = GlobalSettings() settings.cache.hot_cache_max_size = "0" assert not any("hot_cache_max_size" in e for e in settings.validate()) settings.cache.hot_cache_max_size = "8GB" assert not any("hot_cache_max_size" in e for e in settings.validate()) settings.cache.hot_cache_max_size = "auto" errors = settings.validate() assert any("hot_cache_max_size" in e for e in errors) assert any("auto" in e for e in errors) settings.cache.hot_cache_max_size = "not-a-size" errors = settings.validate() assert any("hot_cache_max_size" in e for e in errors) def test_validate_invalid_initial_cache_blocks(self): """Test validation catches invalid initial_cache_blocks.""" settings = GlobalSettings() settings.cache.initial_cache_blocks = 0 errors = settings.validate() assert any("initial_cache_blocks" in e.lower() for e in errors) settings = GlobalSettings() settings.cache.initial_cache_blocks = -1 errors = settings.validate() assert any("initial_cache_blocks" in e.lower() for e in errors) def test_validate_multiple_errors(self): """Test validation returns multiple errors.""" settings = GlobalSettings() settings.server.port = 0 settings.scheduler.max_concurrent_requests = -1 settings.memory.memory_guard_tier = "extreme" # type: ignore[assignment] errors = settings.validate() assert len(errors) >= 3 def test_validate_hf_endpoint_empty_ok(self): """Test empty HuggingFace endpoint passes validation.""" settings = GlobalSettings() settings.huggingface.endpoint = "" errors = settings.validate() assert not any("huggingface" in e.lower() for e in errors) def test_validate_hf_endpoint_valid(self): """Test valid HuggingFace endpoint passes validation.""" settings = GlobalSettings() settings.huggingface.endpoint = "https://hf-mirror.com" errors = settings.validate() assert not any("huggingface" in e.lower() for e in errors) def test_validate_hf_endpoint_invalid(self): """Test invalid HuggingFace endpoint fails validation.""" settings = GlobalSettings() settings.huggingface.endpoint = "not-a-url" errors = settings.validate() assert any("huggingface" in e.lower() for e in errors) def test_env_override_server(self): """Test environment variable override for server settings.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, { "OMLX_HOST": "0.0.0.0", "OMLX_PORT": "9999", "OMLX_LOG_LEVEL": "debug", }, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.server.host == "0.0.0.0" assert settings.server.port == 9999 assert settings.server.log_level == "debug" def test_env_override_model(self): """Test environment variable override for model settings.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, { "OMLX_MODEL_DIR": "/env/models", }, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.model.model_dir == "/env/models" def test_env_override_scheduler(self): """Test environment variable override for scheduler settings.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, {"OMLX_MAX_CONCURRENT_REQUESTS": "512"}, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.scheduler.max_concurrent_requests == 512 def test_env_override_embedding_batch_size(self): """Test environment variable override for embedding batch size.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, {"OMLX_EMBEDDING_BATCH_SIZE": "24"}, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.scheduler.embedding_batch_size == 24 def test_env_override_scheduler_legacy_fallback(self): """Test legacy OMLX_MAX_NUM_SEQS env var is accepted as fallback.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, {"OMLX_MAX_NUM_SEQS": "256"}, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.scheduler.max_concurrent_requests == 256 def test_env_override_cache(self): """Test environment variable override for cache settings.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, { "OMLX_CACHE_ENABLED": "false", "OMLX_SSD_CACHE_DIR": "/env/cache", "OMLX_SSD_CACHE_MAX_SIZE": "200GB", }, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.cache.enabled is False assert settings.cache.ssd_cache_dir == "/env/cache" assert settings.cache.ssd_cache_max_size == "200GB" def test_env_override_initial_cache_blocks(self): """Test environment variable override for initial_cache_blocks.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, {"OMLX_INITIAL_CACHE_BLOCKS": "16384"}, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.cache.initial_cache_blocks == 16384 def test_env_override_cache_enabled_values(self): """Test various values for OMLX_CACHE_ENABLED.""" with tempfile.TemporaryDirectory() as tmpdir: for value in ["true", "1", "yes"]: with patch.dict(os.environ, {"OMLX_CACHE_ENABLED": value}, clear=False): settings = GlobalSettings.load(base_path=tmpdir) assert settings.cache.enabled is True for value in ["false", "0", "no"]: with patch.dict(os.environ, {"OMLX_CACHE_ENABLED": value}, clear=False): settings = GlobalSettings.load(base_path=tmpdir) assert settings.cache.enabled is False def test_env_override_auth(self): """Test environment variable override for auth settings.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict(os.environ, {"OMLX_API_KEY": "env-key"}, clear=False): settings = GlobalSettings.load(base_path=tmpdir) assert settings.auth.api_key == "env-key" def test_env_override_mcp(self): """Test environment variable override for MCP settings.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, {"OMLX_MCP_CONFIG": "/env/mcp.json"}, clear=False ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.mcp.config_path == "/env/mcp.json" def test_env_override_hf_endpoint(self): """Test environment variable override for HuggingFace settings.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, {"OMLX_HF_ENDPOINT": "https://hf-mirror.com"}, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.huggingface.endpoint == "https://hf-mirror.com" def test_env_override_network(self): """Test environment variable override for network proxy settings.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict( os.environ, { "OMLX_HTTP_PROXY": "http://proxy.company.com:8080", "OMLX_HTTPS_PROXY": "http://proxy.company.com:8443", "OMLX_NO_PROXY": "localhost,127.0.0.1", "OMLX_CA_BUNDLE": "/tmp/corp-ca.pem", }, clear=False, ): settings = GlobalSettings.load(base_path=tmpdir) assert settings.network.http_proxy == "http://proxy.company.com:8080" assert settings.network.https_proxy == "http://proxy.company.com:8443" assert settings.network.no_proxy == "localhost,127.0.0.1" assert settings.network.ca_bundle == "/tmp/corp-ca.pem" def test_env_override_invalid_port_logs_warning(self): """Test invalid OMLX_PORT logs warning and keeps default.""" with tempfile.TemporaryDirectory() as tmpdir: with patch.dict(os.environ, {"OMLX_PORT": "not-a-number"}, clear=False): settings = GlobalSettings.load(base_path=tmpdir) # Should keep default due to parse error assert settings.server.port == 8000 def test_env_override_after_file(self): """Test env vars override file settings.""" with tempfile.TemporaryDirectory() as tmpdir: # Create settings file settings_file = Path(tmpdir) / "settings.json" settings_file.write_text( json.dumps({"version": "1.0", "server": {"port": 9000}}) ) with patch.dict(os.environ, {"OMLX_PORT": "8888"}, clear=False): settings = GlobalSettings.load(base_path=tmpdir) # Env should override file assert settings.server.port == 8888 def test_cli_override(self): """Test CLI argument override.""" with tempfile.TemporaryDirectory() as tmpdir: args = Namespace( port=8888, host="0.0.0.0", log_level="warning", model_dir="/cli/models", api_key="cli-key", ) settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) assert settings.server.port == 8888 assert settings.server.host == "0.0.0.0" assert settings.server.log_level == "warning" assert settings.model.model_dir == "/cli/models" assert settings.auth.api_key == "cli-key" def test_cli_override_partial(self): """Test CLI override with some None values.""" with tempfile.TemporaryDirectory() as tmpdir: args = Namespace(port=7777, host=None, log_level=None) settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) assert settings.server.port == 7777 assert settings.server.host == "127.0.0.1" # default assert settings.server.log_level == "info" # default def test_cli_override_scheduler(self): """Test CLI override for scheduler settings.""" with tempfile.TemporaryDirectory() as tmpdir: args = Namespace(max_concurrent_requests=64, embedding_batch_size=12) settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) assert settings.scheduler.max_concurrent_requests == 64 assert settings.scheduler.embedding_batch_size == 12 def test_cli_override_cache(self): """Test CLI override for cache settings.""" with tempfile.TemporaryDirectory() as tmpdir: args = Namespace( cache_enabled=False, ssd_cache_dir="/cli/cache", ssd_cache_max_size="500GB", ) settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) assert settings.cache.enabled is False assert settings.cache.ssd_cache_dir == "/cli/cache" assert settings.cache.ssd_cache_max_size == "500GB" def test_cli_override_initial_cache_blocks(self): """Test CLI override for initial_cache_blocks.""" with tempfile.TemporaryDirectory() as tmpdir: args = Namespace(initial_cache_blocks=4096) settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) assert settings.cache.initial_cache_blocks == 4096 def test_cli_override_mcp(self): """Test CLI override for MCP settings.""" with tempfile.TemporaryDirectory() as tmpdir: args = Namespace(mcp_config="/cli/mcp.json") settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) assert settings.mcp.config_path == "/cli/mcp.json" def test_cli_override_network(self): """Test CLI override for network proxy settings.""" with tempfile.TemporaryDirectory() as tmpdir: args = Namespace( http_proxy="http://proxy.company.com:8080", https_proxy="http://proxy.company.com:8443", no_proxy="localhost,127.0.0.1", ca_bundle="/tmp/corp-ca.pem", ) settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) assert settings.network.http_proxy == "http://proxy.company.com:8080" assert settings.network.https_proxy == "http://proxy.company.com:8443" assert settings.network.no_proxy == "localhost,127.0.0.1" assert settings.network.ca_bundle == "/tmp/corp-ca.pem" def test_validate_invalid_http_proxy(self): """Test invalid http_proxy fails validation.""" settings = GlobalSettings() settings.network.http_proxy = "proxy.company.com:8080" errors = settings.validate() assert any("http_proxy" in e.lower() for e in errors) def test_validate_invalid_https_proxy(self): """Test invalid https_proxy fails validation.""" settings = GlobalSettings() settings.network.https_proxy = "proxy.company.com:8443" errors = settings.validate() assert any("https_proxy" in e.lower() for e in errors) def test_validate_valid_network_proxy(self): """Test valid network proxy values pass validation.""" settings = GlobalSettings() settings.network.http_proxy = "http://proxy.company.com:8080" settings.network.https_proxy = "http://proxy.company.com:8443" errors = settings.validate() assert not any("http_proxy" in e.lower() for e in errors) assert not any("https_proxy" in e.lower() for e in errors) def test_priority_cli_over_env_over_file(self): """Test that CLI > env > file > defaults priority is respected.""" with tempfile.TemporaryDirectory() as tmpdir: # Create settings file with port 9000 settings_file = Path(tmpdir) / "settings.json" settings_file.write_text( json.dumps({"version": "1.0", "server": {"port": 9000}}) ) # Set env to port 8888 with patch.dict(os.environ, {"OMLX_PORT": "8888"}, clear=False): # CLI sets port to 7777 args = Namespace(port=7777) settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) # CLI should win assert settings.server.port == 7777 # Without CLI, env should win over file settings = GlobalSettings.load(base_path=tmpdir) # Env is no longer set, so file should be used assert settings.server.port == 9000 def test_to_dict(self): """Test conversion to dictionary.""" with tempfile.TemporaryDirectory() as tmpdir: settings = GlobalSettings(base_path=Path(tmpdir)) settings.server.port = 9000 result = settings.to_dict() assert result["version"] == "1.0" assert result["base_path"] == tmpdir assert result["server"]["port"] == 9000 assert "model" in result assert "scheduler" in result assert "cache" in result assert "auth" in result assert "mcp" in result def test_to_scheduler_config(self): """Test conversion to SchedulerConfig.""" settings = GlobalSettings() settings.scheduler.max_concurrent_requests = 128 settings.scheduler.embedding_batch_size = 12 scheduler_config = settings.to_scheduler_config() assert scheduler_config.max_num_seqs == 128 assert scheduler_config.completion_batch_size == 128 assert scheduler_config.embedding_batch_size == 12 assert scheduler_config.initial_cache_blocks == 256 # default def test_to_scheduler_config_initial_cache_blocks(self): """Test that initial_cache_blocks passes through to SchedulerConfig.""" settings = GlobalSettings() settings.cache.initial_cache_blocks = 8192 scheduler_config = settings.to_scheduler_config() assert scheduler_config.initial_cache_blocks == 8192 class TestInitSettings: """Tests for init_settings and get_settings.""" def setup_method(self): """Reset settings before each test.""" reset_settings() def teardown_method(self): """Reset settings after each test.""" reset_settings() def test_init_settings(self): """Test initializing global settings.""" with tempfile.TemporaryDirectory() as tmpdir: settings = init_settings(base_path=tmpdir) assert settings is not None assert get_settings() is settings def test_init_settings_with_cli_args(self): """Test initializing with CLI arguments.""" with tempfile.TemporaryDirectory() as tmpdir: args = Namespace(port=9999) settings = init_settings(base_path=tmpdir, cli_args=args) assert settings.server.port == 9999 assert get_settings().server.port == 9999 def test_get_settings_uninitialized_raises(self): """Test get_settings raises RuntimeError when not initialized.""" with pytest.raises(RuntimeError, match="Settings not initialized"): get_settings() def test_reset_settings(self): """Test resetting settings allows re-initialization.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize first init_settings(base_path=tmpdir) assert get_settings() is not None # Reset reset_settings() # Should raise now with pytest.raises(RuntimeError): get_settings() # Can initialize again settings = init_settings(base_path=tmpdir) assert settings is not None def test_multiple_init_overwrites(self): """Test calling init_settings multiple times overwrites.""" with ( tempfile.TemporaryDirectory() as tmpdir1, tempfile.TemporaryDirectory() as tmpdir2, ): settings1 = init_settings(base_path=tmpdir1) settings2 = init_settings(base_path=tmpdir2) assert get_settings() is settings2 # Use resolve() to handle macOS /var -> /private/var symlink assert get_settings().base_path.resolve() == Path(tmpdir2).resolve() class TestHelperFunctions: """Tests for helper functions.""" def test_get_system_memory(self): """Test system memory detection.""" memory = get_system_memory() assert memory > 0 # Should be at least 1GB on any modern system assert memory >= 1024**3 def test_get_system_memory_returns_int(self): """Test that get_system_memory returns an integer.""" memory = get_system_memory() assert isinstance(memory, int) def test_get_system_memory_uses_sysconf_before_compat(self): """macOS should not depend on psutil's HOST_VM_INFO64 adapter.""" def fake_sysconf(name): if name == "SC_PHYS_PAGES": return 123 if name == "SC_PAGE_SIZE": return 4096 raise ValueError(name) with ( patch("omlx.settings.os.sysconf", side_effect=fake_sysconf), patch( "omlx.utils.psutil_compat.get_total_memory", side_effect=AssertionError("compat should not be called"), ), ): assert get_system_memory() == 123 * 4096 def test_get_system_memory_falls_back_to_compat_when_sysconf_fails(self): with ( patch("omlx.settings.os.sysconf", side_effect=ValueError("unsupported")), patch( "omlx.utils.psutil_compat.get_total_memory", return_value=32 * 1024**3, ), ): assert get_system_memory() == 32 * 1024**3 def test_get_ssd_capacity(self): """Test SSD capacity detection.""" capacity = get_ssd_capacity(Path("/")) assert capacity > 0 def test_get_ssd_capacity_returns_int(self): """Test that get_ssd_capacity returns an integer.""" capacity = get_ssd_capacity(Path("/")) assert isinstance(capacity, int) def test_get_ssd_capacity_nonexistent_path(self): """Test SSD capacity for non-existent path uses parent.""" with tempfile.TemporaryDirectory() as tmpdir: nonexistent = Path(tmpdir) / "does" / "not" / "exist" capacity = get_ssd_capacity(nonexistent) assert capacity > 0 def test_get_ssd_capacity_with_string_path(self): """Test SSD capacity with string path.""" capacity = get_ssd_capacity("/") assert capacity > 0 def test_get_ssd_capacity_with_tilde(self): """Test SSD capacity with tilde path expansion.""" capacity = get_ssd_capacity("~/") assert capacity > 0 class TestResolveDefaultBasePath: """Tests for resolve_default_base_path().""" def test_falls_back_to_default_when_nothing_configured(self, monkeypatch): monkeypatch.delenv("OMLX_BASE_PATH", raising=False) monkeypatch.setattr( "omlx.settings.BASE_PATH_BOOTSTRAP_FILE", Path("/nonexistent/oMLX/base-path"), ) assert resolve_default_base_path() == Path.home() / ".omlx" def test_uses_bootstrap_file_when_present(self, monkeypatch, tmp_path): monkeypatch.delenv("OMLX_BASE_PATH", raising=False) custom_base = tmp_path / "external-ssd" / "omlx-data" bootstrap_file = tmp_path / "base-path" bootstrap_file.write_text(f"{custom_base}\n", encoding="utf-8") monkeypatch.setattr("omlx.settings.BASE_PATH_BOOTSTRAP_FILE", bootstrap_file) assert resolve_default_base_path() == custom_base.resolve() def test_env_var_wins_over_bootstrap_file(self, monkeypatch, tmp_path): env_base = tmp_path / "env-base" bootstrap_base = tmp_path / "bootstrap-base" bootstrap_file = tmp_path / "base-path" bootstrap_file.write_text(str(bootstrap_base), encoding="utf-8") monkeypatch.setattr("omlx.settings.BASE_PATH_BOOTSTRAP_FILE", bootstrap_file) monkeypatch.setenv("OMLX_BASE_PATH", str(env_base)) assert resolve_default_base_path() == env_base.resolve() def test_empty_bootstrap_file_falls_back_to_default(self, monkeypatch, tmp_path): monkeypatch.delenv("OMLX_BASE_PATH", raising=False) bootstrap_file = tmp_path / "base-path" bootstrap_file.write_text(" \n", encoding="utf-8") monkeypatch.setattr("omlx.settings.BASE_PATH_BOOTSTRAP_FILE", bootstrap_file) assert resolve_default_base_path() == Path.home() / ".omlx" def test_global_settings_load_uses_resolver_when_no_base_path_given( self, monkeypatch, tmp_path ): resolved = tmp_path / "resolved-base" monkeypatch.setattr( "omlx.settings.resolve_default_base_path", lambda: resolved ) settings = GlobalSettings.load() assert settings.base_path == resolved class TestSettingsVersionMigration: """Tests for settings version handling.""" def test_load_different_version_logs_migration(self): """Test loading settings with different version logs migration.""" with tempfile.TemporaryDirectory() as tmpdir: settings_file = Path(tmpdir) / "settings.json" settings_file.write_text( json.dumps({"version": "0.9", "server": {"port": 9000}}) ) settings = GlobalSettings.load(base_path=tmpdir) # Should still load the settings assert settings.server.port == 9000 def test_load_without_version(self): """Test loading settings file without version field.""" with tempfile.TemporaryDirectory() as tmpdir: settings_file = Path(tmpdir) / "settings.json" settings_file.write_text(json.dumps({"server": {"port": 9000}})) settings = GlobalSettings.load(base_path=tmpdir) assert settings.server.port == 9000 class TestSettingsEdgeCases: """Tests for edge cases and error handling.""" def test_empty_settings_file(self): """Test loading empty settings file.""" with tempfile.TemporaryDirectory() as tmpdir: settings_file = Path(tmpdir) / "settings.json" settings_file.write_text("{}") settings = GlobalSettings.load(base_path=tmpdir) # Should use all defaults assert settings.server.port == 8000 assert settings.server.host == "127.0.0.1" def test_partial_section_in_file(self): """Test loading file with partial section data.""" with tempfile.TemporaryDirectory() as tmpdir: settings_file = Path(tmpdir) / "settings.json" settings_file.write_text( json.dumps( { "version": "1.0", "server": {"port": 9000}, # host and log_level should use defaults } ) ) settings = GlobalSettings.load(base_path=tmpdir) assert settings.server.port == 9000 assert settings.server.host == "127.0.0.1" # default assert settings.server.log_level == "info" # default def test_cli_args_without_expected_attrs(self): """Test CLI args object without all expected attributes.""" with tempfile.TemporaryDirectory() as tmpdir: # Minimal args object args = Namespace(port=9000) settings = GlobalSettings.load(base_path=tmpdir, cli_args=args) assert settings.server.port == 9000 # Other settings should be defaults assert settings.server.host == "127.0.0.1" def test_save_with_unicode_api_key(self): """Test saving settings with unicode in values.""" with tempfile.TemporaryDirectory() as tmpdir: settings = GlobalSettings(base_path=Path(tmpdir)) settings.auth.api_key = "key-with-unicode-\u4e2d\u6587" settings.save() # Reload and verify loaded = GlobalSettings.load(base_path=tmpdir) assert loaded.auth.api_key == "key-with-unicode-\u4e2d\u6587" class TestSamplingSettings: """Tests for SamplingSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = SamplingSettings() # Fallback default kept at 32768 so existing settings.json # files carrying the historical default keep working unchanged # after upgrade. ``max_context_window_policy`` is the explicit # operator policy cap (None by default). assert settings.max_context_window == 32768 assert settings.max_context_window_policy is None assert settings.max_tokens == 32768 assert settings.temperature == 1.0 assert settings.top_p == 0.95 assert settings.top_k == 0 assert settings.repetition_penalty == 1.0 def test_to_dict(self): """Test conversion to dictionary.""" settings = SamplingSettings(max_context_window=4096) d = settings.to_dict() assert d["max_context_window"] == 4096 assert "max_tokens" in d assert "repetition_penalty" in d def test_from_dict(self): """Test creation from dictionary.""" data = { "max_context_window": 8192, "max_tokens": 1024, "repetition_penalty": 1.2, } settings = SamplingSettings.from_dict(data) assert settings.max_context_window == 8192 assert settings.max_tokens == 1024 assert settings.repetition_penalty == 1.2 def test_from_dict_defaults(self): """Test from_dict uses defaults for missing fields.""" settings = SamplingSettings.from_dict({}) assert settings.max_context_window == 32768 assert settings.max_context_window_policy is None assert settings.repetition_penalty == 1.0 def test_policy_field_round_trip(self): """``max_context_window_policy`` must serialize and deserialize without losing its ``None`` semantics.""" unset = SamplingSettings.from_dict({}) assert unset.max_context_window_policy is None # to_dict preserves None d = unset.to_dict() assert d["max_context_window_policy"] is None # Setting an explicit value round-trips with_policy = SamplingSettings.from_dict({"max_context_window_policy": 128_000}) assert with_policy.max_context_window_policy == 128_000 assert with_policy.to_dict()["max_context_window_policy"] == 128_000 class TestClaudeCodeSettings: """Tests for ClaudeCodeSettings dataclass.""" def test_defaults(self): """Test default values.""" settings = ClaudeCodeSettings() assert settings.context_scaling_enabled is False assert settings.target_context_size == 200000 def test_custom_values(self): """Test custom values.""" settings = ClaudeCodeSettings( context_scaling_enabled=True, target_context_size=131072 ) assert settings.context_scaling_enabled is True assert settings.target_context_size == 131072 def test_to_dict(self): """Test conversion to dictionary.""" settings = ClaudeCodeSettings( context_scaling_enabled=True, target_context_size=100000 ) result = settings.to_dict() assert result["context_scaling_enabled"] is True assert result["target_context_size"] == 100000 assert result["mode"] == "cloud" assert result["opus_model"] is None assert result["sonnet_model"] is None assert result["haiku_model"] is None def test_from_dict(self): """Test creation from dictionary.""" data = {"context_scaling_enabled": True, "target_context_size": 131072} settings = ClaudeCodeSettings.from_dict(data) assert settings.context_scaling_enabled is True assert settings.target_context_size == 131072 def test_from_dict_defaults(self): """Test from_dict uses defaults for missing fields.""" settings = ClaudeCodeSettings.from_dict({}) assert settings.context_scaling_enabled is False assert settings.target_context_size == 200000 def test_new_fields_defaults(self): """Test that the four new fields have correct defaults.""" settings = ClaudeCodeSettings() assert settings.mode == "cloud" assert settings.opus_model is None assert settings.sonnet_model is None assert settings.haiku_model is None def test_new_fields_to_dict(self): """Test that to_dict includes all four new fields.""" settings = ClaudeCodeSettings( mode="local", opus_model="mlx-community/Qwen3-30B-A3B-4bit", sonnet_model="mlx-community/Qwen3-14B-4bit", haiku_model="mlx-community/Qwen3-4B-4bit", ) result = settings.to_dict() assert result["mode"] == "local" assert result["opus_model"] == "mlx-community/Qwen3-30B-A3B-4bit" assert result["sonnet_model"] == "mlx-community/Qwen3-14B-4bit" assert result["haiku_model"] == "mlx-community/Qwen3-4B-4bit" def test_new_fields_from_dict_full(self): """Test from_dict with all four new fields present.""" data = { "mode": "local", "opus_model": "mlx-community/Qwen3-30B-A3B-4bit", "sonnet_model": "mlx-community/Qwen3-14B-4bit", "haiku_model": "mlx-community/Qwen3-4B-4bit", } settings = ClaudeCodeSettings.from_dict(data) assert settings.mode == "local" assert settings.opus_model == "mlx-community/Qwen3-30B-A3B-4bit" assert settings.sonnet_model == "mlx-community/Qwen3-14B-4bit" assert settings.haiku_model == "mlx-community/Qwen3-4B-4bit" def test_new_fields_from_dict_backward_compat(self): """Test from_dict({}) gives correct defaults — simulates old settings.json.""" settings = ClaudeCodeSettings.from_dict({}) assert settings.mode == "cloud" assert settings.opus_model is None assert settings.sonnet_model is None assert settings.haiku_model is None def test_new_fields_from_dict_null_model(self): """Test from_dict with explicit null model values.""" data = {"mode": "cloud", "opus_model": None} settings = ClaudeCodeSettings.from_dict(data) assert settings.mode == "cloud" assert settings.opus_model is None class TestIntegrationSettings: """Tests for IntegrationSettings dataclass. Upstream ``tests/test_integrations.py::TestIntegrationSettings`` already covers defaults, basic to_dict, and full/empty from_dict. The local tests below add: exact dict-shape pinning (so a future field addition that forgets to_dict raises a loud test failure — see 81dc2d5 for the MemorySettings case), partial-dict fallback, explicit-null override semantics, and round-trip identity. Plus upstream's MarkItDown-integration tests merged in below. """ def test_to_dict_defaults(self): settings = IntegrationSettings() d = settings.to_dict() # Pin only the integration-model surface — MarkItDown additions # are covered by ``test_markitdown_defaults`` separately, so we # check the model fields exactly and leave the rest free to # grow. assert d["codex_model"] is None assert d["opencode_model"] is None assert d["openclaw_model"] is None assert d["hermes_model"] is None assert d["pi_model"] is None assert d["copilot_model"] is None assert d["openclaw_tools_profile"] == "coding" def test_to_dict_custom(self): settings = IntegrationSettings( codex_model="qwen-coder-30b", opencode_model="qwen-coder-7b", openclaw_model="qwen-coder-3b", hermes_model="hermes-3-8b", pi_model="qwen-3-4b", copilot_model="qwen-coder-1.5b", openclaw_tools_profile="creative", ) d = settings.to_dict() assert d["codex_model"] == "qwen-coder-30b" assert d["opencode_model"] == "qwen-coder-7b" assert d["openclaw_model"] == "qwen-coder-3b" assert d["hermes_model"] == "hermes-3-8b" assert d["pi_model"] == "qwen-3-4b" assert d["copilot_model"] == "qwen-coder-1.5b" assert d["openclaw_tools_profile"] == "creative" def test_from_dict_partial(self): """Missing keys fall back to dataclass defaults.""" settings = IntegrationSettings.from_dict({"pi_model": "qwen-3-4b"}) assert settings.pi_model == "qwen-3-4b" assert settings.codex_model is None assert settings.copilot_model is None assert settings.openclaw_tools_profile == "coding" def test_from_dict_explicit_null_overrides_default(self): """Explicit None for a *_model field must be preserved.""" settings = IntegrationSettings.from_dict({"codex_model": None, "pi_model": "x"}) assert settings.codex_model is None assert settings.pi_model == "x" def test_round_trip(self): """to_dict → from_dict → to_dict is identity.""" original = IntegrationSettings( codex_model="m1", pi_model="m2", openclaw_tools_profile="custom", ) round_tripped = IntegrationSettings.from_dict(original.to_dict()) assert round_tripped.to_dict() == original.to_dict() # --- MarkItDown integration tests merged in from upstream --- def test_markitdown_defaults(self): settings = IntegrationSettings() assert settings.markitdown_enabled is True assert settings.markitdown_expose_model is False assert settings.markitdown_max_file_size_mb == 25 assert settings.markitdown_max_files_per_request == 5 assert settings.markitdown_pdf_processing_engine == "markitdown" def test_markitdown_to_dict(self): settings = IntegrationSettings( markitdown_enabled=False, markitdown_expose_model=False, markitdown_max_file_size_mb=10, markitdown_max_files_per_request=2, markitdown_pdf_processing_engine="OCR-Model", ) result = settings.to_dict() assert result["markitdown_enabled"] is False assert result["markitdown_expose_model"] is False assert result["markitdown_max_file_size_mb"] == 10 assert result["markitdown_max_files_per_request"] == 2 assert result["markitdown_pdf_processing_engine"] == "OCR-Model" def test_markitdown_from_dict_backward_compat(self): settings = IntegrationSettings.from_dict({}) assert settings.markitdown_enabled is True assert settings.markitdown_expose_model is False assert settings.markitdown_max_file_size_mb == 25 assert settings.markitdown_max_files_per_request == 5 assert settings.markitdown_pdf_processing_engine == "markitdown" def test_markitdown_validation(self): settings = GlobalSettings() settings.integrations.markitdown_max_file_size_mb = 0 settings.integrations.markitdown_max_files_per_request = 0 settings.integrations.markitdown_pdf_processing_engine = "" errors = settings.validate() assert "markitdown_max_file_size_mb must be > 0" in errors assert "markitdown_max_files_per_request must be > 0" in errors assert "markitdown_pdf_processing_engine must not be empty" in errors class TestClaudeCodeValidation: """Tests for mode validation in GlobalSettings.validate().""" def _make_global_settings(self, mode: str) -> GlobalSettings: """Create a GlobalSettings with a specific claude_code.mode for testing.""" gs = GlobalSettings.__new__(GlobalSettings) # Copy defaults from a real instance then override claude_code real = GlobalSettings() gs.__dict__.update(real.__dict__) gs.claude_code = ClaudeCodeSettings(mode=mode) return gs def test_validate_mode_cloud_valid(self): """Mode 'cloud' passes validation.""" gs = self._make_global_settings("cloud") errors = gs.validate() mode_errors = [e for e in errors if "claude_code mode" in e] assert mode_errors == [] def test_validate_mode_local_valid(self): """Mode 'local' passes validation.""" gs = self._make_global_settings("local") errors = gs.validate() mode_errors = [e for e in errors if "claude_code mode" in e] assert mode_errors == [] def test_validate_mode_invalid(self): """Invalid mode produces a validation error.""" gs = self._make_global_settings("auto") errors = gs.validate() mode_errors = [e for e in errors if "claude_code mode" in e] assert len(mode_errors) == 1 assert "auto" in mode_errors[0] def test_validate_mode_empty_string_invalid(self): """Empty string mode is invalid.""" gs = self._make_global_settings("") errors = gs.validate() mode_errors = [e for e in errors if "claude_code mode" in e] assert len(mode_errors) == 1 class TestClaudeCodeRouteIntegration: """Integration tests for the settings chain: dataclass <-> dict <-> routes.""" def test_claude_code_to_dict_has_six_keys(self): """to_dict must include all six keys so GlobalSettings.save() persists them.""" s = ClaudeCodeSettings( context_scaling_enabled=True, target_context_size=100000, mode="local", opus_model="mlx-community/Qwen3-30B-A3B-4bit", sonnet_model="mlx-community/Qwen3-14B-4bit", haiku_model="mlx-community/Qwen3-4B-4bit", ) d = s.to_dict() expected_keys = { "context_scaling_enabled", "target_context_size", "mode", "opus_model", "sonnet_model", "haiku_model", } assert set(d.keys()) == expected_keys def test_claude_code_new_fields_round_trip(self): """Full round-trip: set values -> to_dict -> from_dict -> values match.""" original = ClaudeCodeSettings( mode="local", opus_model="mlx-community/Qwen3-30B-A3B-4bit", sonnet_model="mlx-community/Qwen3-14B-4bit", haiku_model="mlx-community/Qwen3-4B-4bit", ) reloaded = ClaudeCodeSettings.from_dict(original.to_dict()) assert reloaded.mode == "local" assert reloaded.opus_model == "mlx-community/Qwen3-30B-A3B-4bit" assert reloaded.sonnet_model == "mlx-community/Qwen3-14B-4bit" assert reloaded.haiku_model == "mlx-community/Qwen3-4B-4bit" def test_claude_code_round_trip_null_models(self): """Null model fields survive the round-trip.""" original = ClaudeCodeSettings(mode="cloud", opus_model=None) reloaded = ClaudeCodeSettings.from_dict(original.to_dict()) assert reloaded.mode == "cloud" assert reloaded.opus_model is None def test_post_handler_model_fields_set_explicit_null(self): """ GlobalSettingsRequest.model_validate with explicit null must include the field in model_fields_set so the POST handler can clear it. """ from omlx.admin.routes import GlobalSettingsRequest r = GlobalSettingsRequest.model_validate({"claude_code_opus_model": None}) assert "claude_code_opus_model" in r.model_fields_set assert r.claude_code_opus_model is None def test_post_handler_model_fields_set_absent_field(self): """ GlobalSettingsRequest() with no claude_code_opus_model must NOT include it in model_fields_set — POST handler must not apply it (leave server value alone). """ from omlx.admin.routes import GlobalSettingsRequest r = GlobalSettingsRequest() assert "claude_code_opus_model" not in r.model_fields_set def test_post_handler_model_fields_set_explicit_value(self): """ GlobalSettingsRequest with an explicit model ID must include the field in model_fields_set and carry the value. """ from omlx.admin.routes import GlobalSettingsRequest r = GlobalSettingsRequest( claude_code_opus_model="mlx-community/Qwen3-30B-A3B-4bit" ) assert "claude_code_opus_model" in r.model_fields_set assert r.claude_code_opus_model == "mlx-community/Qwen3-30B-A3B-4bit" class TestCORSMiddleware: """Test that CORS middleware is correctly applied to the server.""" def test_cors_preflight(self): """Test that CORS preflight requests get proper response headers.""" from fastapi.testclient import TestClient from omlx.server import app, init_server # Reset middleware stack so add_middleware works even if app was # already started by another test in the same process. app.middleware_stack = None with tempfile.TemporaryDirectory() as tmpdir: settings = GlobalSettings(base_path=Path(tmpdir)) init_server( model_dirs=[tmpdir], global_settings=settings, ) client = TestClient(app) resp = client.options( "/v1/models", headers={ "Origin": "https://example.com", "Access-Control-Request-Method": "GET", }, ) assert resp.status_code == 200 assert "access-control-allow-origin" in resp.headers assert resp.headers["access-control-allow-origin"] == "*"