Skip to content

Commit d36ce25

Browse files
committed
test(sep-2567): pin the replacement handshake and its teardown
Records the shutdown DELETE instead of parsing its empty body as JSON, so the teardown is assertable rather than a panic in the server task.
1 parent 7566a73 commit d36ce25

1 file changed

Lines changed: 36 additions & 2 deletions

File tree

‎crates/rmcp/tests/test_streamable_http_sessionless_version.rs‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,21 @@ impl Recorder {
9898
.collect()
9999
}
100100

101+
fn delete_requests(&self) -> Vec<Call> {
102+
self.calls()
103+
.into_iter()
104+
.filter(|(http_method, ..)| http_method == "DELETE")
105+
.collect()
106+
}
107+
108+
/// Session headers on everything after the handshake that is not the teardown DELETE,
109+
/// which carries the id on purpose so the server tears the session down.
101110
fn session_headers_after_handshake(&self) -> Vec<Option<String>> {
102111
self.calls()
103112
.into_iter()
104-
.filter(|(_, jsonrpc_method, _)| jsonrpc_method != "initialize")
113+
.filter(|(http_method, jsonrpc_method, _)| {
114+
jsonrpc_method != "initialize" && http_method != "DELETE"
115+
})
105116
.map(|(.., session)| session)
106117
.collect()
107118
}
@@ -144,6 +155,19 @@ async fn handler(
144155
.expect("build GET rejection");
145156
}
146157

158+
// The shutdown DELETE carries no body, so record it before anything parses one.
159+
if method == axum::http::Method::DELETE {
160+
state.seen.lock().expect("recorder poisoned").push((
161+
"DELETE".to_owned(),
162+
"-".to_owned(),
163+
session,
164+
));
165+
return Response::builder()
166+
.status(StatusCode::OK)
167+
.body(Body::empty())
168+
.expect("build session teardown response");
169+
}
170+
147171
let request: serde_json::Value = serde_json::from_slice(&body).expect("valid JSON-RPC body");
148172
let jsonrpc_method = request["method"].as_str().unwrap_or("-").to_owned();
149173
state.seen.lock().expect("recorder poisoned").push((
@@ -347,7 +371,7 @@ async fn replacement_handshake_at_a_modern_version_drops_the_session_too() {
347371
// Nor on anything after it, and no stream on the replacement session either.
348372
let echoed: Vec<_> = replacement
349373
.iter()
350-
.filter(|(.., session)| session.is_some())
374+
.filter(|(http_method, _, session)| session.is_some() && http_method != "DELETE")
351375
.collect();
352376
assert!(
353377
echoed.is_empty(),
@@ -359,6 +383,16 @@ async fn replacement_handshake_at_a_modern_version_drops_the_session_too() {
359383
"expected only the legacy session's GET stream, got {:?}",
360384
recorder.get_requests()
361385
);
386+
387+
// Dropping the id is a request-and-stream decision, not a licence to leak server
388+
// state: the session the server really did create is still torn down at shutdown.
389+
let deletes = recorder.delete_requests();
390+
assert!(
391+
deletes
392+
.iter()
393+
.any(|(.., session)| session.as_deref() == Some(REPLACEMENT_SESSION_ID)),
394+
"the replacement session was never deleted at shutdown: {deletes:?}"
395+
);
362396
}
363397

364398
#[tokio::test]

0 commit comments

Comments
 (0)