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.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import types
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
# Ensure backend root is on path (same approach used in other tests)
|
|
8
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
9
|
+
|
|
10
|
+
# Patch fastmcp Client usage by MCPToolManager with a fake client/manager
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def patch_mcp(monkeypatch):
|
|
13
|
+
from atlas.modules.mcp_tools.client import MCPToolManager
|
|
14
|
+
|
|
15
|
+
class FakeTool:
|
|
16
|
+
def __init__(self, name, description="", inputSchema=None):
|
|
17
|
+
self.name = name
|
|
18
|
+
self.description = description
|
|
19
|
+
self.inputSchema = inputSchema or {"type": "object", "properties": {"username": {"type": "string"}}}
|
|
20
|
+
|
|
21
|
+
class FakeClient:
|
|
22
|
+
def __init__(self, server_name):
|
|
23
|
+
self.server_name = server_name
|
|
24
|
+
async def __aenter__(self):
|
|
25
|
+
return self
|
|
26
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
27
|
+
return False
|
|
28
|
+
async def call_tool(self, tool_name, arguments, **kwargs):
|
|
29
|
+
# Provide deterministic results for test
|
|
30
|
+
if tool_name == "rag_discover_resources":
|
|
31
|
+
if self.server_name == "docsRag":
|
|
32
|
+
return types.SimpleNamespace(
|
|
33
|
+
structured_content={
|
|
34
|
+
"results": {
|
|
35
|
+
"resources": [
|
|
36
|
+
{"id": "handbook", "name": "Employee Handbook", "authRequired": True, "groups": ["hr"], "defaultSelected": True},
|
|
37
|
+
{"id": "legal", "name": "Legal Docs", "authRequired": True, "groups": ["legal"]},
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
elif self.server_name == "notionRag":
|
|
43
|
+
return types.SimpleNamespace(
|
|
44
|
+
structured_content={
|
|
45
|
+
"results": {
|
|
46
|
+
"resources": [
|
|
47
|
+
{"id": "notion-space", "name": "Notion Space", "authRequired": True, "groups": ["notion"]}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
if tool_name == "rag_get_raw_results":
|
|
53
|
+
# Return hits with scores
|
|
54
|
+
return types.SimpleNamespace(
|
|
55
|
+
structured_content={
|
|
56
|
+
"results": {
|
|
57
|
+
"hits": [
|
|
58
|
+
{"id": f"{self.server_name}-1", "score": 0.9, "resourceId": arguments.get("sources", [""])[0]},
|
|
59
|
+
{"id": f"{self.server_name}-2", "score": 0.5, "resourceId": arguments.get("sources", [""])[-1]},
|
|
60
|
+
],
|
|
61
|
+
"stats": {"top_k": arguments.get("top_k", 8)},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
if tool_name == "rag_get_synthesized_results":
|
|
66
|
+
return types.SimpleNamespace(
|
|
67
|
+
structured_content={
|
|
68
|
+
"results": {
|
|
69
|
+
"answer": f"Answer from {self.server_name}",
|
|
70
|
+
"citations": [{"resourceId": r} for r in arguments.get("sources", [])],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
return types.SimpleNamespace(structured_content={})
|
|
75
|
+
|
|
76
|
+
async def fake_initialize_clients(self):
|
|
77
|
+
# Pretend both servers are configured and online
|
|
78
|
+
self.clients = {"docsRag": FakeClient("docsRag"), "notionRag": FakeClient("notionRag")}
|
|
79
|
+
|
|
80
|
+
async def fake_discover_tools(self):
|
|
81
|
+
# Expose RAG tools on docsRag; notionRag only discovery/raw
|
|
82
|
+
self.available_tools = {
|
|
83
|
+
"docsRag": {
|
|
84
|
+
"tools": [FakeTool("rag_discover_resources"), FakeTool("rag_get_raw_results"), FakeTool("rag_get_synthesized_results")],
|
|
85
|
+
"config": {"description": "Docs RAG"},
|
|
86
|
+
},
|
|
87
|
+
"notionRag": {
|
|
88
|
+
"tools": [FakeTool("rag_discover_resources"), FakeTool("rag_get_raw_results")],
|
|
89
|
+
"config": {"description": "Notion RAG"},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
# Also set servers config for UI fields
|
|
93
|
+
self.servers_config = {
|
|
94
|
+
"docsRag": {"description": "Docs RAG", "ui": {"icon": "book"}},
|
|
95
|
+
"notionRag": {"description": "Notion", "ui": {"icon": "notion"}},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async def fake_discover_prompts(self):
|
|
99
|
+
self.available_prompts = {}
|
|
100
|
+
|
|
101
|
+
async def fake_get_authorized_servers(self, user_email, auth_check_func):
|
|
102
|
+
# Simple ACL: allow both for @company.com; only docsRag otherwise
|
|
103
|
+
return ["docsRag", "notionRag"] if user_email.endswith("@company.com") else ["docsRag"]
|
|
104
|
+
|
|
105
|
+
async def fake_call_tool(self, server_name, tool_name, arguments, **kwargs):
|
|
106
|
+
return await self.clients[server_name].call_tool(tool_name, arguments)
|
|
107
|
+
|
|
108
|
+
monkeypatch.setattr(MCPToolManager, "initialize_clients", fake_initialize_clients, raising=False)
|
|
109
|
+
monkeypatch.setattr(MCPToolManager, "discover_tools", fake_discover_tools, raising=False)
|
|
110
|
+
monkeypatch.setattr(MCPToolManager, "discover_prompts", fake_discover_prompts, raising=False)
|
|
111
|
+
monkeypatch.setattr(MCPToolManager, "get_authorized_servers", fake_get_authorized_servers, raising=False)
|
|
112
|
+
monkeypatch.setattr(MCPToolManager, "call_tool", fake_call_tool, raising=False)
|
|
113
|
+
|
|
114
|
+
# Also patch rag_mcp_config to return the test RAG servers
|
|
115
|
+
# (RAGMCPService uses rag_mcp_config for authorization, not mcp_manager.servers_config)
|
|
116
|
+
from atlas.modules.config.config_manager import ConfigManager, MCPConfig, MCPServerConfig
|
|
117
|
+
fake_rag_servers = {
|
|
118
|
+
"docsRag": MCPServerConfig(
|
|
119
|
+
description="Docs RAG",
|
|
120
|
+
enabled=True,
|
|
121
|
+
groups=["users"], # Everyone is in users group
|
|
122
|
+
),
|
|
123
|
+
"notionRag": MCPServerConfig(
|
|
124
|
+
description="Notion RAG",
|
|
125
|
+
enabled=True,
|
|
126
|
+
groups=["company"], # Only company users
|
|
127
|
+
),
|
|
128
|
+
}
|
|
129
|
+
fake_rag_mcp_config = MCPConfig(servers=fake_rag_servers)
|
|
130
|
+
monkeypatch.setattr(ConfigManager, "rag_mcp_config", property(lambda self: fake_rag_mcp_config))
|
|
131
|
+
|
|
132
|
+
# Patch is_user_in_group to simulate domain-based access
|
|
133
|
+
# @company.com users are in "company" group, everyone is in "users" group
|
|
134
|
+
async def fake_is_user_in_group(user_id: str, group_id: str) -> bool:
|
|
135
|
+
if group_id == "users":
|
|
136
|
+
return True
|
|
137
|
+
if group_id == "company":
|
|
138
|
+
return user_id.endswith("@company.com")
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
from atlas.core import auth as core_auth
|
|
142
|
+
monkeypatch.setattr(core_auth, "is_user_in_group", fake_is_user_in_group)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_discovery_across_multiple_servers():
|
|
147
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
148
|
+
# Initialize MCP
|
|
149
|
+
mcp = app_factory.get_mcp_manager()
|
|
150
|
+
await mcp.initialize_clients()
|
|
151
|
+
await mcp.discover_tools()
|
|
152
|
+
await mcp.discover_prompts()
|
|
153
|
+
|
|
154
|
+
from atlas.core.auth import is_user_in_group
|
|
155
|
+
from atlas.domain.rag_mcp_service import RAGMCPService
|
|
156
|
+
|
|
157
|
+
svc = RAGMCPService(mcp, app_factory.get_config_manager(), is_user_in_group)
|
|
158
|
+
# user with @company.com gets both servers
|
|
159
|
+
sources = await svc.discover_data_sources("alice@company.com")
|
|
160
|
+
assert "docsRag:handbook" in sources
|
|
161
|
+
assert "docsRag:legal" in sources
|
|
162
|
+
assert "notionRag:notion-space" in sources
|
|
163
|
+
|
|
164
|
+
# richer servers
|
|
165
|
+
servers = await svc.discover_servers("alice@company.com")
|
|
166
|
+
assert any(s["server"] == "docsRag" and s["sources"] for s in servers)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
async def test_acl_filtering():
|
|
171
|
+
from atlas.core.auth import is_user_in_group
|
|
172
|
+
from atlas.domain.rag_mcp_service import RAGMCPService
|
|
173
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
174
|
+
|
|
175
|
+
mcp = app_factory.get_mcp_manager()
|
|
176
|
+
await mcp.initialize_clients()
|
|
177
|
+
await mcp.discover_tools()
|
|
178
|
+
await mcp.discover_prompts()
|
|
179
|
+
|
|
180
|
+
svc = RAGMCPService(mcp, app_factory.get_config_manager(), is_user_in_group)
|
|
181
|
+
# Non-company user only sees docsRag
|
|
182
|
+
sources = await svc.discover_data_sources("bob@public.net")
|
|
183
|
+
assert all(s.startswith("docsRag:") for s in sources)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.asyncio
|
|
187
|
+
async def test_search_and_synthesize_merge():
|
|
188
|
+
from atlas.core.auth import is_user_in_group
|
|
189
|
+
from atlas.domain.rag_mcp_service import RAGMCPService
|
|
190
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
191
|
+
|
|
192
|
+
mcp = app_factory.get_mcp_manager()
|
|
193
|
+
await mcp.initialize_clients()
|
|
194
|
+
await mcp.discover_tools()
|
|
195
|
+
await mcp.discover_prompts()
|
|
196
|
+
|
|
197
|
+
svc = RAGMCPService(mcp, app_factory.get_config_manager(), is_user_in_group)
|
|
198
|
+
sources = ["docsRag:handbook", "notionRag:notion-space"]
|
|
199
|
+
res = await svc.search_raw("alice@company.com", "vacation policy", sources, top_k=1)
|
|
200
|
+
assert "results" in res and "hits" in res["results"]
|
|
201
|
+
assert len(res["results"]["hits"]) == 1 # limited by top_k
|
|
202
|
+
|
|
203
|
+
syn = await svc.synthesize("alice@company.com", "vacation policy", sources, top_k=2)
|
|
204
|
+
assert "results" in syn and "answer" in syn["results"]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import types
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
# Ensure backend root is on path (same approach used in other tests)
|
|
9
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FakeTool:
|
|
13
|
+
def __init__(self, name: str):
|
|
14
|
+
self.name = name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FakeMCP:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
# Simulate available tools config per server
|
|
20
|
+
self.available_tools: Dict[str, Dict[str, Any]] = {
|
|
21
|
+
"docsRag": {"tools": [FakeTool("rag_discover_resources"), FakeTool("rag_get_raw_results")], "config": {"ui": {"icon": "book"}}},
|
|
22
|
+
"searchRag": {"tools": [FakeTool("rag_discover_resources"), FakeTool("rag_get_raw_results"), FakeTool("rag_get_synthesized_results")], "config": {}},
|
|
23
|
+
"misc": {"tools": [FakeTool("other")], "config": {}},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async def get_authorized_servers(self, user: str, _auth) -> List[str]:
|
|
27
|
+
# User bob can see all, alice cannot see misc
|
|
28
|
+
if user.startswith("alice"):
|
|
29
|
+
return ["docsRag", "searchRag"]
|
|
30
|
+
return list(self.available_tools.keys())
|
|
31
|
+
|
|
32
|
+
async def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any], *_, **__):
|
|
33
|
+
# Return minimal v2-structured payloads
|
|
34
|
+
if tool_name == "rag_discover_resources":
|
|
35
|
+
if server_name == "docsRag":
|
|
36
|
+
return types.SimpleNamespace(structured_content={
|
|
37
|
+
"results": {"resources": [
|
|
38
|
+
{"id": "handbook", "name": "Employee Handbook", "authRequired": True, "groups": ["hr"], "defaultSelected": True},
|
|
39
|
+
{"id": "legal", "name": "Legal Docs", "authRequired": True, "groups": ["legal"]},
|
|
40
|
+
]}
|
|
41
|
+
})
|
|
42
|
+
if server_name == "searchRag":
|
|
43
|
+
return types.SimpleNamespace(structured_content={
|
|
44
|
+
"results": {"resources": [
|
|
45
|
+
{"id": "kb", "name": "KB", "authRequired": True, "groups": ["kb"]}
|
|
46
|
+
]}
|
|
47
|
+
})
|
|
48
|
+
if tool_name == "rag_get_raw_results":
|
|
49
|
+
q = arguments.get("query")
|
|
50
|
+
srcs = arguments.get("sources", [])
|
|
51
|
+
hits = []
|
|
52
|
+
for i, s in enumerate(srcs):
|
|
53
|
+
hits.append({
|
|
54
|
+
"id": f"{server_name}-{s}-{i}",
|
|
55
|
+
"score": 1.0 - i * 0.01,
|
|
56
|
+
"resourceId": f"{server_name}:{s}",
|
|
57
|
+
"title": f"{q} in {s}",
|
|
58
|
+
})
|
|
59
|
+
return types.SimpleNamespace(structured_content={"results": {"hits": hits}})
|
|
60
|
+
if tool_name == "rag_get_synthesized_results":
|
|
61
|
+
return types.SimpleNamespace(structured_content={
|
|
62
|
+
"results": {"answer": f"Synth for {arguments.get('query')} by {server_name}"}
|
|
63
|
+
})
|
|
64
|
+
return types.SimpleNamespace(structured_content={})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FakeMCPServerConfig:
|
|
68
|
+
"""Minimal server config for testing."""
|
|
69
|
+
def __init__(self, enabled=True, groups=None):
|
|
70
|
+
self.enabled = enabled
|
|
71
|
+
self.groups = groups or []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FakeMCPConfig:
|
|
75
|
+
"""Minimal MCP config for testing."""
|
|
76
|
+
def __init__(self, servers=None):
|
|
77
|
+
self.servers = servers or {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FakeConfig:
|
|
81
|
+
"""Fake config manager for testing RAGMCPService."""
|
|
82
|
+
def __init__(self, rag_servers=None):
|
|
83
|
+
# Default RAG servers matching FakeMCP.available_tools
|
|
84
|
+
if rag_servers is None:
|
|
85
|
+
rag_servers = {
|
|
86
|
+
"docsRag": FakeMCPServerConfig(enabled=True, groups=["users"]),
|
|
87
|
+
"searchRag": FakeMCPServerConfig(enabled=True, groups=["users"]),
|
|
88
|
+
"misc": FakeMCPServerConfig(enabled=True, groups=["users"]),
|
|
89
|
+
}
|
|
90
|
+
self._rag_mcp_config = FakeMCPConfig(servers=rag_servers)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def rag_mcp_config(self):
|
|
94
|
+
return self._rag_mcp_config
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def fake_auth_check(user: str, group: str) -> bool:
|
|
98
|
+
"""Default auth check - everyone is in 'users' group."""
|
|
99
|
+
return group == "users"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.asyncio
|
|
103
|
+
async def test_discovery_flat_and_rich():
|
|
104
|
+
from atlas.domain.rag_mcp_service import RAGMCPService
|
|
105
|
+
|
|
106
|
+
svc = RAGMCPService(FakeMCP(), FakeConfig(), fake_auth_check)
|
|
107
|
+
|
|
108
|
+
flat = await svc.discover_data_sources("bob@example.com")
|
|
109
|
+
# misc has no rag_discover_resources, excluded
|
|
110
|
+
assert set(flat) == {"docsRag:handbook", "docsRag:legal", "searchRag:kb"}
|
|
111
|
+
|
|
112
|
+
rich = await svc.discover_servers("alice@example.com")
|
|
113
|
+
servers = {d["server"] for d in rich}
|
|
114
|
+
assert servers == {"docsRag", "searchRag"}
|
|
115
|
+
# docsRag must include two sources with defaultSelected on handbook
|
|
116
|
+
dr = next(s for s in rich if s["server"] == "docsRag")
|
|
117
|
+
assert any(x.get("selected") for x in dr["sources"]) # handbook default selected
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_search_and_synthesize_merge():
|
|
122
|
+
from atlas.domain.rag_mcp_service import RAGMCPService
|
|
123
|
+
|
|
124
|
+
svc = RAGMCPService(FakeMCP(), FakeConfig(), fake_auth_check)
|
|
125
|
+
res = await svc.search_raw(
|
|
126
|
+
username="bob@example.com",
|
|
127
|
+
query="policy",
|
|
128
|
+
sources=["docsRag:handbook", "searchRag:kb"],
|
|
129
|
+
top_k=2,
|
|
130
|
+
)
|
|
131
|
+
hits = res.get("results", {}).get("hits", [])
|
|
132
|
+
assert len(hits) == 2
|
|
133
|
+
# resourceId should be qualified
|
|
134
|
+
assert all(":" in h.get("resourceId", "") for h in hits)
|
|
135
|
+
|
|
136
|
+
syn = await svc.synthesize(
|
|
137
|
+
username="alice@example.com",
|
|
138
|
+
query="benefits",
|
|
139
|
+
sources=["searchRag:kb"],
|
|
140
|
+
)
|
|
141
|
+
answer = syn.get("results", {}).get("answer")
|
|
142
|
+
assert isinstance(answer, str) and "Synth for" in answer
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_rag_authorization_uses_rag_config_not_mcp_servers_config():
|
|
147
|
+
"""
|
|
148
|
+
Regression test: RAG authorization must use rag_mcp_config, not mcp_manager.servers_config.
|
|
149
|
+
|
|
150
|
+
Bug context: RAGMCPService temporarily adds RAG servers to mcp_manager.servers_config
|
|
151
|
+
for initialization, then restores the original config. If authorization checks
|
|
152
|
+
use servers_config (which no longer has RAG servers), no RAG sources are returned.
|
|
153
|
+
|
|
154
|
+
The fix is to check authorization directly against rag_mcp_config.servers.
|
|
155
|
+
"""
|
|
156
|
+
from atlas.domain.rag_mcp_service import RAGMCPService
|
|
157
|
+
|
|
158
|
+
class MCPWithEmptyServersConfig:
|
|
159
|
+
"""MCP manager with empty servers_config (RAG servers only in rag_mcp_config)."""
|
|
160
|
+
def __init__(self):
|
|
161
|
+
# Simulate RAG servers being initialized but NOT in servers_config
|
|
162
|
+
self.servers_config = {} # Empty! RAG servers were temporarily added then removed
|
|
163
|
+
self.clients = {"ragServer": object()} # Client exists (was initialized)
|
|
164
|
+
self.available_tools = {
|
|
165
|
+
"ragServer": {
|
|
166
|
+
"tools": [FakeTool("rag_discover_resources")],
|
|
167
|
+
"config": {}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async def get_authorized_servers(self, user: str, _auth) -> List[str]:
|
|
172
|
+
# This would return [] because servers_config is empty
|
|
173
|
+
return list(self.servers_config.keys())
|
|
174
|
+
|
|
175
|
+
async def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any], *_, **__):
|
|
176
|
+
if tool_name == "rag_discover_resources":
|
|
177
|
+
return types.SimpleNamespace(structured_content={
|
|
178
|
+
"results": {"resources": [
|
|
179
|
+
{"id": "doc1", "name": "Document 1"}
|
|
180
|
+
]}
|
|
181
|
+
})
|
|
182
|
+
return types.SimpleNamespace(structured_content={})
|
|
183
|
+
|
|
184
|
+
# RAG server configured in rag_mcp_config
|
|
185
|
+
rag_servers = {
|
|
186
|
+
"ragServer": FakeMCPServerConfig(enabled=True, groups=["users"])
|
|
187
|
+
}
|
|
188
|
+
fake_config = FakeConfig(rag_servers=rag_servers)
|
|
189
|
+
|
|
190
|
+
svc = RAGMCPService(MCPWithEmptyServersConfig(), fake_config, fake_auth_check)
|
|
191
|
+
|
|
192
|
+
# This should find ragServer even though mcp_manager.servers_config is empty
|
|
193
|
+
flat = await svc.discover_data_sources("user@example.com")
|
|
194
|
+
|
|
195
|
+
# Before the fix, this would return [] because authorization checked servers_config
|
|
196
|
+
# After the fix, this returns the RAG sources because authorization uses rag_mcp_config
|
|
197
|
+
assert "ragServer:doc1" in flat, \
|
|
198
|
+
"RAG authorization should use rag_mcp_config, not mcp_manager.servers_config"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.asyncio
|
|
202
|
+
async def test_rag_group_filtering():
|
|
203
|
+
"""Test that RAG sources are properly filtered by group membership."""
|
|
204
|
+
from atlas.domain.rag_mcp_service import RAGMCPService
|
|
205
|
+
|
|
206
|
+
# Server requires 'admin' group, not 'users'
|
|
207
|
+
rag_servers = {
|
|
208
|
+
"docsRag": FakeMCPServerConfig(enabled=True, groups=["admin"]),
|
|
209
|
+
"searchRag": FakeMCPServerConfig(enabled=True, groups=["users"]),
|
|
210
|
+
"misc": FakeMCPServerConfig(enabled=True, groups=["users"]),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async def restricted_auth_check(user: str, group: str) -> bool:
|
|
214
|
+
# User is only in 'users' group, not 'admin'
|
|
215
|
+
return group == "users"
|
|
216
|
+
|
|
217
|
+
svc = RAGMCPService(FakeMCP(), FakeConfig(rag_servers=rag_servers), restricted_auth_check)
|
|
218
|
+
|
|
219
|
+
flat = await svc.discover_data_sources("user@example.com")
|
|
220
|
+
|
|
221
|
+
# docsRag requires 'admin' group, user is not in admin
|
|
222
|
+
assert "docsRag:handbook" not in flat, "Admin-only RAG server should not be visible"
|
|
223
|
+
# searchRag is in 'users' group
|
|
224
|
+
assert "searchRag:kb" in flat, "User-accessible RAG server should be visible"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from starlette.testclient import TestClient
|
|
3
|
+
|
|
4
|
+
from atlas.core.rate_limit_middleware import RateLimitMiddleware
|
|
5
|
+
from atlas.modules.config import config_manager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_rate_limit_blocks_after_threshold():
|
|
9
|
+
# Configure very low limits via ConfigManager
|
|
10
|
+
settings = config_manager.app_settings
|
|
11
|
+
orig_rpm = settings.rate_limit_rpm
|
|
12
|
+
orig_window = settings.rate_limit_window_seconds
|
|
13
|
+
orig_per_path = settings.rate_limit_per_path
|
|
14
|
+
|
|
15
|
+
settings.rate_limit_rpm = 2
|
|
16
|
+
settings.rate_limit_window_seconds = 60
|
|
17
|
+
settings.rate_limit_per_path = False
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
app = FastAPI()
|
|
21
|
+
app.add_middleware(RateLimitMiddleware)
|
|
22
|
+
|
|
23
|
+
@app.get("/ping")
|
|
24
|
+
def ping():
|
|
25
|
+
return {"ok": True}
|
|
26
|
+
|
|
27
|
+
client = TestClient(app)
|
|
28
|
+
|
|
29
|
+
# First two requests should pass
|
|
30
|
+
r1 = client.get("/ping")
|
|
31
|
+
assert r1.status_code == 200
|
|
32
|
+
r2 = client.get("/ping")
|
|
33
|
+
assert r2.status_code == 200
|
|
34
|
+
|
|
35
|
+
# Third request within the window should be rate-limited
|
|
36
|
+
r3 = client.get("/ping")
|
|
37
|
+
assert r3.status_code == 429
|
|
38
|
+
data = r3.json()
|
|
39
|
+
assert "detail" in data
|
|
40
|
+
assert "Retry-After" in r3.headers
|
|
41
|
+
finally:
|
|
42
|
+
# Restore original settings to avoid side effects on other tests
|
|
43
|
+
settings.rate_limit_rpm = orig_rpm
|
|
44
|
+
settings.rate_limit_window_seconds = orig_window
|
|
45
|
+
settings.rate_limit_per_path = orig_per_path
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
2
|
+
|
|
3
|
+
from main import app
|
|
4
|
+
from starlette.testclient import TestClient
|
|
5
|
+
|
|
6
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_config_endpoint_smoke(monkeypatch):
|
|
10
|
+
client = TestClient(app)
|
|
11
|
+
resp = client.get("/api/config", headers={"X-User-Email": "test@test.com"})
|
|
12
|
+
# Endpoint should not crash; tolerate 200 with minimal fields
|
|
13
|
+
assert resp.status_code == 200
|
|
14
|
+
data = resp.json()
|
|
15
|
+
assert "app_name" in data
|
|
16
|
+
assert "models" in data
|
|
17
|
+
assert "tools" in data
|
|
18
|
+
assert "prompts" in data
|
|
19
|
+
assert "data_sources" in data
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_rag_discovery_skipped_when_feature_disabled(monkeypatch):
|
|
23
|
+
"""Verify RAG discovery is not attempted when feature_rag_enabled is False."""
|
|
24
|
+
# Create mock unified_rag_service to track if discover_data_sources is called
|
|
25
|
+
mock_unified_rag = MagicMock()
|
|
26
|
+
mock_unified_rag.discover_data_sources = AsyncMock(return_value=[])
|
|
27
|
+
|
|
28
|
+
# Create mock rag_mcp_service
|
|
29
|
+
mock_rag_mcp = MagicMock()
|
|
30
|
+
mock_rag_mcp.discover_data_sources = AsyncMock(return_value=[])
|
|
31
|
+
mock_rag_mcp.discover_servers = AsyncMock(return_value=[])
|
|
32
|
+
|
|
33
|
+
# Patch the app_factory methods
|
|
34
|
+
with patch.object(app_factory, 'get_unified_rag_service', return_value=mock_unified_rag):
|
|
35
|
+
with patch.object(app_factory, 'get_rag_mcp_service', return_value=mock_rag_mcp):
|
|
36
|
+
# Ensure RAG feature is disabled
|
|
37
|
+
config_manager = app_factory.get_config_manager()
|
|
38
|
+
original_setting = config_manager.app_settings.feature_rag_enabled
|
|
39
|
+
# Use object.__setattr__ to bypass Pydantic frozen model protection
|
|
40
|
+
object.__setattr__(config_manager.app_settings, 'feature_rag_enabled', False)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
client = TestClient(app)
|
|
44
|
+
resp = client.get("/api/config", headers={"X-User-Email": "test@test.com"})
|
|
45
|
+
assert resp.status_code == 200
|
|
46
|
+
|
|
47
|
+
# Verify RAG discovery was NOT called when feature is disabled
|
|
48
|
+
mock_unified_rag.discover_data_sources.assert_not_called()
|
|
49
|
+
mock_rag_mcp.discover_data_sources.assert_not_called()
|
|
50
|
+
mock_rag_mcp.discover_servers.assert_not_called()
|
|
51
|
+
|
|
52
|
+
# Verify response still has data_sources field (just empty)
|
|
53
|
+
data = resp.json()
|
|
54
|
+
assert "data_sources" in data
|
|
55
|
+
assert data["data_sources"] == []
|
|
56
|
+
assert "rag_servers" in data
|
|
57
|
+
assert data["rag_servers"] == []
|
|
58
|
+
finally:
|
|
59
|
+
# Restore original setting
|
|
60
|
+
object.__setattr__(config_manager.app_settings, 'feature_rag_enabled', original_setting)
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_files_download_with_token(monkeypatch):
|
|
10
|
+
client = TestClient(app)
|
|
11
|
+
|
|
12
|
+
# Prepare fake file in mock S3 by monkeypatching S3 client get_file
|
|
13
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
14
|
+
s3 = app_factory.get_file_storage()
|
|
15
|
+
|
|
16
|
+
async def fake_get_file(user, key):
|
|
17
|
+
return {
|
|
18
|
+
"key": key,
|
|
19
|
+
"filename": "hello.txt",
|
|
20
|
+
"content_base64": base64.b64encode(b"hello").decode(),
|
|
21
|
+
"content_type": "text/plain",
|
|
22
|
+
"size": 5,
|
|
23
|
+
"last_modified": "",
|
|
24
|
+
"etag": "",
|
|
25
|
+
"tags": {},
|
|
26
|
+
"user_email": user,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
monkeypatch.setattr(s3, "get_file", fake_get_file)
|
|
30
|
+
|
|
31
|
+
token = generate_file_token(user_email="test@test.com", file_key="k1", ttl_seconds=60)
|
|
32
|
+
|
|
33
|
+
resp = client.get(
|
|
34
|
+
"/api/files/download/k1",
|
|
35
|
+
params={"token": token},
|
|
36
|
+
headers={"X-User-Email": "ignored@example.com"}, # token overrides
|
|
37
|
+
)
|
|
38
|
+
assert resp.status_code == 200
|
|
39
|
+
assert resp.content == b"hello"
|
|
40
|
+
ct = resp.headers.get("content-type", "")
|
|
41
|
+
assert ct.startswith("text/plain")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from main import app
|
|
2
|
+
from starlette.testclient import TestClient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_files_health_endpoint(monkeypatch):
|
|
6
|
+
# Stub out S3 client call to avoid external dependency sensitivity
|
|
7
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
8
|
+
s3 = app_factory.get_file_storage()
|
|
9
|
+
# No network call is made by files/health; but ensure attributes exist
|
|
10
|
+
assert hasattr(s3, "endpoint_url")
|
|
11
|
+
|
|
12
|
+
client = TestClient(app)
|
|
13
|
+
resp = client.get("/api/files/healthz", headers={"X-User-Email": "test@test.com"})
|
|
14
|
+
assert resp.status_code == 200
|
|
15
|
+
data = resp.json()
|
|
16
|
+
assert data["status"] == "healthy"
|
|
17
|
+
assert data["service"] == "files-api"
|
|
18
|
+
assert "s3_config" in data
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_subprocess(code: str, cwd: Path, project_root: Path = None):
|
|
8
|
+
env = os.environ.copy()
|
|
9
|
+
# For the atlas package structure, ensure project root is in PYTHONPATH
|
|
10
|
+
if project_root:
|
|
11
|
+
env["PYTHONPATH"] = str(project_root)
|
|
12
|
+
else:
|
|
13
|
+
env.pop("PYTHONPATH", None)
|
|
14
|
+
proc = subprocess.run(
|
|
15
|
+
[sys.executable, "-c", code],
|
|
16
|
+
cwd=str(cwd),
|
|
17
|
+
env=env,
|
|
18
|
+
capture_output=True,
|
|
19
|
+
text=True,
|
|
20
|
+
timeout=30,
|
|
21
|
+
)
|
|
22
|
+
return proc
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_backend_dir_imports_work_without_project_root_in_path():
|
|
26
|
+
"""
|
|
27
|
+
Ensure imports work when running from the atlas directory (the supported run mode).
|
|
28
|
+
The atlas package requires the project root on PYTHONPATH for proper package imports.
|
|
29
|
+
This mirrors `bash agent_start.sh` which sets PYTHONPATH before running from ./atlas.
|
|
30
|
+
"""
|
|
31
|
+
atlas_dir = Path(__file__).resolve().parents[1]
|
|
32
|
+
project_root = atlas_dir.parent
|
|
33
|
+
|
|
34
|
+
code = (
|
|
35
|
+
"import main; "
|
|
36
|
+
"from atlas.modules.config import ConfigManager; "
|
|
37
|
+
"cm=ConfigManager(); "
|
|
38
|
+
"_ = cm.llm_config; _ = cm.mcp_config; _ = cm.rag_mcp_config; "
|
|
39
|
+
"print('OK')"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
proc = run_subprocess(code, atlas_dir, project_root=project_root)
|
|
43
|
+
|
|
44
|
+
# Helpful diagnostics on failure
|
|
45
|
+
if proc.returncode != 0:
|
|
46
|
+
print("STDOUT:\n" + proc.stdout)
|
|
47
|
+
print("STDERR:\n" + proc.stderr)
|
|
48
|
+
|
|
49
|
+
assert proc.returncode == 0, "Subprocess failed to import and initialize config from atlas dir"
|
|
50
|
+
# Guard against the specific regression seen in runtime warnings
|
|
51
|
+
assert "No module named 'atlas'" not in (proc.stdout + proc.stderr)
|
|
52
|
+
assert "Could not validate LLM compliance levels" not in (proc.stdout + proc.stderr)
|
|
53
|
+
assert "Could not validate MCP compliance levels" not in (proc.stdout + proc.stderr)
|