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,482 @@
1
+ """Integration test for MCP sampling functionality."""
2
+
3
+ from unittest.mock import AsyncMock, MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from atlas.domain.messages.models import ToolCall
8
+ from atlas.modules.mcp_tools.client import MCPToolManager
9
+
10
+
11
+ class TestSamplingIntegration:
12
+ """Integration tests for MCP sampling."""
13
+
14
+ @pytest.mark.asyncio
15
+ async def test_sampling_handler_basic(self):
16
+ """Test that sampling handler can be created and configured."""
17
+ manager = MCPToolManager()
18
+
19
+ # Create a sampling handler
20
+ handler = manager._create_sampling_handler("test_server")
21
+
22
+ # Verify handler is callable
23
+ assert callable(handler)
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_sampling_context_manager(self):
27
+ """Test the sampling context manager."""
28
+ manager = MCPToolManager()
29
+
30
+ # Create a mock tool call and update callback
31
+ tool_call = ToolCall(
32
+ id="test_tool_call_1",
33
+ name="test_tool",
34
+ arguments={}
35
+ )
36
+
37
+ update_cb = AsyncMock()
38
+
39
+ # Use the context manager
40
+ async with manager._use_sampling_context("test_server", tool_call, update_cb):
41
+ # Verify routing is set up with composite key (server_name, tool_call.id)
42
+ from atlas.modules.mcp_tools.client import _SAMPLING_ROUTING
43
+ routing_key = ("test_server", "test_tool_call_1")
44
+ assert routing_key in _SAMPLING_ROUTING
45
+ routing = _SAMPLING_ROUTING[routing_key]
46
+ assert routing.server_name == "test_server"
47
+ assert routing.tool_call == tool_call
48
+ assert routing.update_cb == update_cb
49
+
50
+ # Verify routing is cleaned up
51
+ assert routing_key not in _SAMPLING_ROUTING
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_sampling_handler_with_routing(self):
55
+ """Test sampling handler with routing context."""
56
+ manager = MCPToolManager()
57
+
58
+ # Create a mock tool call
59
+ tool_call = ToolCall(
60
+ id="test_tool_call_1",
61
+ name="test_tool",
62
+ arguments={}
63
+ )
64
+
65
+ update_cb = AsyncMock()
66
+
67
+ # Mock the LLM caller - patch where it's imported in the handler
68
+ with patch('atlas.modules.llm.litellm_caller.LiteLLMCaller') as mock_llm_class:
69
+ mock_llm_instance = AsyncMock()
70
+ mock_llm_instance.call_plain = AsyncMock(return_value="Mocked LLM response")
71
+ mock_llm_class.return_value = mock_llm_instance
72
+
73
+ # Set up routing context
74
+ async with manager._use_sampling_context("test_server", tool_call, update_cb):
75
+ handler = manager._create_sampling_handler("test_server")
76
+
77
+ # Create mock sampling params
78
+ mock_params = MagicMock()
79
+ mock_params.systemPrompt = "You are helpful"
80
+ mock_params.temperature = 0.7
81
+ mock_params.maxTokens = 500
82
+ mock_params.modelPreferences = None
83
+
84
+ # Call the handler
85
+ result = await handler(
86
+ messages=["Test message"],
87
+ params=mock_params
88
+ )
89
+
90
+ # Verify result
91
+ assert result.content.text == "Mocked LLM response"
92
+
93
+ # Verify LLM was called correctly
94
+ mock_llm_instance.call_plain.assert_called_once()
95
+ call_args = mock_llm_instance.call_plain.call_args
96
+ assert call_args.kwargs.get('temperature') == 0.7
97
+ assert call_args.kwargs.get('max_tokens') == 500
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_sampling_without_routing_context(self):
101
+ """Test that sampling fails without routing context."""
102
+ manager = MCPToolManager()
103
+
104
+ handler = manager._create_sampling_handler("test_server")
105
+
106
+ # Try to call handler without routing context
107
+ with pytest.raises(Exception, match="No routing context"):
108
+ await handler(messages=["Test"], params=None)
109
+
110
+
111
+ class TestSamplingDemoTools:
112
+ """Integration tests for sampling_demo MCP server tools."""
113
+
114
+ @pytest.mark.asyncio
115
+ async def test_summarize_text_tool(self):
116
+ """Test summarize_text tool with basic sampling."""
117
+ import sys
118
+ from pathlib import Path
119
+
120
+ from fastmcp import Client
121
+ from fastmcp.client.transports import StdioTransport
122
+ from mcp.types import CreateMessageResult, TextContent
123
+
124
+ # Create mock sampling handler
125
+ async def mock_sampling_handler(messages, params=None, context=None):
126
+ # Verify basic sampling call
127
+ assert len(messages) > 0
128
+ return CreateMessageResult(
129
+ role="assistant",
130
+ content=TextContent(type="text", text="This is a concise summary of the text."),
131
+ model="test-model"
132
+ )
133
+
134
+ # Get absolute path to the sampling demo server
135
+ server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
136
+
137
+ # Use StdioTransport explicitly
138
+ transport = StdioTransport(
139
+ command=sys.executable,
140
+ args=[str(server_path)]
141
+ )
142
+ client = Client(transport, sampling_handler=mock_sampling_handler)
143
+
144
+ async with client:
145
+ result = await client.call_tool(
146
+ "summarize_text",
147
+ {"text": "Long text that needs summarization..."}
148
+ )
149
+
150
+ # Verify tool returns the sampled text
151
+ assert "summary" in result.content[0].text.lower()
152
+
153
+ @pytest.mark.asyncio
154
+ async def test_analyze_sentiment_tool(self):
155
+ """Test analyze_sentiment tool with system prompt and low temperature."""
156
+ import sys
157
+ from pathlib import Path
158
+
159
+ from fastmcp import Client
160
+ from fastmcp.client.transports import StdioTransport
161
+ from mcp.types import CreateMessageResult, TextContent
162
+
163
+ captured_params = {}
164
+
165
+ async def mock_sampling_handler(messages, params=None, context=None):
166
+ # Capture params to verify system prompt and temperature
167
+ if params:
168
+ captured_params['system_prompt'] = getattr(params, 'systemPrompt', None)
169
+ captured_params['temperature'] = getattr(params, 'temperature', None)
170
+
171
+ return CreateMessageResult(
172
+ role="assistant",
173
+ content=TextContent(
174
+ type="text",
175
+ text="Positive sentiment - the text expresses enthusiasm and satisfaction."
176
+ ),
177
+ model="test-model"
178
+ )
179
+
180
+ server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
181
+ transport = StdioTransport(
182
+ command=sys.executable,
183
+ args=[str(server_path)]
184
+ )
185
+ client = Client(transport, sampling_handler=mock_sampling_handler)
186
+
187
+ async with client:
188
+ result = await client.call_tool(
189
+ "analyze_sentiment",
190
+ {"text": "I love this product!"}
191
+ )
192
+
193
+ # Verify tool used system prompt and low temperature
194
+ assert captured_params.get('system_prompt') is not None
195
+ assert "sentiment" in captured_params['system_prompt'].lower()
196
+ assert captured_params.get('temperature') == 0.3
197
+ assert "sentiment" in result.content[0].text.lower()
198
+
199
+ @pytest.mark.asyncio
200
+ async def test_generate_code_tool(self):
201
+ """Test generate_code tool with model preferences."""
202
+ import sys
203
+ from pathlib import Path
204
+
205
+ from fastmcp import Client
206
+ from fastmcp.client.transports import StdioTransport
207
+ from mcp.types import CreateMessageResult, TextContent
208
+
209
+ captured_params = {}
210
+
211
+ async def mock_sampling_handler(messages, params=None, context=None):
212
+ # Capture model preferences
213
+ if params:
214
+ captured_params['model_preferences'] = getattr(params, 'modelPreferences', None)
215
+ captured_params['max_tokens'] = getattr(params, 'maxTokens', None)
216
+ captured_params['temperature'] = getattr(params, 'temperature', None)
217
+
218
+ return CreateMessageResult(
219
+ role="assistant",
220
+ content=TextContent(
221
+ type="text",
222
+ text="def fibonacci(n):\n if n <= 1:\n return n\n return fibonacci(n-1) + fibonacci(n-2)"
223
+ ),
224
+ model="test-model"
225
+ )
226
+
227
+ server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
228
+ transport = StdioTransport(
229
+ command=sys.executable,
230
+ args=[str(server_path)]
231
+ )
232
+ client = Client(transport, sampling_handler=mock_sampling_handler)
233
+
234
+ async with client:
235
+ result = await client.call_tool(
236
+ "generate_code",
237
+ {
238
+ "description": "calculate fibonacci numbers",
239
+ "language": "Python"
240
+ }
241
+ )
242
+
243
+ # Verify model preferences were set
244
+ assert captured_params.get('model_preferences') is not None
245
+ # ModelPreferences can be an object or list, just verify it exists
246
+ assert captured_params['model_preferences'] is not None
247
+ # Verify reasonable parameters for code generation
248
+ assert captured_params.get('max_tokens') == 1000
249
+ assert captured_params.get('temperature') == 0.7
250
+ assert "def" in result.content[0].text or "fibonacci" in result.content[0].text.lower()
251
+
252
+ @pytest.mark.asyncio
253
+ async def test_creative_story_tool(self):
254
+ """Test creative_story tool with high temperature."""
255
+ import sys
256
+ from pathlib import Path
257
+
258
+ from fastmcp import Client
259
+ from fastmcp.client.transports import StdioTransport
260
+ from mcp.types import CreateMessageResult, TextContent
261
+
262
+ captured_params = {}
263
+
264
+ async def mock_sampling_handler(messages, params=None, context=None):
265
+ # Capture temperature to verify high value for creativity
266
+ if params:
267
+ captured_params['temperature'] = getattr(params, 'temperature', None)
268
+ captured_params['max_tokens'] = getattr(params, 'maxTokens', None)
269
+
270
+ return CreateMessageResult(
271
+ role="assistant",
272
+ content=TextContent(
273
+ type="text",
274
+ text="Once upon a time, in a world of circuits and code, there lived a robot who dreamed of painting..."
275
+ ),
276
+ model="test-model"
277
+ )
278
+
279
+ server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
280
+ transport = StdioTransport(
281
+ command=sys.executable,
282
+ args=[str(server_path)]
283
+ )
284
+ client = Client(transport, sampling_handler=mock_sampling_handler)
285
+
286
+ async with client:
287
+ result = await client.call_tool(
288
+ "creative_story",
289
+ {"prompt": "a robot learning to paint"}
290
+ )
291
+
292
+ # Verify high temperature for creativity
293
+ assert captured_params.get('temperature') == 0.9
294
+ assert captured_params.get('max_tokens') == 500
295
+ # Story should be present
296
+ assert len(result.content[0].text) > 0
297
+
298
+ @pytest.mark.asyncio
299
+ async def test_multi_turn_conversation_tool(self):
300
+ """Test multi_turn_conversation tool with SamplingMessage objects."""
301
+ import sys
302
+ from pathlib import Path
303
+
304
+ from fastmcp import Client
305
+ from fastmcp.client.transports import StdioTransport
306
+ from mcp.types import CreateMessageResult, TextContent
307
+
308
+ call_count = 0
309
+
310
+ async def mock_sampling_handler(messages, params=None, context=None):
311
+ nonlocal call_count
312
+ call_count += 1
313
+
314
+ # Verify messages include SamplingMessage objects for multi-turn
315
+ if call_count == 1:
316
+ # First turn - initial question
317
+ assert len(messages) == 1
318
+ return CreateMessageResult(
319
+ role="assistant",
320
+ content=TextContent(
321
+ type="text",
322
+ text="Key aspects to consider: history, current state, and future trends."
323
+ ),
324
+ model="test-model"
325
+ )
326
+ else:
327
+ # Second turn - should have conversation history
328
+ assert len(messages) >= 3 # User, Assistant, User
329
+ return CreateMessageResult(
330
+ role="assistant",
331
+ content=TextContent(
332
+ type="text",
333
+ text="The most important point is understanding the historical context."
334
+ ),
335
+ model="test-model"
336
+ )
337
+
338
+ server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
339
+ transport = StdioTransport(
340
+ command=sys.executable,
341
+ args=[str(server_path)]
342
+ )
343
+ client = Client(transport, sampling_handler=mock_sampling_handler)
344
+
345
+ async with client:
346
+ result = await client.call_tool(
347
+ "multi_turn_conversation",
348
+ {"topic": "artificial intelligence"}
349
+ )
350
+
351
+ # Verify two sampling calls were made
352
+ assert call_count == 2
353
+ # Result should contain both turns
354
+ text = result.content[0].text
355
+ assert "Discussion" in text or "Initial Response" in text
356
+
357
+ @pytest.mark.asyncio
358
+ async def test_research_question_tool(self):
359
+ """Test research_question tool with multi-step agentic workflow."""
360
+ import sys
361
+ from pathlib import Path
362
+
363
+ from fastmcp import Client
364
+ from fastmcp.client.transports import StdioTransport
365
+ from mcp.types import CreateMessageResult, TextContent
366
+
367
+ call_count = 0
368
+ captured_calls = []
369
+
370
+ async def mock_sampling_handler(messages, params=None, context=None):
371
+ nonlocal call_count
372
+ call_count += 1
373
+
374
+ # Capture each call for verification
375
+ captured_calls.append({
376
+ 'messages': messages,
377
+ 'params': params
378
+ })
379
+
380
+ if call_count == 1:
381
+ # First call: break down question
382
+ return CreateMessageResult(
383
+ role="assistant",
384
+ content=TextContent(
385
+ type="text",
386
+ text="1. What are renewable energy sources?\n2. What are their benefits?\n3. What are the challenges?"
387
+ ),
388
+ model="test-model"
389
+ )
390
+ else:
391
+ # Second call: comprehensive answer
392
+ return CreateMessageResult(
393
+ role="assistant",
394
+ content=TextContent(
395
+ type="text",
396
+ text="Renewable energy sources include solar, wind, and hydro. Benefits include sustainability, reduced emissions, and energy independence."
397
+ ),
398
+ model="test-model"
399
+ )
400
+
401
+ server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
402
+ transport = StdioTransport(
403
+ command=sys.executable,
404
+ args=[str(server_path)]
405
+ )
406
+ client = Client(transport, sampling_handler=mock_sampling_handler)
407
+
408
+ async with client:
409
+ result = await client.call_tool(
410
+ "research_question",
411
+ {"question": "What are the benefits of renewable energy?"}
412
+ )
413
+
414
+ # Verify two-step research process
415
+ assert call_count == 2
416
+ # First call should be about breaking down the question
417
+ assert "break down" in str(captured_calls[0]['messages']).lower()
418
+ # Second call should reference the breakdown
419
+ assert len(str(captured_calls[1]['messages'])) > len(str(captured_calls[0]['messages']))
420
+ # Result should contain analysis and answer
421
+ text = result.content[0].text
422
+ assert "Research Question" in text or "Analysis" in text or "Answer" in text
423
+
424
+ @pytest.mark.asyncio
425
+ async def test_translate_and_explain_tool(self):
426
+ """Test translate_and_explain tool with sequential sampling workflow."""
427
+ import sys
428
+ from pathlib import Path
429
+
430
+ from fastmcp import Client
431
+ from fastmcp.client.transports import StdioTransport
432
+ from mcp.types import CreateMessageResult, TextContent
433
+
434
+ call_count = 0
435
+
436
+ async def mock_sampling_handler(messages, params=None, context=None):
437
+ nonlocal call_count
438
+ call_count += 1
439
+
440
+ if call_count == 1:
441
+ # First call: translation
442
+ return CreateMessageResult(
443
+ role="assistant",
444
+ content=TextContent(
445
+ type="text",
446
+ text="Hola, ¿cómo estás?"
447
+ ),
448
+ model="test-model"
449
+ )
450
+ else:
451
+ # Second call: explanation
452
+ return CreateMessageResult(
453
+ role="assistant",
454
+ content=TextContent(
455
+ type="text",
456
+ text="Translation uses informal 'tú' form. 'Cómo estás' is the standard greeting in Spanish."
457
+ ),
458
+ model="test-model"
459
+ )
460
+
461
+ server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
462
+ transport = StdioTransport(
463
+ command=sys.executable,
464
+ args=[str(server_path)]
465
+ )
466
+ client = Client(transport, sampling_handler=mock_sampling_handler)
467
+
468
+ async with client:
469
+ result = await client.call_tool(
470
+ "translate_and_explain",
471
+ {
472
+ "text": "Hello, how are you?",
473
+ "target_language": "Spanish"
474
+ }
475
+ )
476
+
477
+ # Verify sequential workflow (two calls)
478
+ assert call_count == 2
479
+ # Result should contain both translation and explanation
480
+ text = result.content[0].text
481
+ assert "Translation" in text or "Hola" in text
482
+ assert "Notes" in text or "explain" in text.lower() or "form" in text.lower()
@@ -0,0 +1,61 @@
1
+ from main import app
2
+ from starlette.testclient import TestClient
3
+
4
+
5
+ def test_admin_routes_require_admin(monkeypatch):
6
+ client = TestClient(app)
7
+
8
+ # Non-admin user should be redirected/forbidden depending on middleware
9
+ # Provide a non-admin email
10
+ r = client.get("/admin/", headers={"X-User-Email": "user@example.com"})
11
+ assert r.status_code in (302, 403)
12
+
13
+ # Admin access when user is in admin group (mocked via config in core.auth)
14
+ r2 = client.get("/admin/", headers={"X-User-Email": "admin@example.com"})
15
+ # In debug mode off, should allow if auth module says admin@example.com is admin
16
+ assert r2.status_code == 200
17
+ data = r2.json()
18
+ assert data.get("available_endpoints") is not None
19
+
20
+
21
+ def test_system_status_endpoint():
22
+ """Test the system status endpoint returns expected data structure."""
23
+ client = TestClient(app)
24
+
25
+ # Test with admin user
26
+ r = client.get("/admin/system-status", headers={"X-User-Email": "admin@example.com"})
27
+ assert r.status_code == 200
28
+
29
+ data = r.json()
30
+
31
+ # Check response structure
32
+ assert "overall_status" in data
33
+ assert "components" in data
34
+ assert "checked_by" in data
35
+
36
+ # Overall status should be "healthy" or "warning"
37
+ assert data["overall_status"] in ("healthy", "warning")
38
+
39
+ # Components should be a list
40
+ assert isinstance(data["components"], list)
41
+
42
+ # Check that expected components are present
43
+ component_names = [c["component"] for c in data["components"]]
44
+ assert "Configuration" in component_names
45
+ assert "Logging" in component_names
46
+
47
+ # Each component should have required fields
48
+ for component in data["components"]:
49
+ assert "component" in component
50
+ assert "status" in component
51
+ assert "details" in component
52
+ assert component["status"] in ("healthy", "warning", "error")
53
+
54
+
55
+ def test_system_status_requires_admin():
56
+ """Test that system status endpoint requires admin access."""
57
+ client = TestClient(app)
58
+
59
+ # Non-admin user should be denied
60
+ r = client.get("/admin/system-status", headers={"X-User-Email": "user@example.com"})
61
+ assert r.status_code in (302, 403)
@@ -0,0 +1,65 @@
1
+ import base64
2
+
3
+ from main import app
4
+ from starlette.testclient import TestClient
5
+
6
+ from atlas.core.capabilities import generate_file_token, verify_file_token
7
+
8
+
9
+ def test_capability_token_roundtrip_and_tamper(monkeypatch):
10
+ # Basic generate/verify
11
+ token = generate_file_token("alice@example.com", "file123", ttl_seconds=60)
12
+ claims = verify_file_token(token)
13
+ assert claims and claims["u"] == "alice@example.com" and claims["k"] == "file123"
14
+
15
+ # Tamper body should fail
16
+ body, sig = token.split(".", 1)
17
+ tampered = body[:-1] + ("A" if body[-1] != "A" else "B")
18
+ bad = f"{tampered}.{sig}"
19
+ assert verify_file_token(bad) is None
20
+
21
+
22
+ def test_capability_token_expiry(monkeypatch):
23
+ # Create a token that is already expired
24
+ token = generate_file_token("bob@example.com", "file999", ttl_seconds=-1)
25
+ assert verify_file_token(token) is None
26
+
27
+
28
+ def test_download_rejects_invalid_or_expired_token(monkeypatch):
29
+ client = TestClient(app)
30
+
31
+ from atlas.infrastructure.app_factory import app_factory
32
+ s3 = app_factory.get_file_storage()
33
+
34
+ async def fake_get_file(user, key):
35
+ return {
36
+ "key": key,
37
+ "filename": "hello.txt",
38
+ "content_base64": base64.b64encode(b"secret").decode(),
39
+ "content_type": "text/plain",
40
+ "size": 6,
41
+ "last_modified": "",
42
+ "etag": "",
43
+ "tags": {},
44
+ "user_email": user,
45
+ }
46
+
47
+ # Always return a file for these tests
48
+ monkeypatch.setattr(s3, "get_file", fake_get_file)
49
+
50
+ # Invalid token
51
+ resp = client.get(
52
+ "/api/files/download/k2",
53
+ params={"token": "not.a.valid.token"},
54
+ headers={"X-User-Email": "ignored@example.com"},
55
+ )
56
+ assert resp.status_code == 403
57
+
58
+ # Expired token
59
+ expired = generate_file_token("alice@example.com", "k2", ttl_seconds=-5)
60
+ resp2 = client.get(
61
+ "/api/files/download/k2",
62
+ params={"token": expired},
63
+ headers={"X-User-Email": "ignored@example.com"},
64
+ )
65
+ assert resp2.status_code == 403
@@ -0,0 +1,21 @@
1
+ from main import app
2
+ from starlette.testclient import TestClient
3
+
4
+
5
+ def test_user_stats_enforces_self_scope(monkeypatch):
6
+ client = TestClient(app)
7
+
8
+ # user stats for self should pass (even if backend returns arbitrary data)
9
+ r_ok = client.get(
10
+ "/api/users/alice@example.com/files/stats",
11
+ headers={"X-User-Email": "alice@example.com"},
12
+ )
13
+ # Endpoint may error if mock S3 down; allow 200 or 500, but importantly not 403
14
+ assert r_ok.status_code in (200, 500)
15
+
16
+ # user cannot view others
17
+ r_forbid = client.get(
18
+ "/api/users/bob@example.com/files/stats",
19
+ headers={"X-User-Email": "alice@example.com"},
20
+ )
21
+ assert r_forbid.status_code == 403