atlas-chat 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,617 @@
1
+ """Unit tests for ConfigManager.
2
+
3
+ Tests the centralized configuration management system without
4
+ modifying the actual environment or configuration files.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from atlas.modules.config.config_manager import (
12
+ AppSettings,
13
+ ConfigManager,
14
+ LLMConfig,
15
+ MCPConfig,
16
+ MCPServerConfig,
17
+ resolve_env_var,
18
+ )
19
+
20
+
21
+ class TestConfigManager:
22
+ """Test ConfigManager initialization and basic functionality."""
23
+
24
+ def test_config_manager_initialization(self):
25
+ """ConfigManager should initialize without errors."""
26
+ cm = ConfigManager()
27
+ assert cm is not None
28
+ assert cm._backend_root.name == "atlas"
29
+
30
+ def test_app_settings_loads(self):
31
+ """AppSettings should load with defaults or environment values."""
32
+ cm = ConfigManager()
33
+ settings = cm.app_settings
34
+
35
+ assert settings is not None
36
+ assert isinstance(settings, AppSettings)
37
+ assert settings.app_name is not None
38
+ assert settings.port > 0
39
+ assert settings.log_level in ["DEBUG", "INFO", "WARNING", "ERROR"]
40
+
41
+ def test_llm_config_loads(self):
42
+ """LLM config should load from config files."""
43
+ cm = ConfigManager()
44
+ llm_config = cm.llm_config
45
+
46
+ assert llm_config is not None
47
+ assert isinstance(llm_config, LLMConfig)
48
+ # Should have at least some models configured
49
+ assert hasattr(llm_config, "models")
50
+
51
+ def test_mcp_config_loads(self):
52
+ """MCP config should load from config files."""
53
+ cm = ConfigManager()
54
+ mcp_config = cm.mcp_config
55
+
56
+ assert mcp_config is not None
57
+ assert isinstance(mcp_config, MCPConfig)
58
+ assert hasattr(mcp_config, "servers")
59
+
60
+ def test_config_manager_caches_settings(self):
61
+ """ConfigManager should cache settings and return same instance."""
62
+ cm = ConfigManager()
63
+
64
+ # Get settings twice
65
+ settings1 = cm.app_settings
66
+ settings2 = cm.app_settings
67
+
68
+ # Should be the exact same object (cached)
69
+ assert settings1 is settings2
70
+
71
+ def test_config_manager_caches_llm_config(self):
72
+ """ConfigManager should cache LLM config."""
73
+ cm = ConfigManager()
74
+
75
+ config1 = cm.llm_config
76
+ config2 = cm.llm_config
77
+
78
+ assert config1 is config2
79
+
80
+ def test_search_paths_returns_list(self):
81
+ """Search paths should return a list of Path objects."""
82
+ cm = ConfigManager()
83
+
84
+ paths = cm._search_paths("llmconfig.yml")
85
+
86
+ assert isinstance(paths, list)
87
+ assert len(paths) > 0
88
+ assert all(isinstance(p, Path) for p in paths)
89
+
90
+ def test_search_paths_includes_overrides_and_defaults(self):
91
+ """Search paths should include both overrides and defaults directories."""
92
+ cm = ConfigManager()
93
+
94
+ paths = cm._search_paths("mcp.json")
95
+ path_strings = [str(p) for p in paths]
96
+
97
+ # Should include overrides directory
98
+ assert any("overrides" in p for p in path_strings)
99
+ # Should include defaults directory
100
+ assert any("defaults" in p for p in path_strings)
101
+
102
+ def test_validate_config_returns_dict(self):
103
+ """Validate config should return a dictionary of validation results."""
104
+ cm = ConfigManager()
105
+
106
+ result = cm.validate_config()
107
+
108
+ assert isinstance(result, dict)
109
+ assert "app_settings" in result
110
+ assert "llm_config" in result
111
+ assert "mcp_config" in result
112
+ # All should be boolean values
113
+ assert all(isinstance(v, bool) for v in result.values())
114
+
115
+ def test_reload_configs_works(self):
116
+ """Reload configs should clear cache and reload."""
117
+ cm = ConfigManager()
118
+
119
+ # Load configs first
120
+ _ = cm.app_settings
121
+ _ = cm.llm_config
122
+
123
+ # Reload should not raise errors
124
+ cm.reload_configs()
125
+
126
+ # Configs should still be accessible
127
+ assert cm.app_settings is not None
128
+ assert cm.llm_config is not None
129
+
130
+
131
+ class TestAppSettings:
132
+ """Test AppSettings model."""
133
+
134
+ def test_app_settings_has_required_fields(self):
135
+ """AppSettings should have all required configuration fields."""
136
+ settings = AppSettings()
137
+
138
+ # Basic app settings
139
+ assert hasattr(settings, "app_name")
140
+ assert hasattr(settings, "port")
141
+ assert hasattr(settings, "debug_mode")
142
+ assert hasattr(settings, "log_level")
143
+
144
+ # Feature flags
145
+ assert hasattr(settings, "feature_rag_enabled")
146
+ assert hasattr(settings, "feature_tools_enabled")
147
+ assert hasattr(settings, "feature_marketplace_enabled")
148
+
149
+ # S3 settings
150
+ assert hasattr(settings, "s3_endpoint")
151
+ assert hasattr(settings, "s3_bucket_name")
152
+
153
+ # Config paths
154
+ assert hasattr(settings, "app_config_overrides")
155
+ assert hasattr(settings, "app_config_defaults")
156
+
157
+ def test_app_settings_defaults(self):
158
+ """AppSettings should have sensible defaults."""
159
+ settings = AppSettings()
160
+
161
+ assert isinstance(settings.port, int) and 1 <= settings.port <= 65535
162
+ assert settings.log_level in ["DEBUG", "INFO", "WARNING", "ERROR"]
163
+ assert isinstance(settings.debug_mode, bool)
164
+ assert isinstance(settings.banner_enabled, bool)
165
+
166
+ def test_app_settings_agent_backward_compatibility(self):
167
+ """Agent mode available should maintain backward compatibility."""
168
+ settings = AppSettings()
169
+
170
+ # Both new and old property should work
171
+ assert hasattr(settings, "feature_agent_mode_available")
172
+ assert hasattr(settings, "agent_mode_available")
173
+ assert settings.agent_mode_available == settings.feature_agent_mode_available
174
+
175
+
176
+ class TestConfigManagerCustomRoot:
177
+ """Test ConfigManager with custom backend root."""
178
+
179
+ def test_custom_backend_root(self):
180
+ """ConfigManager should accept custom backend root path."""
181
+ custom_root = Path(__file__).parent.parent
182
+ cm = ConfigManager(backend_root=custom_root)
183
+
184
+ assert cm._backend_root == custom_root
185
+
186
+ def test_custom_root_still_loads_configs(self):
187
+ """ConfigManager with custom root should still load configs."""
188
+ custom_root = Path(__file__).parent.parent
189
+ cm = ConfigManager(backend_root=custom_root)
190
+
191
+ # Should still be able to load configs
192
+ assert cm.app_settings is not None
193
+ assert cm.llm_config is not None
194
+
195
+
196
+ class TestResolveEnvVar:
197
+ """Test environment variable substitution in config values."""
198
+
199
+ def test_resolve_env_var_with_none(self):
200
+ """Should return None when input is None."""
201
+ assert resolve_env_var(None) is None
202
+
203
+ def test_resolve_env_var_with_literal_string(self):
204
+ """Should return literal string unchanged."""
205
+ assert resolve_env_var("my-literal-token") == "my-literal-token"
206
+
207
+ def test_resolve_env_var_with_existing_env_var(self, monkeypatch):
208
+ """Should resolve ${VAR_NAME} when env var exists."""
209
+ monkeypatch.setenv("TEST_TOKEN", "secret-123")
210
+ assert resolve_env_var("${TEST_TOKEN}") == "secret-123"
211
+
212
+ def test_resolve_env_var_with_missing_env_var(self):
213
+ """Should raise ValueError when env var doesn't exist."""
214
+ with pytest.raises(ValueError, match="Environment variable 'MISSING_VAR' is not set"):
215
+ resolve_env_var("${MISSING_VAR}")
216
+
217
+ def test_resolve_env_var_with_empty_string(self):
218
+ """Should return empty string unchanged."""
219
+ assert resolve_env_var("") == ""
220
+
221
+ def test_resolve_env_var_with_empty_env_var(self, monkeypatch):
222
+ """Should allow empty env var values."""
223
+ monkeypatch.setenv("EMPTY_VAR", "")
224
+ assert resolve_env_var("${EMPTY_VAR}") == ""
225
+
226
+ def test_resolve_env_var_with_partial_pattern(self):
227
+ """Should not match partial patterns like 'prefix-${VAR}'."""
228
+ # Only exact ${VAR} pattern is supported, not embedded in strings
229
+ assert resolve_env_var("prefix-${VAR}") == "prefix-${VAR}"
230
+
231
+ def test_resolve_env_var_with_suffix_pattern(self, monkeypatch):
232
+ """Should not match patterns with suffix like '${VAR}-suffix'.
233
+
234
+ Regression test: Previously re.match() would match '${VAR}' at the start
235
+ and return just the env value, silently dropping the suffix.
236
+ Now we use re.fullmatch() to ensure the entire string is a pattern.
237
+ """
238
+ monkeypatch.setenv("MY_VAR", "resolved")
239
+ # Pattern with suffix should be treated as literal, not partially resolved
240
+ assert resolve_env_var("${MY_VAR}-suffix") == "${MY_VAR}-suffix"
241
+ # Contrast with exact pattern which should resolve
242
+ assert resolve_env_var("${MY_VAR}") == "resolved"
243
+
244
+ def test_resolve_env_var_with_invalid_pattern(self):
245
+ """Should return strings with invalid patterns unchanged."""
246
+ assert resolve_env_var("${123_INVALID}") == "${123_INVALID}"
247
+ assert resolve_env_var("${INVALID-NAME}") == "${INVALID-NAME}"
248
+ assert resolve_env_var("$VAR") == "$VAR"
249
+ assert resolve_env_var("{VAR}") == "{VAR}"
250
+
251
+ def test_resolve_env_var_case_sensitive(self, monkeypatch):
252
+ """Environment variable names should be case-sensitive."""
253
+ monkeypatch.setenv("MY_VAR", "value1")
254
+ monkeypatch.setenv("my_var", "value2")
255
+ assert resolve_env_var("${MY_VAR}") == "value1"
256
+ assert resolve_env_var("${my_var}") == "value2"
257
+
258
+ def test_resolve_env_var_with_special_chars(self, monkeypatch):
259
+ """Should handle env vars with special characters in values."""
260
+ monkeypatch.setenv("SPECIAL_TOKEN", "abc!@#$%^&*()_+-=[]{}|;:,.<>?")
261
+ assert resolve_env_var("${SPECIAL_TOKEN}") == "abc!@#$%^&*()_+-=[]{}|;:,.<>?"
262
+
263
+ class TestMCPServerConfig:
264
+ """Test MCPServerConfig with auth_token field."""
265
+
266
+ def test_auth_token_is_optional(self):
267
+ """auth_token field should be optional."""
268
+ config = MCPServerConfig(
269
+ description="Test server",
270
+ command=["python", "server.py"]
271
+ )
272
+ assert config.auth_token is None
273
+
274
+ def test_auth_token_accepts_string(self):
275
+ """auth_token should accept string values."""
276
+ config = MCPServerConfig(
277
+ description="Test server",
278
+ command=["python", "server.py"],
279
+ auth_token="my-token-123"
280
+ )
281
+ assert config.auth_token == "my-token-123"
282
+
283
+ def test_auth_token_accepts_env_var_pattern(self):
284
+ """auth_token should accept environment variable patterns."""
285
+ config = MCPServerConfig(
286
+ description="Test server",
287
+ url="http://localhost:8000",
288
+ auth_token="${MY_TOKEN}"
289
+ )
290
+ assert config.auth_token == "${MY_TOKEN}"
291
+
292
+ def test_auth_token_accepts_none(self):
293
+ """auth_token should explicitly accept None."""
294
+ config = MCPServerConfig(
295
+ description="Test server",
296
+ command=["python", "server.py"],
297
+ auth_token=None
298
+ )
299
+ assert config.auth_token is None
300
+
301
+
302
+ class TestLLMConfigEnvExpansion:
303
+ """Test LLM configuration with environment variable expansion."""
304
+
305
+ def test_llm_model_config_with_env_var_api_key(self, monkeypatch):
306
+ """LLM model config should accept environment variable patterns in api_key."""
307
+ from atlas.modules.config.config_manager import ModelConfig
308
+
309
+ monkeypatch.setenv("TEST_API_KEY", "secret-key-123")
310
+
311
+ config = ModelConfig(
312
+ model_name="test-model",
313
+ model_url="https://api.openai.com/v1",
314
+ api_key="${TEST_API_KEY}"
315
+ )
316
+ assert config.api_key == "${TEST_API_KEY}"
317
+
318
+ # Test that resolve_env_var works
319
+ resolved_key = resolve_env_var(config.api_key)
320
+ assert resolved_key == "secret-key-123"
321
+
322
+ def test_llm_model_config_with_literal_api_key(self):
323
+ """LLM model config should accept literal api_key values."""
324
+ from atlas.modules.config.config_manager import ModelConfig
325
+
326
+ config = ModelConfig(
327
+ model_name="test-model",
328
+ model_url="https://api.openai.com/v1",
329
+ api_key="sk-literal-key-123"
330
+ )
331
+ assert config.api_key == "sk-literal-key-123"
332
+
333
+ # Test that resolve_env_var returns literal value unchanged
334
+ resolved_key = resolve_env_var(config.api_key)
335
+ assert resolved_key == "sk-literal-key-123"
336
+
337
+ def test_llm_model_config_with_missing_env_var(self):
338
+ """resolve_env_var should raise ValueError for missing env vars in api_key."""
339
+ from atlas.modules.config.config_manager import ModelConfig
340
+
341
+ config = ModelConfig(
342
+ model_name="test-model",
343
+ model_url="https://api.openai.com/v1",
344
+ api_key="${MISSING_API_KEY}"
345
+ )
346
+
347
+ with pytest.raises(ValueError, match="Environment variable 'MISSING_API_KEY' is not set"):
348
+ resolve_env_var(config.api_key)
349
+
350
+ def test_llm_model_config_with_env_var_in_extra_headers(self, monkeypatch):
351
+ """LLM model config should support environment variables in extra_headers."""
352
+ from atlas.modules.config.config_manager import ModelConfig
353
+
354
+ monkeypatch.setenv("REFERER_URL", "https://myapp.com")
355
+ monkeypatch.setenv("APP_NAME", "MyApp")
356
+
357
+ config = ModelConfig(
358
+ model_name="test-model",
359
+ model_url="https://openrouter.ai/api/v1",
360
+ api_key="sk-test",
361
+ extra_headers={
362
+ "HTTP-Referer": "${REFERER_URL}",
363
+ "X-Title": "${APP_NAME}"
364
+ }
365
+ )
366
+
367
+ # Test that headers are stored as-is
368
+ assert config.extra_headers["HTTP-Referer"] == "${REFERER_URL}"
369
+ assert config.extra_headers["X-Title"] == "${APP_NAME}"
370
+
371
+ # Test that resolve_env_var works for each header
372
+ resolved_referer = resolve_env_var(config.extra_headers["HTTP-Referer"])
373
+ resolved_title = resolve_env_var(config.extra_headers["X-Title"])
374
+ assert resolved_referer == "https://myapp.com"
375
+ assert resolved_title == "MyApp"
376
+
377
+ def test_llm_model_config_with_literal_extra_headers(self):
378
+ """LLM model config should support literal values in extra_headers."""
379
+ from atlas.modules.config.config_manager import ModelConfig
380
+
381
+ config = ModelConfig(
382
+ model_name="test-model",
383
+ model_url="https://api.openai.com/v1",
384
+ api_key="sk-test",
385
+ extra_headers={
386
+ "X-Custom-Header": "literal-value",
387
+ "X-Another-Header": "another-literal"
388
+ }
389
+ )
390
+
391
+ # Test that headers are stored as-is
392
+ assert config.extra_headers["X-Custom-Header"] == "literal-value"
393
+ assert config.extra_headers["X-Another-Header"] == "another-literal"
394
+
395
+ # Test that resolve_env_var returns literal values unchanged
396
+ resolved_custom = resolve_env_var(config.extra_headers["X-Custom-Header"])
397
+ resolved_another = resolve_env_var(config.extra_headers["X-Another-Header"])
398
+ assert resolved_custom == "literal-value"
399
+ assert resolved_another == "another-literal"
400
+
401
+ def test_llm_model_config_with_missing_env_var_in_extra_headers(self):
402
+ """resolve_env_var should raise ValueError for missing env vars in extra_headers."""
403
+ from atlas.modules.config.config_manager import ModelConfig
404
+
405
+ config = ModelConfig(
406
+ model_name="test-model",
407
+ model_url="https://api.openai.com/v1",
408
+ api_key="sk-test",
409
+ extra_headers={
410
+ "X-Custom-Header": "${MISSING_HEADER_VAR}"
411
+ }
412
+ )
413
+
414
+ with pytest.raises(ValueError, match="Environment variable 'MISSING_HEADER_VAR' is not set"):
415
+ resolve_env_var(config.extra_headers["X-Custom-Header"])
416
+
417
+
418
+ class TestAppSettingsRAGFeature:
419
+ """Test AppSettings RAG feature flag configuration.
420
+
421
+ RAG is now configured via a simple on/off toggle (FEATURE_RAG_ENABLED).
422
+ All RAG source configuration is done in rag-sources.json.
423
+ """
424
+
425
+ def test_feature_rag_enabled_default_false(self, monkeypatch):
426
+ """feature_rag_enabled should default to False."""
427
+ monkeypatch.delenv("FEATURE_RAG_ENABLED", raising=False)
428
+ settings = AppSettings(_env_file=None)
429
+ assert settings.feature_rag_enabled is False
430
+
431
+ def test_feature_rag_enabled_from_environment(self, monkeypatch):
432
+ """FEATURE_RAG_ENABLED environment variable should enable RAG."""
433
+ monkeypatch.setenv("FEATURE_RAG_ENABLED", "true")
434
+ settings = AppSettings()
435
+ assert settings.feature_rag_enabled is True
436
+
437
+ def test_feature_rag_disabled_from_environment(self, monkeypatch):
438
+ """FEATURE_RAG_ENABLED=false should disable RAG."""
439
+ monkeypatch.setenv("FEATURE_RAG_ENABLED", "false")
440
+ settings = AppSettings()
441
+ assert settings.feature_rag_enabled is False
442
+
443
+ def test_feature_rag_enabled_is_stored_field(self):
444
+ """feature_rag_enabled should be a stored field, not a derived property."""
445
+ assert "feature_rag_enabled" in AppSettings.model_fields
446
+
447
+
448
+ class TestRAGSourceConfig:
449
+ """Test RAGSourceConfig model validation."""
450
+
451
+ def test_mcp_source_with_command(self):
452
+ """MCP source should accept command for stdio transport."""
453
+ from atlas.modules.config.config_manager import RAGSourceConfig
454
+
455
+ config = RAGSourceConfig(
456
+ type="mcp",
457
+ display_name="Test MCP",
458
+ command=["python", "server.py"],
459
+ )
460
+ assert config.type == "mcp"
461
+ assert config.command == ["python", "server.py"]
462
+
463
+ def test_mcp_source_with_url(self):
464
+ """MCP source should accept url for HTTP/SSE transport."""
465
+ from atlas.modules.config.config_manager import RAGSourceConfig
466
+
467
+ config = RAGSourceConfig(
468
+ type="mcp",
469
+ display_name="Test MCP",
470
+ url="http://localhost:8080",
471
+ )
472
+ assert config.type == "mcp"
473
+ assert config.url == "http://localhost:8080"
474
+
475
+ def test_mcp_source_requires_command_or_url(self):
476
+ """MCP source should raise error if neither command nor url is provided."""
477
+ from atlas.modules.config.config_manager import RAGSourceConfig
478
+
479
+ with pytest.raises(ValueError, match="MCP RAG source requires either 'command' or 'url'"):
480
+ RAGSourceConfig(
481
+ type="mcp",
482
+ display_name="Invalid MCP",
483
+ )
484
+
485
+ def test_http_source_with_url(self):
486
+ """HTTP source should accept url."""
487
+ from atlas.modules.config.config_manager import RAGSourceConfig
488
+
489
+ config = RAGSourceConfig(
490
+ type="http",
491
+ display_name="Test HTTP RAG",
492
+ url="https://rag-api.example.com",
493
+ bearer_token="secret-token",
494
+ )
495
+ assert config.type == "http"
496
+ assert config.url == "https://rag-api.example.com"
497
+ assert config.bearer_token == "secret-token"
498
+
499
+ def test_http_source_requires_url(self):
500
+ """HTTP source should raise error if url is not provided."""
501
+ from atlas.modules.config.config_manager import RAGSourceConfig
502
+
503
+ with pytest.raises(ValueError, match="HTTP RAG source requires 'url'"):
504
+ RAGSourceConfig(
505
+ type="http",
506
+ display_name="Invalid HTTP",
507
+ )
508
+
509
+ def test_rag_source_defaults(self):
510
+ """RAGSourceConfig should have sensible defaults."""
511
+ from atlas.modules.config.config_manager import RAGSourceConfig
512
+
513
+ config = RAGSourceConfig(
514
+ type="http",
515
+ url="https://example.com",
516
+ )
517
+ assert config.enabled is True
518
+ assert config.groups == []
519
+ assert config.compliance_level is None
520
+ assert config.top_k == 4
521
+ assert config.timeout == 60.0
522
+ assert config.discovery_endpoint == "/discover/datasources"
523
+ assert config.query_endpoint == "/rag/completions"
524
+
525
+ def test_rag_source_with_all_fields(self):
526
+ """RAGSourceConfig should accept all optional fields."""
527
+ from atlas.modules.config.config_manager import RAGSourceConfig
528
+
529
+ config = RAGSourceConfig(
530
+ type="http",
531
+ display_name="Full Config RAG",
532
+ description="A fully configured RAG source",
533
+ icon="search",
534
+ groups=["admin", "users"],
535
+ compliance_level="Internal",
536
+ enabled=True,
537
+ url="https://rag.example.com",
538
+ bearer_token="my-token",
539
+ default_model="custom-model",
540
+ top_k=10,
541
+ timeout=120.0,
542
+ discovery_endpoint="/custom/discover",
543
+ query_endpoint="/custom/query",
544
+ )
545
+ assert config.display_name == "Full Config RAG"
546
+ assert config.description == "A fully configured RAG source"
547
+ assert config.icon == "search"
548
+ assert config.groups == ["admin", "users"]
549
+ assert config.compliance_level == "Internal"
550
+ assert config.default_model == "custom-model"
551
+ assert config.top_k == 10
552
+ assert config.timeout == 120.0
553
+ assert config.discovery_endpoint == "/custom/discover"
554
+ assert config.query_endpoint == "/custom/query"
555
+
556
+ def test_rag_source_disabled(self):
557
+ """RAGSourceConfig should support disabled state."""
558
+ from atlas.modules.config.config_manager import RAGSourceConfig
559
+
560
+ config = RAGSourceConfig(
561
+ type="http",
562
+ url="https://example.com",
563
+ enabled=False,
564
+ )
565
+ assert config.enabled is False
566
+
567
+
568
+ class TestRAGSourcesConfig:
569
+ """Test RAGSourcesConfig model for multiple RAG sources."""
570
+
571
+ def test_empty_sources_config(self):
572
+ """RAGSourcesConfig should accept empty sources dict."""
573
+ from atlas.modules.config.config_manager import RAGSourcesConfig
574
+
575
+ config = RAGSourcesConfig()
576
+ assert config.sources == {}
577
+
578
+ def test_sources_config_with_multiple_sources(self):
579
+ """RAGSourcesConfig should accept multiple source configurations."""
580
+ from atlas.modules.config.config_manager import RAGSourceConfig, RAGSourcesConfig
581
+
582
+ config = RAGSourcesConfig(
583
+ sources={
584
+ "http_source": RAGSourceConfig(
585
+ type="http",
586
+ url="https://http-rag.example.com",
587
+ ),
588
+ "mcp_source": RAGSourceConfig(
589
+ type="mcp",
590
+ command=["python", "mcp_server.py"],
591
+ ),
592
+ }
593
+ )
594
+ assert len(config.sources) == 2
595
+ assert "http_source" in config.sources
596
+ assert "mcp_source" in config.sources
597
+ assert config.sources["http_source"].type == "http"
598
+ assert config.sources["mcp_source"].type == "mcp"
599
+
600
+ def test_sources_config_from_dict(self):
601
+ """RAGSourcesConfig should convert dict values to RAGSourceConfig."""
602
+ from atlas.modules.config.config_manager import RAGSourcesConfig
603
+
604
+ config = RAGSourcesConfig(
605
+ sources={
606
+ "test_source": {
607
+ "type": "http",
608
+ "url": "https://example.com",
609
+ "display_name": "Test Source",
610
+ }
611
+ }
612
+ )
613
+ assert config.sources["test_source"].type == "http"
614
+ assert config.sources["test_source"].url == "https://example.com"
615
+ assert config.sources["test_source"].display_name == "Test Source"
616
+
617
+
@@ -0,0 +1,12 @@
1
+
2
+ from atlas.modules.config.config_manager import ConfigManager
3
+
4
+
5
+ def test_search_paths_prefer_project_config_dirs():
6
+ cm = ConfigManager()
7
+ paths = cm._search_paths("llmconfig.yml")
8
+ # Ensure both overrides/defaults and legacy paths are present in the list
9
+ str_paths = [str(p) for p in paths]
10
+ assert any("config/overrides" in s for s in str_paths)
11
+ assert any("config/defaults" in s for s in str_paths)
12
+ assert any("atlas/configfiles" in s for s in str_paths)
@@ -0,0 +1,18 @@
1
+
2
+ import pytest
3
+
4
+ from atlas.modules.config.config_manager import config_manager
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_is_user_in_group_debug_admin(monkeypatch):
9
+ # Enable debug mode so test user is treated as admin per core.auth logic
10
+ monkeypatch.setenv("DEBUG_MODE", "true")
11
+ config_manager.reload_configs()
12
+
13
+ from atlas.core.auth import is_user_in_group # import after reload to use env
14
+
15
+ test_user = config_manager.app_settings.test_user
16
+ admin_group = config_manager.app_settings.admin_group
17
+
18
+ assert await is_user_in_group(test_user, admin_group) is True