test: interaction-model end-to-end suite with a requirements manifest (#2691)
This commit is contained in:
parent
3eb579948a
commit
24725633f1
4 changed files with 14355 additions and 121 deletions
|
|
@ -360,10 +360,10 @@ class OAuthClientProvider(httpx.Auth):
|
|||
auth_code, returned_state = await self.context.callback_handler()
|
||||
|
||||
if returned_state is None or not secrets.compare_digest(returned_state, state):
|
||||
raise OAuthFlowError(f"State parameter mismatch: {returned_state} != {state}") # pragma: no cover
|
||||
raise OAuthFlowError(f"State parameter mismatch: {returned_state} != {state}")
|
||||
|
||||
if not auth_code:
|
||||
raise OAuthFlowError("No authorization code received") # pragma: no cover
|
||||
raise OAuthFlowError("No authorization code received")
|
||||
|
||||
# Return auth code and code verifier for token exchange
|
||||
return auth_code, pkce_params.code_verifier
|
||||
|
|
@ -452,7 +452,7 @@ class OAuthClientProvider(httpx.Auth):
|
|||
|
||||
return httpx.Request("POST", token_url, data=refresh_data, headers=headers)
|
||||
|
||||
async def _handle_refresh_response(self, response: httpx.Response) -> bool: # pragma: no cover
|
||||
async def _handle_refresh_response(self, response: httpx.Response) -> bool:
|
||||
"""Handle token refresh response. Returns True if successful."""
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Token refresh failed: {response.status_code}")
|
||||
|
|
@ -468,12 +468,12 @@ class OAuthClientProvider(httpx.Auth):
|
|||
await self.context.storage.set_tokens(token_response)
|
||||
|
||||
return True
|
||||
except ValidationError:
|
||||
except ValidationError: # pragma: no cover
|
||||
logger.exception("Invalid refresh response")
|
||||
self.context.clear_tokens()
|
||||
return False
|
||||
|
||||
async def _initialize(self) -> None: # pragma: no cover
|
||||
async def _initialize(self) -> None:
|
||||
"""Load stored tokens and client info."""
|
||||
self.context.current_tokens = await self.context.storage.get_tokens()
|
||||
self.context.client_info = await self.context.storage.get_client_info()
|
||||
|
|
@ -507,17 +507,17 @@ class OAuthClientProvider(httpx.Auth):
|
|||
"""HTTPX auth flow integration."""
|
||||
async with self.context.lock:
|
||||
if not self._initialized:
|
||||
await self._initialize() # pragma: no cover
|
||||
await self._initialize()
|
||||
|
||||
# Capture protocol version from request headers
|
||||
self.context.protocol_version = request.headers.get(MCP_PROTOCOL_VERSION)
|
||||
|
||||
if not self.context.is_token_valid() and self.context.can_refresh_token():
|
||||
# Try to refresh token
|
||||
refresh_request = await self._refresh_token() # pragma: no cover
|
||||
refresh_response = yield refresh_request # pragma: no cover
|
||||
refresh_request = await self._refresh_token()
|
||||
refresh_response = yield refresh_request
|
||||
|
||||
if not await self._handle_refresh_response(refresh_response): # pragma: no cover
|
||||
if not await self._handle_refresh_response(refresh_response):
|
||||
# Refresh failed, need full re-authentication
|
||||
self._initialized = False
|
||||
|
||||
|
|
@ -612,7 +612,7 @@ class OAuthClientProvider(httpx.Auth):
|
|||
# Step 5: Perform authorization and complete token exchange
|
||||
token_response = yield await self._perform_authorization()
|
||||
await self._handle_token_response(token_response)
|
||||
except Exception: # pragma: no cover
|
||||
except Exception:
|
||||
logger.exception("OAuth flow error")
|
||||
raise
|
||||
|
||||
|
|
|
|||
|
|
@ -305,4 +305,4 @@ class Client:
|
|||
async def send_roots_list_changed(self) -> None:
|
||||
"""Send a notification that the roots list has changed."""
|
||||
# TODO(Marcelo): Currently, there is no way for the server to handle this. We should add support.
|
||||
await self.session.send_roots_list_changed() # pragma: no cover
|
||||
await self.session.send_roots_list_changed()
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ async def _default_elicitation_callback(
|
|||
context: RequestContext[ClientSession],
|
||||
params: types.ElicitRequestParams,
|
||||
) -> types.ElicitResult | types.ErrorData:
|
||||
return types.ErrorData( # pragma: no cover
|
||||
return types.ErrorData(
|
||||
code=types.INVALID_REQUEST,
|
||||
message="Elicitation not supported",
|
||||
)
|
||||
|
|
@ -337,9 +337,7 @@ class ClientSession(
|
|||
from jsonschema import SchemaError, ValidationError, validate
|
||||
|
||||
if result.structured_content is None:
|
||||
raise RuntimeError(
|
||||
f"Tool {name} has an output schema but did not return structured content"
|
||||
) # pragma: no cover
|
||||
raise RuntimeError(f"Tool {name} has an output schema but did not return structured content")
|
||||
try:
|
||||
validate(result.structured_content, output_schema)
|
||||
except ValidationError as e:
|
||||
|
|
@ -408,7 +406,7 @@ class ClientSession(
|
|||
|
||||
return result
|
||||
|
||||
async def send_roots_list_changed(self) -> None: # pragma: no cover
|
||||
async def send_roots_list_changed(self) -> None:
|
||||
"""Send a roots/list_changed notification."""
|
||||
await self.send_notification(types.RootsListChangedNotification())
|
||||
|
||||
|
|
@ -449,7 +447,7 @@ class ClientSession(
|
|||
client_response = ClientResponse.validate_python(response)
|
||||
await responder.respond(client_response)
|
||||
|
||||
case types.PingRequest(): # pragma: no cover
|
||||
case types.PingRequest():
|
||||
with responder:
|
||||
return await responder.respond(types.EmptyResult())
|
||||
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ class StreamableHTTPTransport:
|
|||
# Stream ended normally (server closed) - reset attempt counter
|
||||
attempt = 0
|
||||
|
||||
except Exception: # pragma: lax no cover
|
||||
except Exception:
|
||||
logger.debug("GET stream error", exc_info=True)
|
||||
attempt += 1
|
||||
|
||||
|
|
@ -267,8 +267,8 @@ class StreamableHTTPTransport:
|
|||
logger.debug("Received 202 Accepted")
|
||||
return
|
||||
|
||||
if response.status_code == 404: # pragma: no branch
|
||||
if isinstance(message, JSONRPCRequest): # pragma: no branch
|
||||
if response.status_code == 404:
|
||||
if isinstance(message, JSONRPCRequest):
|
||||
error_data = ErrorData(code=INVALID_REQUEST, message="Session terminated")
|
||||
session_message = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data))
|
||||
await ctx.read_stream_writer.send(session_message)
|
||||
|
|
@ -492,17 +492,17 @@ class StreamableHTTPTransport:
|
|||
|
||||
async def terminate_session(self, client: httpx.AsyncClient) -> None:
|
||||
"""Terminate the session by sending a DELETE request."""
|
||||
if not self.session_id: # pragma: lax no cover
|
||||
return
|
||||
if not self.session_id:
|
||||
return # pragma: no cover
|
||||
|
||||
try:
|
||||
headers = self._prepare_headers()
|
||||
response = await client.delete(self.url, headers=headers)
|
||||
|
||||
if response.status_code == 405: # pragma: lax no cover
|
||||
if response.status_code == 405:
|
||||
logger.debug("Server does not allow session termination")
|
||||
elif response.status_code not in (200, 204): # pragma: lax no cover
|
||||
logger.warning(f"Session termination failed: {response.status_code}")
|
||||
elif response.status_code not in (200, 204):
|
||||
logger.warning(f"Session termination failed: {response.status_code}") # pragma: no cover
|
||||
except Exception as exc: # pragma: no cover
|
||||
logger.warning(f"Session termination failed: {exc}")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue