use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{DispatchError, Dispatcher};
use crate::application::server::context::{
CallbackExchange, ExchangeError, PeerSession, RequestContext,
};
use crate::application::server::{Server, ToolHandler};
use crate::application::session::{FrameSink, SinkError};
use crate::domain::feature::{Negotiation, resolve};
use crate::domain::ir::{JsonObject, JsonObjectError};
use crate::domain::registry::{AuthContext, Primitive, Registered, Scope, ScopeSet, Tool};
use crate::domain::session::{CancellationToken, SessionId};
use crate::domain::{Capability, CapabilitySet, ProtocolVersion, RequestId, TransportKind};
#[derive(Debug)]
enum Failure {
Message(String),
Fixture(JsonObjectError),
Result(serde_json::Error),
}
impl core::fmt::Display for Failure {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Message(text) => f.write_str(text),
Self::Fixture(error) => write!(f, "bad fixture: {error}"),
Self::Result(error) => write!(f, "unreadable result: {error}"),
}
}
}
impl std::error::Error for Failure {}
impl From<String> for Failure {
fn from(text: String) -> Self {
Self::Message(text)
}
}
impl From<&str> for Failure {
fn from(text: &str) -> Self {
Self::Message(text.to_string())
}
}
impl From<JsonObjectError> for Failure {
fn from(error: JsonObjectError) -> Self {
Self::Fixture(error)
}
}
impl From<DispatchError> for Failure {
fn from(error: DispatchError) -> Self {
Self::Message(error.to_string())
}
}
impl From<serde_json::Error> for Failure {
fn from(error: serde_json::Error) -> Self {
Self::Result(error)
}
}
type TestError = Failure;
fn fail(message: impl Into<String>) -> TestError {
Failure::Message(message.into())
}
struct NoExchange;
impl CallbackExchange for NoExchange {
fn exchange(
&self,
method: &str,
_params: &JsonObject,
) -> impl std::future::Future<Output = Result<JsonObject, ExchangeError>> + Send {
let _ = method;
std::future::ready(Err(ExchangeError::new("no callbacks here")))
}
}
struct NoSink;
impl FrameSink for NoSink {
fn send(
&self,
_frame: &str,
) -> impl std::future::Future<Output = Result<(), SinkError>> + Send {
std::future::ready(Ok(()))
}
}
fn answering(answer: &'static str) -> ToolHandler {
let answer: JsonObject = JsonObject::new(answer).unwrap_or_default();
Arc::new(move |_ctx, _args| {
let answer = answer.clone();
Box::pin(async move { answer })
})
}
fn dispatcher() -> Result<Dispatcher, TestError> {
let restricted = Registered::from(Primitive::from(Tool::new(
"secret",
r#"{"type":"object"}"#,
)?))
.requiring_scope(Scope::Read);
Ok(Dispatcher::new(Arc::new(
Server::builder("test")
.tool(Tool::new("echo", r#"{"type":"object"}"#)?, answering("{}"))
.restricted_tool(restricted, answering("{}"))
.build(),
)))
}
fn peer(version: ProtocolVersion, scopes: ScopeSet, server_caps: CapabilitySet) -> PeerSession {
PeerSession::new(
version,
resolve(Negotiation {
version,
client_caps: CapabilitySet::empty(),
server_caps,
transport: TransportKind::StreamableHttp,
}),
SessionId::new(1),
TransportKind::Stdio,
Arc::new(NoExchange),
Arc::new(NoSink),
)
.with_auth(AuthContext::new("tester", scopes))
}
fn ctx(peer: &PeerSession) -> RequestContext {
RequestContext::for_request(peer, RequestId::text("r1"), CancellationToken::new(), None)
}
fn caps(version: ProtocolVersion) -> CapabilitySet {
CapabilitySet::all_for(version)
}
fn peer_on(
version: ProtocolVersion,
scopes: ScopeSet,
server_caps: CapabilitySet,
id: u64,
) -> PeerSession {
PeerSession::new(
version,
resolve(Negotiation {
version,
client_caps: CapabilitySet::empty(),
server_caps,
transport: TransportKind::StreamableHttp,
}),
SessionId::new(id),
TransportKind::Stdio,
Arc::new(NoExchange),
Arc::new(NoSink),
)
.with_auth(AuthContext::new("tester", scopes))
}
fn charge_tool() -> Result<Tool, TestError> {
Ok(Tool::new("charge", r#"{"type":"object"}"#)?)
}
fn init_params(version: &str) -> Result<JsonObject, TestError> {
Ok(JsonObject::new(
r#"{"protocolVersion":""#.to_string()
+ version
+ r#"","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}"#,
)?)
}
fn init_params_with(version: &str, capabilities: &str) -> Result<JsonObject, TestError> {
Ok(JsonObject::new(
r#"{"protocolVersion":""#.to_string()
+ version
+ r#"","capabilities":"#
+ capabilities
+ r#","clientInfo":{"name":"probe","version":"0"}}"#,
)?)
}
fn parsed(object: &JsonObject) -> Result<serde_json::Value, TestError> {
Ok(serde_json::from_str(object.as_str())?)
}
#[tokio::test]
async fn advertised_capabilities_follow_the_negotiated_revision() -> Result<(), TestError> {
let session_version = ProtocolVersion::V2025_06_18;
let peer = peer(
session_version,
ScopeSet::all(),
CapabilitySet::all_for(session_version),
);
let result = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2024-11-05")?)
.await
.map_err(|error| Failure::Message(format!("the handshake refused: {error}")))?;
let capabilities = parsed(&result)?
.get("capabilities")
.cloned()
.ok_or("the handshake advertised no capabilities")?;
assert!(
capabilities.get("elicitation").is_none(),
"2024-11-05 has no elicitation capability: {capabilities}"
);
Ok(())
}
#[tokio::test]
async fn the_client_capabilities_are_read_not_assumed() -> Result<(), TestError> {
let session_version = ProtocolVersion::V2025_11_25;
let peer = peer(
session_version,
ScopeSet::all(),
CapabilitySet::all_for(session_version),
);
dispatcher()?
.dispatch(
&ctx(&peer),
"initialize",
&init_params_with("2025-11-25", r#"{"roots":{"listChanged":true}}"#)?,
)
.await
.map_err(|error| Failure::Message(format!("the handshake refused: {error}")))?;
Ok(())
}
#[tokio::test]
async fn initialize_reports_the_negotiated_revision() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let result = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2024-11-05")?)
.await
.map_err(|error| Failure::Message(format!("the handshake refused: {error}")))?;
let reported = parsed(&result)?
.get("protocolVersion")
.and_then(serde_json::Value::as_str)
.ok_or("the handshake did not report a revision")?
.to_string();
assert_eq!(reported, "2024-11-05");
Ok(())
}
#[tokio::test]
async fn a_client_newer_than_this_build_gets_the_newest_handshake() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let result = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2099-01-01")?)
.await
.map_err(|error| Failure::Message(format!("a newer client was refused: {error}")))?;
let reported = parsed(&result)?
.get("protocolVersion")
.and_then(serde_json::Value::as_str)
.ok_or("the handshake did not report a revision")?
.to_string();
assert_eq!(
reported,
ProtocolVersion::V2025_11_25.to_string(),
"the newest revision this build can actually complete a handshake on"
);
Ok(())
}
#[tokio::test]
async fn the_handshake_advertises_only_server_capabilities() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let result = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2025-06-18")?)
.await
.map_err(|error| Failure::Message(format!("the handshake refused: {error}")))?;
let capabilities = parsed(&result)?
.get("capabilities")
.cloned()
.ok_or("the handshake advertised no capabilities")?;
for forbidden in ["roots", "sampling", "elicitation"] {
assert!(
capabilities.get(forbidden).is_none(),
"a client-side capability reached the server advertisement: {forbidden}"
);
}
assert!(
capabilities.get("tools").is_some(),
"a session that negotiated tools did not advertise them"
);
Ok(())
}
#[tokio::test]
async fn initialize_answers_on_a_session_with_no_features() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::empty());
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2025-06-18")?)
.await;
assert!(outcome.is_ok(), "the handshake was gated off: {outcome:?}");
Ok(())
}
#[tokio::test]
async fn a_malformed_handshake_is_refused_whatever_is_missing() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let bodies = [
"{}",
r#"{"capabilities":{},"clientInfo":{"name":"p","version":"0"}}"#,
r#"{"protocolVersion":"2025-06-18","clientInfo":{"name":"p","version":"0"}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":{}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":[],"clientInfo":{"name":"p"}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{}}"#,
r#"{"protocolVersion":"nonsense","capabilities":{},"clientInfo":{"name":"p"}}"#,
];
for body in bodies {
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &JsonObject::new(body)?)
.await;
assert_eq!(
outcome,
Err(DispatchError::MalformedInitialize),
"this body was accepted: {body}"
);
}
Ok(())
}
#[tokio::test]
async fn a_session_without_the_handshake_refuses_the_method() -> Result<(), TestError> {
let version = ProtocolVersion::V2026_07_28;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2026-07-28")?)
.await;
assert_eq!(outcome, Err(DispatchError::UnknownMethod));
Ok(())
}
#[tokio::test]
async fn an_undefined_method_is_refused() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "tools/frobnicate", &JsonObject::default())
.await;
assert_eq!(outcome, Err(DispatchError::UnknownMethod));
Ok(())
}
#[tokio::test]
async fn a_call_with_no_name_is_malformed_rather_than_unknown() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let params = JsonObject::new(r#"{"arguments":{}}"#)?;
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await;
assert_eq!(outcome, Err(DispatchError::MalformedParams));
Ok(())
}
#[tokio::test]
async fn a_call_without_arguments_still_reaches_the_handler() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let params = JsonObject::new(r#"{"name":"echo"}"#)?;
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await;
assert_eq!(
outcome.map(|answer| answer.as_str().to_string()),
Ok("{}".to_string())
);
Ok(())
}
#[tokio::test]
async fn a_method_the_session_lacks_is_refused() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::empty());
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "tools/list", &JsonObject::default())
.await;
assert_eq!(outcome, Err(DispatchError::NotCallable));
Ok(())
}
#[tokio::test]
async fn discover_answers_only_where_sep_2575_defines_it() -> Result<(), TestError> {
let modern = ProtocolVersion::V2026_07_28;
let answered = dispatcher()?
.dispatch(
&ctx(&peer(modern, ScopeSet::empty(), caps(modern))),
"server/discover",
&JsonObject::default(),
)
.await
.map_err(|error| Failure::from(error.to_string()))?;
let parsed: serde_json::Value = serde_json::from_str(answered.as_str())
.map_err(|error| Failure::from(error.to_string()))?;
for member in [
"resultType",
"supportedVersions",
"ttlMs",
"cacheScope",
"capabilities",
] {
assert!(
parsed.get(member).is_some(),
"DiscoverResult is missing required member `{member}`",
);
}
assert_eq!(
parsed.get("resultType").and_then(serde_json::Value::as_str),
Some("complete")
);
let older = ProtocolVersion::V2025_11_25;
let refused = dispatcher()?
.dispatch(
&ctx(&peer(older, ScopeSet::empty(), caps(older))),
"server/discover",
&JsonObject::default(),
)
.await;
assert_eq!(
refused.map(|answer| answer.as_str().to_string()),
Err(DispatchError::UnknownMethod),
"SEP-2575 defines server/discover only from 2026-07-28",
);
Ok(())
}
#[tokio::test]
async fn discover_advertises_the_profile_the_server_honours() -> Result<(), TestError> {
let version = ProtocolVersion::V2026_07_28;
let server_caps = CapabilitySet::empty().with(Capability::Tools);
let peer = peer(version, ScopeSet::empty(), server_caps);
let answered = dispatcher()?
.dispatch(&ctx(&peer), "server/discover", &JsonObject::default())
.await
.map_err(|error| Failure::from(error.to_string()))?;
let parsed: serde_json::Value = serde_json::from_str(answered.as_str())
.map_err(|error| Failure::from(error.to_string()))?;
let capabilities = parsed
.get("capabilities")
.ok_or("the result carried no capabilities")?;
assert!(
capabilities.get("tools").is_some(),
"tools was negotiated and not advertised: {capabilities}",
);
assert!(
capabilities.get("prompts").is_none(),
"prompts was not negotiated and was advertised anyway: {capabilities}",
);
Ok(())
}
#[tokio::test]
async fn the_result_envelope_is_stamped_only_from_2026_07_28() -> Result<(), TestError> {
let modern = ProtocolVersion::V2026_07_28;
let listed = dispatcher()?
.dispatch(
&ctx(&peer(modern, ScopeSet::empty(), caps(modern))),
"tools/list",
&JsonObject::default(),
)
.await
.map_err(|error| Failure::from(error.to_string()))?;
let listed: serde_json::Value =
serde_json::from_str(listed.as_str()).map_err(|error| Failure::from(error.to_string()))?;
for member in ["resultType", "ttlMs", "cacheScope"] {
assert!(
listed.get(member).is_some(),
"a cacheable result is missing `{member}`: {listed}",
);
}
let older = ProtocolVersion::V2025_11_25;
let legacy = dispatcher()?
.dispatch(
&ctx(&peer(older, ScopeSet::empty(), caps(older))),
"tools/list",
&JsonObject::default(),
)
.await
.map_err(|error| Failure::from(error.to_string()))?;
let legacy: serde_json::Value =
serde_json::from_str(legacy.as_str()).map_err(|error| Failure::from(error.to_string()))?;
for member in ["resultType", "ttlMs", "cacheScope"] {
assert!(
legacy.get(member).is_none(),
"`{member}` is not defined before 2026-07-28 and was stamped anyway: {legacy}",
);
}
Ok(())
}
#[tokio::test]
async fn ping_answers_with_no_features_at_all() -> Result<(), TestError> {
let version = ProtocolVersion::V2024_11_05;
let peer = peer(version, ScopeSet::empty(), CapabilitySet::empty());
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "ping", &JsonObject::default())
.await;
assert_eq!(
outcome.map(|answer| answer.as_str().to_string()),
Ok("{}".to_string())
);
Ok(())
}
#[tokio::test]
async fn the_listing_hides_a_tool_the_caller_may_not_see() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::empty(), CapabilitySet::all_for(version));
let listed = dispatcher()?
.dispatch(&ctx(&peer), "tools/list", &JsonObject::default())
.await
.map_err(|error| Failure::from(error.to_string()))?;
let parsed: serde_json::Value =
serde_json::from_str(listed.as_str()).map_err(|error| Failure::from(error.to_string()))?;
let names: Vec<&str> = parsed
.get("tools")
.and_then(serde_json::Value::as_array)
.ok_or("the listing has no tools array")?
.iter()
.filter_map(|tool| tool.get("name"))
.filter_map(serde_json::Value::as_str)
.collect();
assert_eq!(names, ["echo"]);
Ok(())
}
#[tokio::test]
async fn the_listing_shows_the_tool_to_a_caller_holding_its_scope() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let listed = dispatcher()?
.dispatch(&ctx(&peer), "tools/list", &JsonObject::default())
.await
.map_err(|error| Failure::from(error.to_string()))?;
assert!(
listed.as_str().contains("secret"),
"an unrestricted caller could not see the restricted tool: {}",
listed.as_str()
);
Ok(())
}
fn counting(count: &Arc<AtomicUsize>, answer: &'static str) -> Result<Dispatcher, TestError> {
let answer = JsonObject::new(answer).unwrap_or_default();
let seen = Arc::clone(count);
let handler: ToolHandler = Arc::new(move |_ctx, _args| {
let count = Arc::clone(&seen);
let answer = answer.clone();
Box::pin(async move {
count.fetch_add(1, Ordering::SeqCst);
answer
})
});
Ok(Dispatcher::new(Arc::new(
Server::builder("test")
.tool(charge_tool()?, handler)
.build(),
)))
}
#[tokio::test]
async fn a_retry_after_a_dropped_connection_runs_the_tool_once() -> Result<(), TestError> {
let count = Arc::new(AtomicUsize::new(0));
let dispatcher = counting(&count, r#"{"charged":true}"#)?;
let peer = peer(
ProtocolVersion::V2025_06_18,
ScopeSet::all(),
caps(ProtocolVersion::V2025_06_18),
);
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
let first = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await?;
let second = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await?;
if count.load(Ordering::SeqCst) != 1 {
return Err(fail(format!(
"the tool ran {} times",
count.load(Ordering::SeqCst)
)));
}
if first != second {
return Err(fail("the retry was not answered from the recording"));
}
Ok(())
}
#[tokio::test]
async fn the_same_request_id_on_another_session_is_not_a_retry() -> Result<(), TestError> {
let count = Arc::new(AtomicUsize::new(0));
let dispatcher = counting(&count, r#"{"charged":true}"#)?;
let version = ProtocolVersion::V2025_06_18;
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
let first_peer = peer(version, ScopeSet::all(), caps(version));
let second_peer = peer_on(version, ScopeSet::all(), caps(version), 2);
let _ = dispatcher
.dispatch(&ctx(&first_peer), "tools/call", ¶ms)
.await?;
let _ = dispatcher
.dispatch(&ctx(&second_peer), "tools/call", ¶ms)
.await?;
if count.load(Ordering::SeqCst) != 2 {
return Err(fail(format!(
"two sessions ran the tool {} time(s); the second was treated as a retry",
count.load(Ordering::SeqCst)
)));
}
Ok(())
}
#[derive(Debug, Default)]
struct Journal {
log: std::sync::Mutex<Vec<&'static str>>,
}
impl Journal {
fn entries(&self) -> Vec<&'static str> {
self.log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
}
fn room_for(keys: usize) -> NonZeroUsize {
NonZeroUsize::new(keys).unwrap_or(NonZeroUsize::MIN)
}
struct Recording {
inner: crate::adapters::secondary::idempotency::InMemoryIdempotencyStore<JsonObject>,
log: Arc<Journal>,
}
impl crate::application::idempotency::IdempotencyStore for Recording {
type Result = JsonObject;
fn claim(
&self,
key: &crate::domain::idempotency::IdempotencyKey,
) -> Result<
crate::domain::idempotency::Claim<Self::Result>,
crate::domain::idempotency::IdempotencyError,
> {
self.log
.log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push("claim");
self.inner.claim(key)
}
fn complete(
&self,
token: &crate::domain::idempotency::ClaimToken,
result: Self::Result,
) -> Result<(), crate::domain::idempotency::IdempotencyError> {
self.log
.log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push("complete");
self.inner.complete(token, result)
}
fn abandon(
&self,
token: &crate::domain::idempotency::ClaimToken,
) -> Result<(), crate::domain::idempotency::IdempotencyError> {
self.log
.log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push("abandon");
self.inner.abandon(token)
}
}
#[tokio::test]
async fn a_successful_call_claims_then_records() -> Result<(), TestError> {
let journal = Arc::new(Journal::default());
let store = Arc::new(Recording {
inner: crate::adapters::secondary::idempotency::InMemoryIdempotencyStore::with_capacity(
room_for(8),
),
log: Arc::clone(&journal),
});
let count = Arc::new(AtomicUsize::new(0));
let seen = Arc::clone(&count);
let handler: ToolHandler = Arc::new(move |_ctx, _args| {
let count = Arc::clone(&seen);
Box::pin(async move {
count.fetch_add(1, Ordering::SeqCst);
JsonObject::new(r#"{"charged":true}"#).unwrap_or_default()
})
});
let dispatcher = Dispatcher::new(Arc::new(
Server::builder("test")
.tool(charge_tool()?, handler)
.idempotency(store)
.build(),
));
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), caps(version));
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
let _ = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await?;
let _ = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await?;
if journal.entries() != vec!["claim", "complete", "claim"] {
return Err(fail(format!("the store saw {:?}", journal.entries())));
}
if count.load(Ordering::SeqCst) != 1 {
return Err(fail(format!(
"the tool ran {} times",
count.load(Ordering::SeqCst)
)));
}
Ok(())
}
#[tokio::test]
async fn a_refused_call_releases_the_key() -> Result<(), TestError> {
let journal = Arc::new(Journal::default());
let store = Arc::new(Recording {
inner: crate::adapters::secondary::idempotency::InMemoryIdempotencyStore::with_capacity(
room_for(8),
),
log: Arc::clone(&journal),
});
let secret = Registered::from(Primitive::from(Tool::new(
"secret",
r#"{"type":"object"}"#,
)?))
.requiring_scope(Scope::Read);
let handler: ToolHandler = Arc::new(|_ctx, _args| {
Box::pin(async { JsonObject::new(r#"{"leaked":true}"#).unwrap_or_default() })
});
let dispatcher = Dispatcher::new(Arc::new(
Server::builder("test")
.restricted_tool(secret, handler)
.idempotency(store)
.build(),
));
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::empty(), caps(version));
let params = JsonObject::new(r#"{"name":"secret"}"#)?;
for _ in 0..2 {
let outcome = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await;
if outcome != Err(DispatchError::UnknownTool) {
return Err(fail(format!("a hidden tool answered {outcome:?}")));
}
}
if journal.entries() != vec!["claim", "abandon", "claim", "abandon"] {
return Err(fail(format!("the store saw {:?}", journal.entries())));
}
Ok(())
}
#[tokio::test]
async fn a_duplicate_while_the_first_is_running_is_refused() -> Result<(), TestError> {
let count = Arc::new(AtomicUsize::new(0));
let (gate_tx, gate_rx) = tokio::sync::oneshot::channel::<()>();
let gate_rx = Arc::new(tokio::sync::Mutex::new(Some(gate_rx)));
let seen = Arc::clone(&count);
let handler: ToolHandler = Arc::new(move |_ctx, _args| {
let count = Arc::clone(&seen);
let gate_rx = Arc::clone(&gate_rx);
Box::pin(async move {
let waiting = { gate_rx.lock().await.take() };
if let Some(receiver) = waiting {
let _ = receiver.await;
}
count.fetch_add(1, Ordering::SeqCst);
JsonObject::new(r#"{"charged":true}"#).unwrap_or_default()
})
});
let dispatcher = Dispatcher::new(Arc::new(
Server::builder("test")
.tool(charge_tool()?, handler)
.build(),
));
let peer = peer(
ProtocolVersion::V2025_06_18,
ScopeSet::all(),
caps(ProtocolVersion::V2025_06_18),
);
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
let running = tokio::spawn({
let dispatcher = Dispatcher::new(Arc::clone(dispatcher.server()));
let peer = peer_on(
ProtocolVersion::V2025_06_18,
ScopeSet::all(),
caps(ProtocolVersion::V2025_06_18),
1,
);
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
async move {
dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await
}
});
tokio::task::yield_now().await;
let duplicate = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await;
let _ = gate_tx.send(());
let _ = running.await;
match duplicate {
Err(DispatchError::DuplicateInFlight(_)) => {}
other => return Err(fail(format!("the duplicate answered {other:?}"))),
}
if count.load(Ordering::SeqCst) != 1 {
return Err(fail(format!(
"the tool ran {} time(s) while a duplicate was refused",
count.load(Ordering::SeqCst)
)));
}
Ok(())
}