huskies: merge 905
This commit is contained in:
@@ -927,3 +927,176 @@ enabled = false
|
||||
let config = BotConfig::load(tmp.path());
|
||||
assert!(config.is_none());
|
||||
}
|
||||
|
||||
// ── Gateway MCP SSE proxy integration tests ──────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_mcp_sse_proxy_streams_progress_and_final_response() {
|
||||
let mut mock_sled = mockito::Server::new_async().await;
|
||||
|
||||
let prog1 = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/progress",
|
||||
"params": { "progressToken": "tok1", "progress": 1.0 }
|
||||
});
|
||||
let prog2 = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/progress",
|
||||
"params": { "progressToken": "tok1", "progress": 2.0 }
|
||||
});
|
||||
let final_resp = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": { "content": [{ "type": "text", "text": "tests passed" }] }
|
||||
});
|
||||
let sse_body = format!("data: {prog1}\n\ndata: {prog2}\n\ndata: {final_resp}\n\n");
|
||||
|
||||
let _mock = mock_sled
|
||||
.mock("POST", "/mcp")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "text/event-stream")
|
||||
.with_body(&sse_body)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let mut projects = BTreeMap::new();
|
||||
projects.insert(
|
||||
"sled".to_string(),
|
||||
ProjectEntry {
|
||||
url: mock_sled.url(),
|
||||
},
|
||||
);
|
||||
let config = GatewayConfig { projects };
|
||||
let state = Arc::new(GatewayState::new(config, PathBuf::new(), 3000).unwrap());
|
||||
|
||||
let app = poem::Route::new()
|
||||
.at("/mcp", poem::post(gateway_mcp_post_handler))
|
||||
.data(state.clone());
|
||||
let cli = poem::test::TestClient::new(app);
|
||||
|
||||
let rpc_body = serde_json::to_vec(&serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "run_tests",
|
||||
"arguments": {},
|
||||
"_meta": { "progressToken": "tok1" }
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let resp = cli
|
||||
.post("/mcp")
|
||||
.header("content-type", "application/json")
|
||||
.header("accept", "text/event-stream")
|
||||
.body(rpc_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let body = resp.0.into_body().into_string().await.unwrap();
|
||||
|
||||
let data_lines: Vec<&str> = body
|
||||
.lines()
|
||||
.filter_map(|l| l.strip_prefix("data: "))
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
data_lines.len(),
|
||||
3,
|
||||
"Expected 3 SSE events (2 progress + 1 final); got {}: {:?}",
|
||||
data_lines.len(),
|
||||
body
|
||||
);
|
||||
|
||||
let ev1: serde_json::Value =
|
||||
serde_json::from_str(data_lines[0]).expect("event 1 is valid JSON");
|
||||
assert_eq!(
|
||||
ev1["method"], "notifications/progress",
|
||||
"event 1 must be a progress notification"
|
||||
);
|
||||
assert_eq!(ev1["params"]["progress"], 1.0);
|
||||
|
||||
let ev2: serde_json::Value =
|
||||
serde_json::from_str(data_lines[1]).expect("event 2 is valid JSON");
|
||||
assert_eq!(
|
||||
ev2["method"], "notifications/progress",
|
||||
"event 2 must be a progress notification"
|
||||
);
|
||||
assert_eq!(ev2["params"]["progress"], 2.0);
|
||||
|
||||
let ev3: serde_json::Value =
|
||||
serde_json::from_str(data_lines[2]).expect("event 3 is valid JSON");
|
||||
assert_eq!(ev3["id"], 1, "event 3 must be the final JSON-RPC response");
|
||||
assert!(
|
||||
ev3.get("result").is_some(),
|
||||
"event 3 must carry a result field"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_mcp_post_without_sse_returns_plain_json() {
|
||||
let mut mock_sled = mockito::Server::new_async().await;
|
||||
|
||||
let json_resp = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": { "content": [{ "type": "text", "text": "done" }] }
|
||||
});
|
||||
|
||||
let _mock = mock_sled
|
||||
.mock("POST", "/mcp")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(serde_json::to_string(&json_resp).unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let mut projects = BTreeMap::new();
|
||||
projects.insert(
|
||||
"sled".to_string(),
|
||||
ProjectEntry {
|
||||
url: mock_sled.url(),
|
||||
},
|
||||
);
|
||||
let config = GatewayConfig { projects };
|
||||
let state = Arc::new(GatewayState::new(config, PathBuf::new(), 3000).unwrap());
|
||||
|
||||
let app = poem::Route::new()
|
||||
.at("/mcp", poem::post(gateway_mcp_post_handler))
|
||||
.data(state.clone());
|
||||
let cli = poem::test::TestClient::new(app);
|
||||
|
||||
let rpc_body = serde_json::to_vec(&serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": { "name": "run_tests", "arguments": {} }
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let resp = cli
|
||||
.post("/mcp")
|
||||
.header("content-type", "application/json")
|
||||
.body(rpc_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let ct = resp
|
||||
.0
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
assert!(
|
||||
ct.contains("application/json"),
|
||||
"Non-SSE path must return application/json; got: {ct}"
|
||||
);
|
||||
|
||||
let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
|
||||
assert_eq!(body["id"], 2);
|
||||
assert!(
|
||||
body.get("result").is_some(),
|
||||
"Expected result in plain JSON response"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user