first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use rmcp::model::{ErrorCode, ErrorData, Resource, ServerCapabilities, Tool};
|
||||
|
||||
use super::{query_resources_for, query_tools_for, should_query_resources, should_query_tools};
|
||||
|
||||
/// Build a `ServerCapabilities` with selected capability flags toggled on.
|
||||
/// Each `Some(default)` mirrors how rmcp deserializes a capability the
|
||||
/// server advertised with no inner flags set.
|
||||
fn caps(tools: bool, resources: bool) -> ServerCapabilities {
|
||||
match (tools, resources) {
|
||||
(true, true) => ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_resources()
|
||||
.build(),
|
||||
(true, false) => ServerCapabilities::builder().enable_tools().build(),
|
||||
(false, true) => ServerCapabilities::builder().enable_resources().build(),
|
||||
(false, false) => ServerCapabilities::builder().build(),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_tool(name: &str) -> Tool {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"name": name,
|
||||
"description": "test tool",
|
||||
"inputSchema": { "type": "object" },
|
||||
}))
|
||||
.expect("Tool deserialization")
|
||||
}
|
||||
|
||||
fn test_resource(uri: &str) -> Resource {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"uri": uri,
|
||||
"name": "test resource",
|
||||
}))
|
||||
.expect("Resource deserialization")
|
||||
}
|
||||
|
||||
/// Regression test for warpdotdev/warp#6798: each capability is queried
|
||||
/// independently. Previously, asymmetric handling could cause `tools/list`
|
||||
/// to be skipped when a server advertised both `tools` and `resources`,
|
||||
/// resulting in "No tools available" even though the server had tools.
|
||||
#[test]
|
||||
fn each_capability_is_queried_independently() {
|
||||
for has_tools in [false, true] {
|
||||
for has_resources in [false, true] {
|
||||
let c = caps(has_tools, has_resources);
|
||||
assert_eq!(
|
||||
should_query_tools(Some(&c)),
|
||||
has_tools,
|
||||
"tools={has_tools}, resources={has_resources}",
|
||||
);
|
||||
assert_eq!(
|
||||
should_query_resources(Some(&c)),
|
||||
has_resources,
|
||||
"tools={has_tools}, resources={has_resources}",
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(!should_query_tools(None));
|
||||
assert!(!should_query_resources(None));
|
||||
}
|
||||
|
||||
/// When `tools` is not advertised, the helper must skip the list call so
|
||||
/// we don't waste a round trip and pollute the wire log with a request
|
||||
/// that's destined to return `METHOD_NOT_FOUND`.
|
||||
#[tokio::test]
|
||||
async fn query_tools_for_skips_listing_when_capability_not_advertised() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
let no_caps = caps(false, false);
|
||||
|
||||
let result = query_tools_for(Some(&no_caps), "srv", || async move {
|
||||
calls_clone.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(vec![test_tool("never")])
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_empty());
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
/// Skips `tools/list` when server info is absent.
|
||||
#[tokio::test]
|
||||
async fn query_tools_for_skips_listing_when_server_info_is_none() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
|
||||
let result = query_tools_for(None, "srv", || async move {
|
||||
calls_clone.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(vec![test_tool("never")])
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_empty());
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
/// Returns listed tools when `tools` is advertised.
|
||||
#[tokio::test]
|
||||
async fn query_tools_for_returns_listed_tools_when_capability_advertised() {
|
||||
let c = caps(true, false);
|
||||
let expected = vec![test_tool("greet"), test_tool("review")];
|
||||
let to_return = expected.clone();
|
||||
|
||||
let result = query_tools_for(Some(&c), "srv", || async move { Ok(to_return) }).await;
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
/// Returns an empty vector when the server lists no tools.
|
||||
#[tokio::test]
|
||||
async fn query_tools_for_returns_empty_vec_when_server_lists_no_tools() {
|
||||
let c = caps(true, false);
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
|
||||
let result = query_tools_for(Some(&c), "srv", || async move {
|
||||
calls_clone.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(Vec::new())
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_empty());
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
/// **The fail-soft test the bug ticket implicitly demands.** Transport-
|
||||
/// closed errors must not abort server startup; the helper must log and
|
||||
/// return an empty vec. This is the regression-protector for #6798's
|
||||
/// underlying asymmetry — if anyone re-introduces a `return Err(...)` here,
|
||||
/// this test fails.
|
||||
#[tokio::test]
|
||||
async fn query_tools_for_returns_empty_on_transport_error() {
|
||||
let c = caps(true, false);
|
||||
let result = query_tools_for(Some(&c), "srv", || async {
|
||||
Err(rmcp::ServiceError::TransportClosed)
|
||||
})
|
||||
.await;
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
/// MCP-protocol errors (e.g. METHOD_NOT_FOUND from a misbehaving server
|
||||
/// that advertised the capability but rejects the call) also fail soft,
|
||||
/// so the rest of the server surface still comes up.
|
||||
#[tokio::test]
|
||||
async fn query_tools_for_returns_empty_on_mcp_error() {
|
||||
let c = caps(true, false);
|
||||
let result = query_tools_for(Some(&c), "srv", || async {
|
||||
Err(rmcp::ServiceError::McpError(ErrorData {
|
||||
code: ErrorCode::METHOD_NOT_FOUND,
|
||||
message: "tools/list not implemented".into(),
|
||||
data: None,
|
||||
}))
|
||||
})
|
||||
.await;
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
/// Calls the `tools/list` function exactly once per query.
|
||||
#[tokio::test]
|
||||
async fn query_tools_for_calls_list_function_exactly_once() {
|
||||
let c = caps(true, false);
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
|
||||
let _ = query_tools_for(Some(&c), "srv", || async move {
|
||||
calls_clone.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(vec![test_tool("p")])
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
/// Keeps the tools-listing decision independent of resource capability state.
|
||||
#[tokio::test]
|
||||
async fn query_tools_for_decision_independent_of_other_capabilities() {
|
||||
let tools = vec![test_tool("x")];
|
||||
for has_tools in [false, true] {
|
||||
for has_resources in [false, true] {
|
||||
let c = caps(has_tools, has_resources);
|
||||
let to_return = tools.clone();
|
||||
let result = query_tools_for(Some(&c), "srv", || async move { Ok(to_return) }).await;
|
||||
|
||||
if has_tools {
|
||||
assert_eq!(result, tools);
|
||||
} else {
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Skips `resources/list` when `resources` is not advertised.
|
||||
#[tokio::test]
|
||||
async fn query_resources_for_skips_listing_when_capability_not_advertised() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
let no_caps = caps(false, false);
|
||||
|
||||
let result = query_resources_for(Some(&no_caps), "srv", || async move {
|
||||
calls_clone.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(vec![test_resource("file:///nope")])
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_empty());
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
/// Skips `resources/list` when server info is absent.
|
||||
#[tokio::test]
|
||||
async fn query_resources_for_skips_listing_when_server_info_is_none() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
|
||||
let result = query_resources_for(None, "srv", || async move {
|
||||
calls_clone.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(vec![test_resource("file:///nope")])
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_empty());
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
/// Returns listed resources when `resources` is advertised.
|
||||
#[tokio::test]
|
||||
async fn query_resources_for_returns_listed_resources_when_capability_advertised() {
|
||||
let c = caps(false, true);
|
||||
let expected = vec![test_resource("file:///a"), test_resource("file:///b")];
|
||||
let to_return = expected.clone();
|
||||
|
||||
let result = query_resources_for(Some(&c), "srv", || async move { Ok(to_return) }).await;
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
/// Fails soft when `resources/list` sees a transport error.
|
||||
#[tokio::test]
|
||||
async fn query_resources_for_returns_empty_on_transport_error() {
|
||||
let c = caps(false, true);
|
||||
let result = query_resources_for(Some(&c), "srv", || async {
|
||||
Err(rmcp::ServiceError::TransportClosed)
|
||||
})
|
||||
.await;
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
/// Fails soft when `resources/list` returns an MCP protocol error.
|
||||
#[tokio::test]
|
||||
async fn query_resources_for_returns_empty_on_mcp_error() {
|
||||
let c = caps(false, true);
|
||||
let result = query_resources_for(Some(&c), "srv", || async {
|
||||
Err(rmcp::ServiceError::McpError(ErrorData {
|
||||
code: ErrorCode::METHOD_NOT_FOUND,
|
||||
message: "resources/list not implemented".into(),
|
||||
data: None,
|
||||
}))
|
||||
})
|
||||
.await;
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
/// Calls the `resources/list` function exactly once per query.
|
||||
#[tokio::test]
|
||||
async fn query_resources_for_calls_list_function_exactly_once() {
|
||||
let c = caps(false, true);
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
|
||||
let _ = query_resources_for(Some(&c), "srv", || async move {
|
||||
calls_clone.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(vec![test_resource("file:///a")])
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
Reference in New Issue
Block a user