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,408 @@
|
|
|
1
|
+
"""Tests for the elicitation manager."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from atlas.application.chat.elicitation_manager import ElicitationManager, ElicitationRequest, get_elicitation_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestElicitationRequest:
|
|
11
|
+
"""Test ElicitationRequest class."""
|
|
12
|
+
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
async def test_create_elicitation_request(self):
|
|
15
|
+
"""Test creating an elicitation request."""
|
|
16
|
+
request = ElicitationRequest(
|
|
17
|
+
elicitation_id="elicit_123",
|
|
18
|
+
tool_call_id="tool_456",
|
|
19
|
+
tool_name="test_tool",
|
|
20
|
+
message="Please provide your name",
|
|
21
|
+
response_schema={"type": "object", "properties": {"value": {"type": "string"}}}
|
|
22
|
+
)
|
|
23
|
+
assert request.elicitation_id == "elicit_123"
|
|
24
|
+
assert request.tool_call_id == "tool_456"
|
|
25
|
+
assert request.tool_name == "test_tool"
|
|
26
|
+
assert request.message == "Please provide your name"
|
|
27
|
+
assert "properties" in request.response_schema
|
|
28
|
+
|
|
29
|
+
@pytest.mark.asyncio
|
|
30
|
+
async def test_wait_for_response_accept(self):
|
|
31
|
+
"""Test waiting for an accept response."""
|
|
32
|
+
request = ElicitationRequest(
|
|
33
|
+
elicitation_id="elicit_123",
|
|
34
|
+
tool_call_id="tool_456",
|
|
35
|
+
tool_name="test_tool",
|
|
36
|
+
message="Enter your name",
|
|
37
|
+
response_schema={"type": "object"}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Simulate setting a response
|
|
41
|
+
response_data = {"action": "accept", "data": {"name": "John"}}
|
|
42
|
+
request.future.set_result(response_data)
|
|
43
|
+
|
|
44
|
+
# Wait for the response (should be immediate since we already set it)
|
|
45
|
+
response = await request.wait_for_response(timeout=1.0)
|
|
46
|
+
|
|
47
|
+
assert response["action"] == "accept"
|
|
48
|
+
assert response["data"] == {"name": "John"}
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_wait_for_response_decline(self):
|
|
52
|
+
"""Test waiting for a decline response."""
|
|
53
|
+
request = ElicitationRequest(
|
|
54
|
+
elicitation_id="elicit_123",
|
|
55
|
+
tool_call_id="tool_456",
|
|
56
|
+
tool_name="test_tool",
|
|
57
|
+
message="Enter your name",
|
|
58
|
+
response_schema={"type": "object"}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Simulate decline response
|
|
62
|
+
response_data = {"action": "decline", "data": None}
|
|
63
|
+
request.future.set_result(response_data)
|
|
64
|
+
|
|
65
|
+
response = await request.wait_for_response(timeout=1.0)
|
|
66
|
+
|
|
67
|
+
assert response["action"] == "decline"
|
|
68
|
+
assert response["data"] is None
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_wait_for_response_cancel(self):
|
|
72
|
+
"""Test waiting for a cancel response."""
|
|
73
|
+
request = ElicitationRequest(
|
|
74
|
+
elicitation_id="elicit_123",
|
|
75
|
+
tool_call_id="tool_456",
|
|
76
|
+
tool_name="test_tool",
|
|
77
|
+
message="Enter your name",
|
|
78
|
+
response_schema={"type": "object"}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Simulate cancel response
|
|
82
|
+
response_data = {"action": "cancel", "data": None}
|
|
83
|
+
request.future.set_result(response_data)
|
|
84
|
+
|
|
85
|
+
response = await request.wait_for_response(timeout=1.0)
|
|
86
|
+
|
|
87
|
+
assert response["action"] == "cancel"
|
|
88
|
+
assert response["data"] is None
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_timeout(self):
|
|
92
|
+
"""Test that timeout works correctly."""
|
|
93
|
+
request = ElicitationRequest(
|
|
94
|
+
elicitation_id="elicit_123",
|
|
95
|
+
tool_call_id="tool_456",
|
|
96
|
+
tool_name="test_tool",
|
|
97
|
+
message="Enter your name",
|
|
98
|
+
response_schema={"type": "object"}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Should timeout since we don't set a response
|
|
102
|
+
with pytest.raises(asyncio.TimeoutError):
|
|
103
|
+
await request.wait_for_response(timeout=0.1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestElicitationManager:
|
|
107
|
+
"""Test ElicitationManager class."""
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_create_elicitation_request(self):
|
|
111
|
+
"""Test creating an elicitation request via manager."""
|
|
112
|
+
manager = ElicitationManager()
|
|
113
|
+
manager.create_elicitation_request(
|
|
114
|
+
elicitation_id="elicit_123",
|
|
115
|
+
tool_call_id="tool_456",
|
|
116
|
+
tool_name="test_tool",
|
|
117
|
+
message="Enter your name",
|
|
118
|
+
response_schema={"type": "object"}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert "elicit_123" in manager.get_all_pending_requests()
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_handle_elicitation_response_accept(self):
|
|
125
|
+
"""Test handling an accept response."""
|
|
126
|
+
manager = ElicitationManager()
|
|
127
|
+
manager.create_elicitation_request(
|
|
128
|
+
elicitation_id="elicit_123",
|
|
129
|
+
tool_call_id="tool_456",
|
|
130
|
+
tool_name="test_tool",
|
|
131
|
+
message="Enter your name",
|
|
132
|
+
response_schema={"type": "object"}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Handle the response
|
|
136
|
+
result = manager.handle_elicitation_response(
|
|
137
|
+
elicitation_id="elicit_123",
|
|
138
|
+
action="accept",
|
|
139
|
+
data={"name": "John"}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
assert result is True
|
|
143
|
+
|
|
144
|
+
@pytest.mark.asyncio
|
|
145
|
+
async def test_handle_elicitation_response_decline(self):
|
|
146
|
+
"""Test handling a decline response."""
|
|
147
|
+
manager = ElicitationManager()
|
|
148
|
+
manager.create_elicitation_request(
|
|
149
|
+
elicitation_id="elicit_123",
|
|
150
|
+
tool_call_id="tool_456",
|
|
151
|
+
tool_name="test_tool",
|
|
152
|
+
message="Enter your name",
|
|
153
|
+
response_schema={"type": "object"}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Handle decline response
|
|
157
|
+
result = manager.handle_elicitation_response(
|
|
158
|
+
elicitation_id="elicit_123",
|
|
159
|
+
action="decline",
|
|
160
|
+
data=None
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert result is True
|
|
164
|
+
|
|
165
|
+
@pytest.mark.asyncio
|
|
166
|
+
async def test_handle_elicitation_response_cancel(self):
|
|
167
|
+
"""Test handling a cancel response."""
|
|
168
|
+
manager = ElicitationManager()
|
|
169
|
+
manager.create_elicitation_request(
|
|
170
|
+
elicitation_id="elicit_123",
|
|
171
|
+
tool_call_id="tool_456",
|
|
172
|
+
tool_name="test_tool",
|
|
173
|
+
message="Enter your name",
|
|
174
|
+
response_schema={"type": "object"}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Handle cancel response
|
|
178
|
+
result = manager.handle_elicitation_response(
|
|
179
|
+
elicitation_id="elicit_123",
|
|
180
|
+
action="cancel",
|
|
181
|
+
data=None
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
assert result is True
|
|
185
|
+
|
|
186
|
+
def test_handle_unknown_elicitation(self):
|
|
187
|
+
"""Test handling response for unknown elicitation."""
|
|
188
|
+
manager = ElicitationManager()
|
|
189
|
+
|
|
190
|
+
# Try to handle response for non-existent elicitation
|
|
191
|
+
result = manager.handle_elicitation_response(
|
|
192
|
+
elicitation_id="unknown_123",
|
|
193
|
+
action="accept",
|
|
194
|
+
data={"name": "John"}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert result is False
|
|
198
|
+
|
|
199
|
+
@pytest.mark.asyncio
|
|
200
|
+
async def test_cleanup_request(self):
|
|
201
|
+
"""Test cleaning up an elicitation request."""
|
|
202
|
+
manager = ElicitationManager()
|
|
203
|
+
manager.create_elicitation_request(
|
|
204
|
+
elicitation_id="elicit_123",
|
|
205
|
+
tool_call_id="tool_456",
|
|
206
|
+
tool_name="test_tool",
|
|
207
|
+
message="Enter your name",
|
|
208
|
+
response_schema={"type": "object"}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Verify request exists
|
|
212
|
+
assert "elicit_123" in manager.get_all_pending_requests()
|
|
213
|
+
|
|
214
|
+
# Cleanup the request
|
|
215
|
+
manager.cleanup_request("elicit_123")
|
|
216
|
+
|
|
217
|
+
# Verify request is removed
|
|
218
|
+
assert "elicit_123" not in manager.get_all_pending_requests()
|
|
219
|
+
|
|
220
|
+
@pytest.mark.asyncio
|
|
221
|
+
async def test_get_pending_request(self):
|
|
222
|
+
"""Test retrieving a pending request."""
|
|
223
|
+
manager = ElicitationManager()
|
|
224
|
+
manager.create_elicitation_request(
|
|
225
|
+
elicitation_id="elicit_123",
|
|
226
|
+
tool_call_id="tool_456",
|
|
227
|
+
tool_name="test_tool",
|
|
228
|
+
message="Enter your name",
|
|
229
|
+
response_schema={"type": "object"}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Retrieve the request
|
|
233
|
+
retrieved = manager.get_pending_request("elicit_123")
|
|
234
|
+
|
|
235
|
+
assert retrieved is not None
|
|
236
|
+
assert retrieved.elicitation_id == "elicit_123"
|
|
237
|
+
assert retrieved.tool_call_id == "tool_456"
|
|
238
|
+
|
|
239
|
+
@pytest.mark.asyncio
|
|
240
|
+
async def test_get_pending_request_not_found(self):
|
|
241
|
+
"""Test retrieving non-existent request."""
|
|
242
|
+
manager = ElicitationManager()
|
|
243
|
+
|
|
244
|
+
# Try to get non-existent request
|
|
245
|
+
retrieved = manager.get_pending_request("unknown_123")
|
|
246
|
+
|
|
247
|
+
assert retrieved is None
|
|
248
|
+
|
|
249
|
+
@pytest.mark.asyncio
|
|
250
|
+
async def test_cancel_all_requests(self):
|
|
251
|
+
"""Test cancelling all pending requests."""
|
|
252
|
+
manager = ElicitationManager()
|
|
253
|
+
|
|
254
|
+
# Create multiple requests
|
|
255
|
+
manager.create_elicitation_request(
|
|
256
|
+
elicitation_id="elicit_1",
|
|
257
|
+
tool_call_id="tool_1",
|
|
258
|
+
tool_name="test_tool",
|
|
259
|
+
message="Request 1",
|
|
260
|
+
response_schema={"type": "object"}
|
|
261
|
+
)
|
|
262
|
+
manager.create_elicitation_request(
|
|
263
|
+
elicitation_id="elicit_2",
|
|
264
|
+
tool_call_id="tool_2",
|
|
265
|
+
tool_name="test_tool",
|
|
266
|
+
message="Request 2",
|
|
267
|
+
response_schema={"type": "object"}
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Verify both exist
|
|
271
|
+
assert len(manager.get_all_pending_requests()) == 2
|
|
272
|
+
|
|
273
|
+
# Cancel all requests
|
|
274
|
+
manager.cancel_all_requests()
|
|
275
|
+
|
|
276
|
+
# Verify all are removed
|
|
277
|
+
assert len(manager.get_all_pending_requests()) == 0
|
|
278
|
+
|
|
279
|
+
def test_get_elicitation_manager_singleton(self):
|
|
280
|
+
"""Test that get_elicitation_manager returns singleton."""
|
|
281
|
+
manager1 = get_elicitation_manager()
|
|
282
|
+
manager2 = get_elicitation_manager()
|
|
283
|
+
|
|
284
|
+
assert manager1 is manager2
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestElicitationManagerIntegration:
|
|
288
|
+
"""Integration tests for ElicitationManager."""
|
|
289
|
+
|
|
290
|
+
@pytest.mark.asyncio
|
|
291
|
+
async def test_full_elicitation_flow(self):
|
|
292
|
+
"""Test complete elicitation flow from request to response."""
|
|
293
|
+
manager = ElicitationManager()
|
|
294
|
+
|
|
295
|
+
# Create request
|
|
296
|
+
request = manager.create_elicitation_request(
|
|
297
|
+
elicitation_id="elicit_123",
|
|
298
|
+
tool_call_id="tool_456",
|
|
299
|
+
tool_name="test_tool",
|
|
300
|
+
message="Enter your information",
|
|
301
|
+
response_schema={
|
|
302
|
+
"type": "object",
|
|
303
|
+
"properties": {
|
|
304
|
+
"name": {"type": "string"},
|
|
305
|
+
"age": {"type": "integer"}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Simulate async handling
|
|
311
|
+
async def simulate_user_response():
|
|
312
|
+
# Wait a bit to simulate user thinking
|
|
313
|
+
await asyncio.sleep(0.1)
|
|
314
|
+
# User responds
|
|
315
|
+
manager.handle_elicitation_response(
|
|
316
|
+
elicitation_id="elicit_123",
|
|
317
|
+
action="accept",
|
|
318
|
+
data={"name": "Alice", "age": 30}
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Start the simulation
|
|
322
|
+
asyncio.create_task(simulate_user_response())
|
|
323
|
+
|
|
324
|
+
# Wait for response
|
|
325
|
+
response = await request.wait_for_response(timeout=2.0)
|
|
326
|
+
|
|
327
|
+
assert response["action"] == "accept"
|
|
328
|
+
assert response["data"]["name"] == "Alice"
|
|
329
|
+
assert response["data"]["age"] == 30
|
|
330
|
+
|
|
331
|
+
# Cleanup
|
|
332
|
+
manager.cleanup_request("elicit_123")
|
|
333
|
+
assert "elicit_123" not in manager.get_all_pending_requests()
|
|
334
|
+
|
|
335
|
+
@pytest.mark.asyncio
|
|
336
|
+
async def test_multi_turn_elicitation(self):
|
|
337
|
+
"""Test handling multiple sequential elicitation requests."""
|
|
338
|
+
manager = ElicitationManager()
|
|
339
|
+
|
|
340
|
+
# First elicitation
|
|
341
|
+
request1 = manager.create_elicitation_request(
|
|
342
|
+
elicitation_id="elicit_1",
|
|
343
|
+
tool_call_id="tool_456",
|
|
344
|
+
tool_name="test_tool",
|
|
345
|
+
message="Enter your name",
|
|
346
|
+
response_schema={"type": "object"}
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Immediately respond to first
|
|
350
|
+
manager.handle_elicitation_response(
|
|
351
|
+
elicitation_id="elicit_1",
|
|
352
|
+
action="accept",
|
|
353
|
+
data={"name": "Bob"}
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
response1 = await request1.wait_for_response(timeout=1.0)
|
|
357
|
+
assert response1["action"] == "accept"
|
|
358
|
+
|
|
359
|
+
manager.cleanup_request("elicit_1")
|
|
360
|
+
|
|
361
|
+
# Second elicitation
|
|
362
|
+
request2 = manager.create_elicitation_request(
|
|
363
|
+
elicitation_id="elicit_2",
|
|
364
|
+
tool_call_id="tool_456",
|
|
365
|
+
tool_name="test_tool",
|
|
366
|
+
message="Enter your age",
|
|
367
|
+
response_schema={"type": "object"}
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Respond to second
|
|
371
|
+
manager.handle_elicitation_response(
|
|
372
|
+
elicitation_id="elicit_2",
|
|
373
|
+
action="accept",
|
|
374
|
+
data={"age": 25}
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
response2 = await request2.wait_for_response(timeout=1.0)
|
|
378
|
+
assert response2["action"] == "accept"
|
|
379
|
+
assert response2["data"]["age"] == 25
|
|
380
|
+
|
|
381
|
+
manager.cleanup_request("elicit_2")
|
|
382
|
+
|
|
383
|
+
@pytest.mark.asyncio
|
|
384
|
+
async def test_elicitation_with_decline(self):
|
|
385
|
+
"""Test elicitation flow when user declines."""
|
|
386
|
+
manager = ElicitationManager()
|
|
387
|
+
|
|
388
|
+
request = manager.create_elicitation_request(
|
|
389
|
+
elicitation_id="elicit_123",
|
|
390
|
+
tool_call_id="tool_456",
|
|
391
|
+
tool_name="test_tool",
|
|
392
|
+
message="Enter optional information",
|
|
393
|
+
response_schema={"type": "object"}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# User declines
|
|
397
|
+
manager.handle_elicitation_response(
|
|
398
|
+
elicitation_id="elicit_123",
|
|
399
|
+
action="decline",
|
|
400
|
+
data=None
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
response = await request.wait_for_response(timeout=1.0)
|
|
404
|
+
|
|
405
|
+
assert response["action"] == "decline"
|
|
406
|
+
assert response["data"] is None
|
|
407
|
+
|
|
408
|
+
manager.cleanup_request("elicit_123")
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for MCP elicitation routing functionality.
|
|
3
|
+
|
|
4
|
+
Tests the dictionary-based routing system that allows elicitation requests
|
|
5
|
+
from MCP tools to reach the correct WebSocket connection across async tasks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from atlas.domain.messages.models import ToolCall
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestElicitationRouting:
|
|
16
|
+
"""Test elicitation routing context management."""
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_tool_call(self):
|
|
20
|
+
"""Create a mock ToolCall object."""
|
|
21
|
+
return ToolCall(
|
|
22
|
+
id="test_call_123",
|
|
23
|
+
name="elicitation_demo_get_user_name",
|
|
24
|
+
arguments={}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def mock_update_callback(self):
|
|
29
|
+
"""Create a mock update callback."""
|
|
30
|
+
return AsyncMock()
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def manager(self):
|
|
34
|
+
"""Create a MCPToolManager instance for testing."""
|
|
35
|
+
from atlas.modules.mcp_tools.client import MCPToolManager
|
|
36
|
+
return MCPToolManager(config_path="/tmp/nonexistent_mcp_test.json")
|
|
37
|
+
|
|
38
|
+
@pytest.mark.asyncio
|
|
39
|
+
async def test_elicitation_context_sets_routing(self, manager, mock_tool_call, mock_update_callback):
|
|
40
|
+
"""Test that elicitation context correctly sets routing in dictionary."""
|
|
41
|
+
from atlas.modules.mcp_tools.client import _ELICITATION_ROUTING
|
|
42
|
+
|
|
43
|
+
# Clear routing before test
|
|
44
|
+
_ELICITATION_ROUTING.clear()
|
|
45
|
+
|
|
46
|
+
server_name = "test_server"
|
|
47
|
+
routing_key = (server_name, mock_tool_call.id)
|
|
48
|
+
|
|
49
|
+
# Use the context manager
|
|
50
|
+
async with manager._use_elicitation_context(server_name, mock_tool_call, mock_update_callback):
|
|
51
|
+
# Inside context: routing should exist with composite key
|
|
52
|
+
assert routing_key in _ELICITATION_ROUTING
|
|
53
|
+
routing = _ELICITATION_ROUTING[routing_key]
|
|
54
|
+
assert routing.server_name == server_name
|
|
55
|
+
assert routing.tool_call == mock_tool_call
|
|
56
|
+
assert routing.update_cb == mock_update_callback
|
|
57
|
+
|
|
58
|
+
# After context: routing should be cleaned up
|
|
59
|
+
assert routing_key not in _ELICITATION_ROUTING
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_elicitation_routing_cleanup_on_error(self, manager, mock_tool_call, mock_update_callback):
|
|
63
|
+
"""Test that routing is cleaned up even if error occurs."""
|
|
64
|
+
from atlas.modules.mcp_tools.client import _ELICITATION_ROUTING
|
|
65
|
+
|
|
66
|
+
_ELICITATION_ROUTING.clear()
|
|
67
|
+
|
|
68
|
+
server_name = "test_server"
|
|
69
|
+
routing_key = (server_name, mock_tool_call.id)
|
|
70
|
+
|
|
71
|
+
# Simulate an error inside the context
|
|
72
|
+
with pytest.raises(RuntimeError):
|
|
73
|
+
async with manager._use_elicitation_context(server_name, mock_tool_call, mock_update_callback):
|
|
74
|
+
assert routing_key in _ELICITATION_ROUTING
|
|
75
|
+
raise RuntimeError("Simulated error")
|
|
76
|
+
|
|
77
|
+
# Routing should still be cleaned up
|
|
78
|
+
assert routing_key not in _ELICITATION_ROUTING
|
|
79
|
+
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
async def test_multiple_servers_routing(self, manager, mock_update_callback):
|
|
82
|
+
"""Test that multiple servers can have separate routing contexts."""
|
|
83
|
+
from atlas.modules.mcp_tools.client import _ELICITATION_ROUTING
|
|
84
|
+
|
|
85
|
+
_ELICITATION_ROUTING.clear()
|
|
86
|
+
|
|
87
|
+
tool_call_1 = ToolCall(id="call_1", name="tool_1", arguments={})
|
|
88
|
+
tool_call_2 = ToolCall(id="call_2", name="tool_2", arguments={})
|
|
89
|
+
routing_key_1 = ("server_1", "call_1")
|
|
90
|
+
routing_key_2 = ("server_2", "call_2")
|
|
91
|
+
|
|
92
|
+
# Create contexts for two different servers
|
|
93
|
+
async with manager._use_elicitation_context("server_1", tool_call_1, mock_update_callback):
|
|
94
|
+
async with manager._use_elicitation_context("server_2", tool_call_2, mock_update_callback):
|
|
95
|
+
# Both should exist simultaneously
|
|
96
|
+
assert routing_key_1 in _ELICITATION_ROUTING
|
|
97
|
+
assert routing_key_2 in _ELICITATION_ROUTING
|
|
98
|
+
assert _ELICITATION_ROUTING[routing_key_1].tool_call == tool_call_1
|
|
99
|
+
assert _ELICITATION_ROUTING[routing_key_2].tool_call == tool_call_2
|
|
100
|
+
|
|
101
|
+
# server_2 cleaned up, server_1 still exists
|
|
102
|
+
assert routing_key_1 in _ELICITATION_ROUTING
|
|
103
|
+
assert routing_key_2 not in _ELICITATION_ROUTING
|
|
104
|
+
|
|
105
|
+
# Both cleaned up
|
|
106
|
+
assert routing_key_1 not in _ELICITATION_ROUTING
|
|
107
|
+
assert routing_key_2 not in _ELICITATION_ROUTING
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_elicitation_with_none_callback(self, manager, mock_tool_call):
|
|
111
|
+
"""Test elicitation context with None callback (should still work)."""
|
|
112
|
+
from atlas.modules.mcp_tools.client import _ELICITATION_ROUTING
|
|
113
|
+
|
|
114
|
+
_ELICITATION_ROUTING.clear()
|
|
115
|
+
|
|
116
|
+
server_name = "test_server"
|
|
117
|
+
routing_key = (server_name, mock_tool_call.id)
|
|
118
|
+
|
|
119
|
+
# Use context with None callback
|
|
120
|
+
async with manager._use_elicitation_context(server_name, mock_tool_call, None):
|
|
121
|
+
routing = _ELICITATION_ROUTING[routing_key]
|
|
122
|
+
assert routing.update_cb is None
|
|
123
|
+
|
|
124
|
+
assert routing_key not in _ELICITATION_ROUTING
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestElicitationHandler:
|
|
128
|
+
"""Test per-server elicitation handler creation."""
|
|
129
|
+
|
|
130
|
+
@pytest.fixture
|
|
131
|
+
def manager(self):
|
|
132
|
+
"""Create a MCPToolManager instance for testing."""
|
|
133
|
+
from atlas.modules.mcp_tools.client import MCPToolManager
|
|
134
|
+
return MCPToolManager(config_path="/tmp/nonexistent_mcp_test.json")
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
async def test_handler_creation_captures_server_name(self, manager):
|
|
138
|
+
"""Test that handler closure captures the correct server_name."""
|
|
139
|
+
|
|
140
|
+
# Create handlers for different servers
|
|
141
|
+
handler_1 = manager._create_elicitation_handler("server_1")
|
|
142
|
+
handler_2 = manager._create_elicitation_handler("server_2")
|
|
143
|
+
|
|
144
|
+
# Handlers should be different functions (different closures)
|
|
145
|
+
assert handler_1 != handler_2
|
|
146
|
+
assert callable(handler_1)
|
|
147
|
+
assert callable(handler_2)
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def test_handler_returns_cancel_when_no_routing(self, manager):
|
|
151
|
+
"""Test that handler returns cancel when routing not found."""
|
|
152
|
+
from fastmcp.client.elicitation import ElicitResult
|
|
153
|
+
|
|
154
|
+
from atlas.modules.mcp_tools.client import _ELICITATION_ROUTING
|
|
155
|
+
|
|
156
|
+
_ELICITATION_ROUTING.clear()
|
|
157
|
+
|
|
158
|
+
handler = manager._create_elicitation_handler("test_server")
|
|
159
|
+
|
|
160
|
+
# Call handler with no routing set
|
|
161
|
+
result = await handler("Test message", str, None, None)
|
|
162
|
+
|
|
163
|
+
assert isinstance(result, ElicitResult)
|
|
164
|
+
assert result.action == "cancel"
|
|
165
|
+
assert result.content is None
|
|
166
|
+
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
async def test_handler_returns_cancel_when_no_update_cb(self, manager):
|
|
169
|
+
"""Test that handler returns cancel when update_cb is None."""
|
|
170
|
+
from fastmcp.client.elicitation import ElicitResult
|
|
171
|
+
|
|
172
|
+
from atlas.domain.messages.models import ToolCall
|
|
173
|
+
from atlas.modules.mcp_tools.client import _ELICITATION_ROUTING, _ElicitationRoutingContext
|
|
174
|
+
|
|
175
|
+
_ELICITATION_ROUTING.clear()
|
|
176
|
+
|
|
177
|
+
server_name = "test_server"
|
|
178
|
+
tool_call = ToolCall(id="call_123", name="test_tool", arguments={})
|
|
179
|
+
routing_key = (server_name, tool_call.id)
|
|
180
|
+
|
|
181
|
+
# Set routing with None callback using composite key
|
|
182
|
+
_ELICITATION_ROUTING[routing_key] = _ElicitationRoutingContext(
|
|
183
|
+
server_name=server_name,
|
|
184
|
+
tool_call=tool_call,
|
|
185
|
+
update_cb=None
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
handler = manager._create_elicitation_handler(server_name)
|
|
189
|
+
result = await handler("Test message", str, None, None)
|
|
190
|
+
|
|
191
|
+
assert isinstance(result, ElicitResult)
|
|
192
|
+
assert result.action == "cancel"
|
|
193
|
+
assert result.content is None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TestElicitationIntegration:
|
|
197
|
+
"""Integration tests for elicitation workflow."""
|
|
198
|
+
|
|
199
|
+
@pytest.fixture
|
|
200
|
+
def manager(self):
|
|
201
|
+
"""Create a MCPToolManager instance for testing."""
|
|
202
|
+
from atlas.modules.mcp_tools.client import MCPToolManager
|
|
203
|
+
return MCPToolManager(config_path="/tmp/nonexistent_mcp_test.json")
|
|
204
|
+
|
|
205
|
+
@pytest.mark.asyncio
|
|
206
|
+
async def test_elicitation_request_sent_to_callback(self, manager):
|
|
207
|
+
"""Test that elicitation request is sent to update callback."""
|
|
208
|
+
from atlas.domain.messages.models import ToolCall
|
|
209
|
+
from atlas.modules.mcp_tools.client import _ELICITATION_ROUTING, _ElicitationRoutingContext
|
|
210
|
+
|
|
211
|
+
_ELICITATION_ROUTING.clear()
|
|
212
|
+
|
|
213
|
+
server_name = "test_server"
|
|
214
|
+
tool_call = ToolCall(id="call_123", name="test_tool", arguments={})
|
|
215
|
+
mock_callback = AsyncMock()
|
|
216
|
+
routing_key = (server_name, tool_call.id)
|
|
217
|
+
|
|
218
|
+
# Set routing with mock callback using composite key
|
|
219
|
+
_ELICITATION_ROUTING[routing_key] = _ElicitationRoutingContext(
|
|
220
|
+
server_name=server_name,
|
|
221
|
+
tool_call=tool_call,
|
|
222
|
+
update_cb=mock_callback
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
handler = manager._create_elicitation_handler(server_name)
|
|
226
|
+
|
|
227
|
+
# Mock elicitation manager
|
|
228
|
+
with patch('atlas.application.chat.elicitation_manager.get_elicitation_manager') as mock_get_mgr:
|
|
229
|
+
mock_elicit_mgr = Mock()
|
|
230
|
+
mock_request = AsyncMock()
|
|
231
|
+
mock_request.wait_for_response = AsyncMock(return_value={
|
|
232
|
+
"action": "accept",
|
|
233
|
+
"data": "test_value"
|
|
234
|
+
})
|
|
235
|
+
mock_elicit_mgr.create_elicitation_request = Mock(return_value=mock_request)
|
|
236
|
+
mock_elicit_mgr.cleanup_request = Mock()
|
|
237
|
+
mock_get_mgr.return_value = mock_elicit_mgr
|
|
238
|
+
|
|
239
|
+
result = await handler("What's your name?", str, None, None)
|
|
240
|
+
|
|
241
|
+
# Verify callback was called with elicitation_request
|
|
242
|
+
mock_callback.assert_called_once()
|
|
243
|
+
call_args = mock_callback.call_args[0][0]
|
|
244
|
+
assert call_args["type"] == "elicitation_request"
|
|
245
|
+
assert call_args["message"] == "What's your name?"
|
|
246
|
+
assert call_args["tool_call_id"] == "call_123"
|
|
247
|
+
|
|
248
|
+
# Verify result
|
|
249
|
+
assert result.action == "accept"
|
|
250
|
+
assert result.content == {"value": "test_value"}
|
|
251
|
+
|
|
252
|
+
@pytest.mark.asyncio
|
|
253
|
+
async def test_elicitation_accept_no_data_returns_empty_object(self, manager):
|
|
254
|
+
"""Test approval-only elicitation returns empty object on accept.
|
|
255
|
+
|
|
256
|
+
FastMCP validation for response_type=None expects an empty response object.
|
|
257
|
+
Some UIs send placeholder payloads like {'none': ''}; we must not forward them.
|
|
258
|
+
"""
|
|
259
|
+
from atlas.domain.messages.models import ToolCall
|
|
260
|
+
from atlas.modules.mcp_tools.client import _ELICITATION_ROUTING, _ElicitationRoutingContext
|
|
261
|
+
|
|
262
|
+
_ELICITATION_ROUTING.clear()
|
|
263
|
+
|
|
264
|
+
server_name = "test_server"
|
|
265
|
+
tool_call = ToolCall(id="call_123", name="test_tool", arguments={})
|
|
266
|
+
mock_callback = AsyncMock()
|
|
267
|
+
routing_key = (server_name, tool_call.id)
|
|
268
|
+
|
|
269
|
+
_ELICITATION_ROUTING[routing_key] = _ElicitationRoutingContext(
|
|
270
|
+
server_name=server_name,
|
|
271
|
+
tool_call=tool_call,
|
|
272
|
+
update_cb=mock_callback,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
handler = manager._create_elicitation_handler(server_name)
|
|
276
|
+
|
|
277
|
+
with patch('atlas.application.chat.elicitation_manager.get_elicitation_manager') as mock_get_mgr:
|
|
278
|
+
mock_elicit_mgr = Mock()
|
|
279
|
+
mock_request = AsyncMock()
|
|
280
|
+
mock_request.wait_for_response = AsyncMock(return_value={
|
|
281
|
+
"action": "accept",
|
|
282
|
+
"data": {"none": ""},
|
|
283
|
+
})
|
|
284
|
+
mock_elicit_mgr.create_elicitation_request = Mock(return_value=mock_request)
|
|
285
|
+
mock_elicit_mgr.cleanup_request = Mock()
|
|
286
|
+
mock_get_mgr.return_value = mock_elicit_mgr
|
|
287
|
+
|
|
288
|
+
result = await handler(
|
|
289
|
+
"Are you sure you want to delete this item?",
|
|
290
|
+
None,
|
|
291
|
+
None,
|
|
292
|
+
None,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
assert result.action == "accept"
|
|
296
|
+
assert result.content == {}
|