Coverage for agentos/tests/test_config.py: 0%
237 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
1"""Tests for agentos.core.config — ConfigManager, ConfigSource, helpers."""
3import json
4import os
5import tempfile
6from dataclasses import dataclass
8import pytest
10from agentos.core.config import (
11 ConfigError,
12 ConfigManager,
13 ConfigNotFoundError,
14 ConfigSource,
15 SourceType,
16 _coerce,
17 _flatten_dict,
18 _parse_env_value,
19)
21# ============================================================================
22# _parse_env_value
23# ============================================================================
25class TestParseEnvValue:
26 def test_bool_true(self):
27 assert _parse_env_value("true") is True
28 assert _parse_env_value("yes") is True
29 assert _parse_env_value("on") is True
31 def test_bool_false(self):
32 assert _parse_env_value("false") is False
33 assert _parse_env_value("no") is False
34 assert _parse_env_value("off") is False
36 def test_null(self):
37 assert _parse_env_value("null") is None
38 assert _parse_env_value("none") is None
39 assert _parse_env_value("") is None
41 def test_int(self):
42 assert _parse_env_value("42") == 42
43 assert _parse_env_value("-10") == -10
45 def test_float(self):
46 assert _parse_env_value("3.14") == 3.14
48 def test_json(self):
49 assert _parse_env_value('[1,2,3]') == [1, 2, 3]
50 assert _parse_env_value('{"a":1}') == {"a": 1}
52 def test_fallback_to_string(self):
53 assert _parse_env_value("hello world") == "hello world"
56# ============================================================================
57# _flatten_dict
58# ============================================================================
60class TestFlattenDict:
61 def test_flat(self):
62 assert _flatten_dict({"a": 1, "b": 2}) == {"a": 1, "b": 2}
64 def test_nested(self):
65 d = {"database": {"host": "localhost", "port": 5432}}
66 result = _flatten_dict(d)
67 assert result == {"database_host": "localhost", "database_port": 5432}
69 def test_deep_nested(self):
70 d = {"a": {"b": {"c": 1}}}
71 assert _flatten_dict(d) == {"a_b_c": 1}
73 def test_skip_environ_style(self):
74 d = {"DB_HOST": "1", "nested": {"x": 2}}
75 result = _flatten_dict(d)
76 assert "db_host" in result
77 assert "nested_x" in result
79 def test_skip_list_value(self):
80 d = {"items": [1, 2, 3]}
81 assert _flatten_dict(d) == {"items": [1, 2, 3]}
84# ============================================================================
85# _coerce
86# ============================================================================
88class TestCoerce:
89 def test_none(self):
90 assert _coerce(None, str) is None
92 def test_bool_from_str(self):
93 assert _coerce("true", bool) is True
95 def test_bool_already_bool(self):
96 assert _coerce(True, bool) is True
98 def test_int(self):
99 assert _coerce("42", int) == 42
101 def test_float(self):
102 assert _coerce("3.14", float) == 3.14
104 def test_str(self):
105 assert _coerce(42, str) == "42"
107 def test_list_from_str(self):
108 assert _coerce("a,b,c", list) == ["a", "b", "c"]
110 def test_list_already_list(self):
111 assert _coerce([1, 2], list) == [1, 2]
113 def test_dict(self):
114 assert _coerce({"a": 1}, dict) == {"a": 1}
116 def test_optional_pass(self):
117 assert _coerce("hello", str | None) == "hello"
118 assert _coerce(None, str | None) is None
120 def test_dataclass_coerce(self):
121 @dataclass
122 class Sub:
123 x: int = 1
124 result = _coerce({"x": 42}, Sub)
125 assert result.x == 42
128# ============================================================================
129# ConfigSource
130# ============================================================================
132class TestConfigSource:
133 def test_from_dict(self):
134 s = ConfigSource.from_dict({"a": 1, "b": "hello"})
135 assert s.source_type == SourceType.DICT
136 assert s.data == {"a": 1, "b": "hello"}
137 assert s.precedence == 500
139 def test_from_env(self):
140 os.environ["TEST_CFG_KEY"] = "42"
141 s = ConfigSource.from_env(prefix="TEST_CFG_")
142 assert "key" in s.data
143 assert s.data["key"] == 42
144 del os.environ["TEST_CFG_KEY"]
146 def test_from_json(self):
147 with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
148 json.dump({"host": "localhost", "port": 5432}, f)
149 f.flush()
150 s = ConfigSource.from_json(f.name)
151 os.unlink(f.name)
152 assert s.data["host"] == "localhost"
153 assert s.data["port"] == 5432
155 def test_from_dotenv(self):
156 with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f:
157 f.write("HOST=localhost\nPORT=8080\nDEBUG=true\n")
158 f.flush()
159 s = ConfigSource.from_dotenv(f.name)
160 os.unlink(f.name)
161 assert s.data["host"] == "localhost"
162 assert s.data["port"] == 8080
163 assert s.data["debug"] is True
165 def test_from_dotenv_not_found(self):
166 with pytest.raises(ConfigError):
167 ConfigSource.from_dotenv("/nonexistent/path/.env")
169 def test_from_dotenv_not_found_override(self):
170 s = ConfigSource.from_dotenv("/nonexistent/path/.env", override=True)
171 assert s.data == {}
173 def test_from_yaml_not_found(self):
174 with pytest.raises(ConfigError):
175 ConfigSource.from_yaml("/nonexistent/config.yaml")
177 def test_from_json_not_found(self):
178 with pytest.raises(ConfigError):
179 ConfigSource.from_json("/nonexistent/config.json")
182# ============================================================================
183# ConfigManager
184# ============================================================================
186class TestConfigManager:
187 def test_get_basic(self):
188 cm = ConfigManager(auto_env=False)
189 cm.add_source(ConfigSource.from_dict({"host": "localhost"}))
190 assert cm.get("host") == "localhost"
192 def test_get_missing_raises(self):
193 cm = ConfigManager(auto_env=False)
194 with pytest.raises(ConfigNotFoundError):
195 cm.get("nonexistent")
197 def test_get_default(self):
198 cm = ConfigManager(auto_env=False)
199 assert cm.get("missing", "default") == "default"
201 def test_get_str(self):
202 cm = ConfigManager(auto_env=False)
203 cm.add_source(ConfigSource.from_dict({"port": 8080}))
204 assert cm.get_str("port") == "8080"
206 def test_get_int(self):
207 cm = ConfigManager(auto_env=False)
208 cm.add_source(ConfigSource.from_dict({"port": "8080"}))
209 assert cm.get_int("port") == 8080
211 def test_get_float(self):
212 cm = ConfigManager(auto_env=False)
213 cm.add_source(ConfigSource.from_dict({"rate": "3.14"}))
214 assert cm.get_float("rate") == 3.14
216 def test_get_bool(self):
217 cm = ConfigManager(auto_env=False)
218 cm.add_source(ConfigSource.from_dict({"debug": "true", "verbose": "yes"}))
219 assert cm.get_bool("debug") is True
220 assert cm.get_bool("verbose") is True
222 def test_get_bool_real_false(self):
223 cm = ConfigManager(auto_env=False)
224 cm.add_source(ConfigSource.from_dict({"debug": False}))
225 assert cm.get_bool("debug") is False
227 def test_get_list_from_str(self):
228 cm = ConfigManager(auto_env=False)
229 cm.add_source(ConfigSource.from_dict({"hosts": "a,b,c"}))
230 assert cm.get_list("hosts") == ["a", "b", "c"]
232 def test_get_list_already_list(self):
233 cm = ConfigManager(auto_env=False)
234 cm.add_source(ConfigSource.from_dict({"hosts": ["a", "b"]}))
235 assert cm.get_list("hosts") == ["a", "b"]
237 def test_get_dict(self):
238 cm = ConfigManager(auto_env=False)
239 cm.add_source(ConfigSource.from_dict({"db": {"host": "localhost"}}))
240 assert cm.get_dict("db") == {"host": "localhost"}
242 def test_keys(self):
243 cm = ConfigManager(auto_env=False)
244 cm.add_source(ConfigSource.from_dict({"a": 1, "b": 2}))
245 assert cm.keys() == ["a", "b"]
247 def test_to_dict(self):
248 cm = ConfigManager(auto_env=False)
249 cm.add_source(ConfigSource.from_dict({"a": 1}))
250 assert cm.to_dict() == {"a": 1}
252 def test_mask_secrets(self):
253 cm = ConfigManager(auto_env=False)
254 cm.add_source(ConfigSource.from_dict({"password": "secret123", "host": "localhost"}))
255 cm.mark_secret("password")
256 d = cm.to_dict(mask_secrets=True)
257 assert d["password"] == "***MASKED***"
258 assert d["host"] == "localhost"
260 def test_precedence(self):
261 cm = ConfigManager(auto_env=False)
262 cm.add_source(ConfigSource.from_dict({"key": "low"}, precedence=500))
263 cm.add_source(ConfigSource.from_dict({"key": "high"}, precedence=100))
264 assert cm.get("key") == "high"
266 def test_set(self):
267 cm = ConfigManager(auto_env=False)
268 cm.add_source(ConfigSource.from_dict({"key": "old"}))
269 cm.set("key", "new")
270 assert cm.get("key") == "new"
272 def test_on_change(self):
273 cm = ConfigManager(auto_env=False)
274 changes = []
276 def cb(key, old, new):
277 changes.append((key, old, new))
279 cm.on_change(cb)
280 cm.add_source(ConfigSource.from_dict({"key": "old"}))
281 cm.set("key", "new")
282 assert changes == [("key", "old", "new")]
284 def test_reload(self):
285 cm = ConfigManager(auto_env=False)
286 cm.add_source(ConfigSource.from_dict({"key": "v1"}))
287 assert cm.get("key") == "v1"
288 cm.reload()
289 assert cm.get("key") == "v1"
291 def test_repr(self):
292 cm = ConfigManager(auto_env=False)
293 cm.add_source(ConfigSource.from_dict({"a": 1}))
294 r = repr(cm)
295 assert "ConfigManager" in r
296 assert "sources=1" in r
298 def test_custom_separator(self):
299 cm = ConfigManager(auto_env=False)
300 cm.add_source(ConfigSource.from_dict({"items": "a|b|c"}))
301 assert cm.get_list("items", separator="|") == ["a", "b", "c"]
303 def test_auto_env(self):
304 os.environ["AGENTOS_TEST_X"] = "hello"
305 cm = ConfigManager(auto_env=True, env_prefix="AGENTOS_")
306 assert cm.get("test_x") == "hello"
307 del os.environ["AGENTOS_TEST_X"]
309 def test_bind_dataclass(self):
310 @dataclass
311 class AppConfig:
312 host: str = "0.0.0.0"
313 port: int = 8080
314 debug: bool = False
316 cm = ConfigManager(auto_env=False)
317 cm.add_source(ConfigSource.from_dict({"host": "10.0.0.1", "port": "3000", "debug": "true"}))
318 cfg = cm.bind(AppConfig)
319 assert cfg.host == "10.0.0.1"
320 assert cfg.port == 3000
321 assert cfg.debug is True
323 def test_bind_defaults(self):
324 @dataclass
325 class AppConfig:
326 host: str = "0.0.0.0"
327 port: int = 8080
329 cm = ConfigManager(auto_env=False)
330 cfg = cm.bind(AppConfig)
331 assert cfg.host == "0.0.0.0"
332 assert cfg.port == 8080