Skip to content

Commit c8c6bbb

Browse files
committed
Fix for model not listing after import
1 parent ed3fbde commit c8c6bbb

File tree

2 files changed

+137
-56
lines changed

2 files changed

+137
-56
lines changed

src/server/api/utils/settings.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,26 @@ def _load_prompt_configs(config_data: dict) -> None:
164164

165165

166166
def update_server(config_data: dict) -> None:
167-
"""Update server configuration"""
167+
"""Update server configuration.
168+
169+
Note: We mutate the existing lists (clear + extend) rather than replacing them.
170+
This is critical because other modules import these lists directly
171+
(e.g., `from server.bootstrap.bootstrap import DATABASE_OBJECTS`).
172+
Replacing the list would leave those modules with stale references.
173+
"""
168174
config = Configuration(**config_data)
169175

170176
if "database_configs" in config_data:
171-
bootstrap.DATABASE_OBJECTS = config.database_configs or []
177+
bootstrap.DATABASE_OBJECTS.clear()
178+
bootstrap.DATABASE_OBJECTS.extend(config.database_configs or [])
172179

173180
if "model_configs" in config_data:
174-
bootstrap.MODEL_OBJECTS = config.model_configs or []
181+
bootstrap.MODEL_OBJECTS.clear()
182+
bootstrap.MODEL_OBJECTS.extend(config.model_configs or [])
175183

176184
if "oci_configs" in config_data:
177-
bootstrap.OCI_OBJECTS = config.oci_configs or []
185+
bootstrap.OCI_OBJECTS.clear()
186+
bootstrap.OCI_OBJECTS.extend(config.oci_configs or [])
178187

179188
# Load MCP prompt text into cache from prompt_configs
180189
_load_prompt_configs(config_data)

tests/server/unit/api/utils/test_utils_settings.py

Lines changed: 124 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,61 +14,105 @@
1414
from common.schema import Settings, Configuration, Database, Model, OracleCloudSettings
1515

1616

17-
class TestSettings:
18-
"""Test settings module functionality"""
19-
20-
def __init__(self):
21-
"""Setup test data for all tests"""
22-
self.default_settings = Settings(client="default")
23-
self.test_client_settings = Settings(client="test_client")
24-
self.sample_config_data = {
25-
"database_configs": [{"name": "test_db", "user": "user", "password": "pass", "dsn": "dsn"}],
26-
"model_configs": [{"id": "test-model", "provider": "openai", "type": "ll"}],
27-
"oci_configs": [{"auth_profile": "DEFAULT", "compartment_id": "ocid1.compartment.oc1..test"}],
28-
"prompt_overrides": {"optimizer_basic-default": "You are helpful"},
29-
"client_settings": {"client": "default", "max_tokens": 1000, "temperature": 0.7},
30-
}
17+
#####################################################
18+
# Helper functions for test data
19+
#####################################################
20+
def make_default_settings():
21+
"""Create default settings for tests"""
22+
return Settings(client="default")
23+
24+
25+
def make_test_client_settings():
26+
"""Create test client settings for tests"""
27+
return Settings(client="test_client")
28+
29+
30+
def make_sample_config_data():
31+
"""Create sample configuration data for tests"""
32+
return {
33+
"database_configs": [{"name": "test_db", "user": "user", "password": "pass", "dsn": "dsn"}],
34+
"model_configs": [{"id": "test-model", "provider": "openai", "type": "ll"}],
35+
"oci_configs": [{"auth_profile": "DEFAULT", "compartment_id": "ocid1.compartment.oc1..test"}],
36+
"prompt_overrides": {"optimizer_basic-default": "You are helpful"},
37+
"client_settings": {"client": "default", "max_tokens": 1000, "temperature": 0.7},
38+
}
39+
40+
41+
#####################################################
42+
# Client Settings Tests
43+
#####################################################
44+
class TestClientSettings:
45+
"""Test client settings CRUD operations"""
3146

3247
@patch("server.api.utils.settings.bootstrap")
3348
def test_create_client_success(self, mock_bootstrap):
3449
"""Test successful client settings creation"""
35-
# Create a list that includes the default settings and will be appended to
36-
settings_list = [self.default_settings]
50+
default_cfg = make_default_settings()
51+
settings_list = [default_cfg]
3752
mock_bootstrap.SETTINGS_OBJECTS = settings_list
3853

3954
result = settings.create_client("new_client")
4055

4156
assert result.client == "new_client"
42-
assert result.ll_model.max_tokens == self.default_settings.ll_model.max_tokens # pylint: disable=no-member
43-
# Check that a new client was added to the list
57+
# Verify ll_model settings are copied from default
58+
result_ll_model = result.model_dump()["ll_model"]
59+
default_ll_model = default_cfg.model_dump()["ll_model"]
60+
assert result_ll_model["max_tokens"] == default_ll_model["max_tokens"]
4461
assert len(settings_list) == 2
4562
assert settings_list[-1].client == "new_client"
4663

4764
@patch("server.api.utils.settings.bootstrap.SETTINGS_OBJECTS")
4865
def test_create_client_already_exists(self, mock_settings_objects):
4966
"""Test creating client settings when client already exists"""
50-
mock_settings_objects.__iter__ = MagicMock(return_value=iter([self.test_client_settings]))
67+
test_cfg = make_test_client_settings()
68+
mock_settings_objects.__iter__ = MagicMock(return_value=iter([test_cfg]))
5169

5270
with pytest.raises(ValueError, match="client test_client already exists"):
5371
settings.create_client("test_client")
5472

5573
@patch("server.api.utils.settings.bootstrap.SETTINGS_OBJECTS")
5674
def test_get_client_found(self, mock_settings_objects):
5775
"""Test getting client settings when client exists"""
58-
mock_settings_objects.__iter__ = MagicMock(return_value=iter([self.test_client_settings]))
76+
test_cfg = make_test_client_settings()
77+
mock_settings_objects.__iter__ = MagicMock(return_value=iter([test_cfg]))
5978

6079
result = settings.get_client("test_client")
6180

62-
assert result == self.test_client_settings
81+
assert result == test_cfg
6382

6483
@patch("server.api.utils.settings.bootstrap.SETTINGS_OBJECTS")
6584
def test_get_client_not_found(self, mock_settings_objects):
6685
"""Test getting client settings when client doesn't exist"""
67-
mock_settings_objects.__iter__ = MagicMock(return_value=iter([self.default_settings]))
86+
default_cfg = make_default_settings()
87+
mock_settings_objects.__iter__ = MagicMock(return_value=iter([default_cfg]))
6888

6989
with pytest.raises(ValueError, match="client nonexistent not found"):
7090
settings.get_client("nonexistent")
7191

92+
@patch("server.api.utils.settings.bootstrap.SETTINGS_OBJECTS")
93+
@patch("server.api.utils.settings.get_client")
94+
def test_update_client(self, mock_get_settings, mock_settings_objects):
95+
"""Test updating client settings"""
96+
test_cfg = make_test_client_settings()
97+
mock_get_settings.return_value = test_cfg
98+
mock_settings_objects.remove = MagicMock()
99+
mock_settings_objects.append = MagicMock()
100+
mock_settings_objects.__iter__ = MagicMock(return_value=iter([test_cfg]))
101+
102+
new_settings = Settings(client="test_client", max_tokens=800, temperature=0.9)
103+
result = settings.update_client(new_settings, "test_client")
104+
105+
assert result.client == "test_client"
106+
mock_settings_objects.remove.assert_called_once_with(test_cfg)
107+
mock_settings_objects.append.assert_called_once()
108+
109+
110+
#####################################################
111+
# Server Configuration Tests
112+
#####################################################
113+
class TestServerConfiguration:
114+
"""Test server configuration operations"""
115+
72116
@pytest.mark.asyncio
73117
@patch("server.api.utils.settings.get_mcp_prompts_with_overrides")
74118
@patch("server.api.utils.settings.bootstrap.DATABASE_OBJECTS")
@@ -81,7 +125,7 @@ async def test_get_server(self, mock_oci, mock_models, mock_databases, mock_get_
81125
)
82126
mock_models.__iter__ = MagicMock(return_value=iter([Model(id="test", provider="openai", type="ll")]))
83127
mock_oci.__iter__ = MagicMock(return_value=iter([OracleCloudSettings(auth_profile="DEFAULT")]))
84-
mock_get_prompts.return_value = [] # Return empty list of prompts
128+
mock_get_prompts.return_value = []
85129

86130
mock_mcp_engine = MagicMock()
87131
result = await settings.get_server(mock_mcp_engine)
@@ -91,54 +135,79 @@ async def test_get_server(self, mock_oci, mock_models, mock_databases, mock_get_
91135
assert "oci_configs" in result
92136
assert "prompt_configs" in result
93137

94-
@patch("server.api.utils.settings.bootstrap.SETTINGS_OBJECTS")
95-
@patch("server.api.utils.settings.get_client")
96-
def test_update_client(self, mock_get_settings, mock_settings_objects):
97-
"""Test updating client settings"""
98-
mock_get_settings.return_value = self.test_client_settings
99-
mock_settings_objects.remove = MagicMock()
100-
mock_settings_objects.append = MagicMock()
101-
mock_settings_objects.__iter__ = MagicMock(return_value=iter([self.test_client_settings]))
102-
103-
new_settings = Settings(client="test_client", max_tokens=800, temperature=0.9)
104-
result = settings.update_client(new_settings, "test_client")
105-
106-
assert result.client == "test_client"
107-
mock_settings_objects.remove.assert_called_once_with(self.test_client_settings)
108-
mock_settings_objects.append.assert_called_once()
109-
110138
@patch("server.api.utils.settings.bootstrap")
111139
def test_update_server(self, mock_bootstrap):
112140
"""Test updating server configuration"""
113-
# Use the valid sample config data that includes client_settings
114-
settings.update_server(self.sample_config_data)
141+
mock_bootstrap.DATABASE_OBJECTS = []
142+
mock_bootstrap.MODEL_OBJECTS = []
143+
mock_bootstrap.OCI_OBJECTS = []
144+
145+
settings.update_server(make_sample_config_data())
115146

116147
assert hasattr(mock_bootstrap, "DATABASE_OBJECTS")
117148
assert hasattr(mock_bootstrap, "MODEL_OBJECTS")
118149

150+
@patch("server.api.utils.settings.bootstrap")
151+
def test_update_server_mutates_lists_not_replaces(self, mock_bootstrap):
152+
"""Test that update_server mutates existing lists rather than replacing them.
153+
154+
This is critical because other modules import these lists directly
155+
(e.g., `from server.bootstrap.bootstrap import DATABASE_OBJECTS`).
156+
If we replace the list, those modules would hold stale references.
157+
"""
158+
original_db_list = []
159+
original_model_list = []
160+
original_oci_list = []
161+
162+
mock_bootstrap.DATABASE_OBJECTS = original_db_list
163+
mock_bootstrap.MODEL_OBJECTS = original_model_list
164+
mock_bootstrap.OCI_OBJECTS = original_oci_list
165+
166+
settings.update_server(make_sample_config_data())
167+
168+
# Verify the lists are the SAME objects (mutated, not replaced)
169+
assert mock_bootstrap.DATABASE_OBJECTS is original_db_list, "DATABASE_OBJECTS was replaced instead of mutated"
170+
assert mock_bootstrap.MODEL_OBJECTS is original_model_list, "MODEL_OBJECTS was replaced instead of mutated"
171+
assert mock_bootstrap.OCI_OBJECTS is original_oci_list, "OCI_OBJECTS was replaced instead of mutated"
172+
173+
# Verify the lists now contain the new data
174+
assert len(original_db_list) == 1
175+
assert original_db_list[0].name == "test_db"
176+
assert len(original_model_list) == 1
177+
assert original_model_list[0].id == "test-model"
178+
assert len(original_oci_list) == 1
179+
assert original_oci_list[0].auth_profile == "DEFAULT"
180+
181+
182+
#####################################################
183+
# Config Loading Tests
184+
#####################################################
185+
class TestConfigLoading:
186+
"""Test configuration loading operations"""
187+
119188
@patch("server.api.utils.settings.update_server")
120189
@patch("server.api.utils.settings.update_client")
121190
def test_load_config_from_json_data_with_client(self, mock_update_client, mock_update_server):
122191
"""Test loading config from JSON data with specific client"""
123-
settings.load_config_from_json_data(self.sample_config_data, client="test_client")
192+
config_data = make_sample_config_data()
193+
settings.load_config_from_json_data(config_data, client="test_client")
124194

125-
mock_update_server.assert_called_once_with(self.sample_config_data)
195+
mock_update_server.assert_called_once_with(config_data)
126196
mock_update_client.assert_called_once()
127197

128198
@patch("server.api.utils.settings.update_server")
129199
@patch("server.api.utils.settings.update_client")
130200
def test_load_config_from_json_data_without_client(self, mock_update_client, mock_update_server):
131201
"""Test loading config from JSON data without specific client"""
132-
settings.load_config_from_json_data(self.sample_config_data)
202+
config_data = make_sample_config_data()
203+
settings.load_config_from_json_data(config_data)
133204

134-
mock_update_server.assert_called_once_with(self.sample_config_data)
135-
# Should be called twice: once for "server" and once for "default"
205+
mock_update_server.assert_called_once_with(config_data)
136206
assert mock_update_client.call_count == 2
137207

138208
@patch("server.api.utils.settings.update_server")
139209
def test_load_config_from_json_data_missing_client_settings(self, _mock_update_server):
140210
"""Test loading config from JSON data without client_settings"""
141-
# Create config without client_settings
142211
invalid_config = {"database_configs": [], "model_configs": [], "oci_configs": [], "prompt_configs": []}
143212

144213
with pytest.raises(KeyError, match="Missing client_settings in config file"):
@@ -153,7 +222,7 @@ def test_read_config_from_json_file_success(self, mock_json_load, mock_access, m
153222
"""Test successful reading of config file"""
154223
mock_isfile.return_value = True
155224
mock_access.return_value = True
156-
mock_json_load.return_value = self.sample_config_data
225+
mock_json_load.return_value = make_sample_config_data()
157226

158227
result = settings.read_config_from_json_file()
159228

@@ -166,19 +235,22 @@ def test_read_config_from_json_file_not_exists(self, mock_isfile):
166235
"""Test reading config file that doesn't exist"""
167236
mock_isfile.return_value = False
168237

169-
# This should still attempt to process, but will log a warning
170-
# The actual behavior depends on the implementation
171-
172238
@patch.dict(os.environ, {"CONFIG_FILE": "/path/to/config.txt"})
173239
def test_read_config_from_json_file_wrong_extension(self):
174240
"""Test reading config file with wrong extension"""
175-
# This should log a warning about the file extension
176241

177242
def test_logger_exists(self):
178243
"""Test that logger is properly configured"""
179244
assert hasattr(settings, "logger")
180245
assert settings.logger.name == "api.core.settings"
181246

247+
248+
#####################################################
249+
# Prompt Override Tests
250+
#####################################################
251+
class TestPromptOverrides:
252+
"""Test prompt override operations"""
253+
182254
@patch("server.api.utils.settings.cache")
183255
def test_load_prompt_override_with_text(self, mock_cache):
184256
"""Test loading prompt override when text is provided"""

0 commit comments

Comments
 (0)