Skip to content

Commit 3a913c7

Browse files
committed
tests: add a new integration test for list_changed notification
Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com>
1 parent 3a65f83 commit 3a913c7

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed

tests/transport_integration_test.rs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,240 @@ async fn test_stdio_transport() -> Result<()> {
535535
Ok(())
536536
}
537537

538+
#[test(tokio::test)]
539+
async fn test_tool_list_notification() -> Result<()> {
540+
// Create a temporary directory for this test to avoid loading existing components
541+
let temp_dir = tempfile::tempdir()?;
542+
let plugin_dir_arg = format!("--plugin-dir={}", temp_dir.path().display());
543+
544+
// Get the path to the built binary
545+
let binary_path = std::env::current_dir()
546+
.context("Failed to get current directory")?
547+
.join("target/debug/wassette");
548+
549+
// Start the server with stdio transport (disable logs to avoid stdout pollution)
550+
let mut child = tokio::process::Command::new(&binary_path)
551+
.args(["serve", &plugin_dir_arg])
552+
.env("RUST_LOG", "off")
553+
.stdin(Stdio::piped())
554+
.stdout(Stdio::piped())
555+
.stderr(Stdio::piped())
556+
.spawn()
557+
.context("Failed to start wassette with stdio transport")?;
558+
559+
let stdin = child.stdin.take().context("Failed to get stdin handle")?;
560+
let stdout = child.stdout.take().context("Failed to get stdout handle")?;
561+
let stderr = child.stderr.take().context("Failed to get stderr handle")?;
562+
563+
let mut stdin = stdin;
564+
let mut stdout = BufReader::new(stdout);
565+
let mut stderr = BufReader::new(stderr);
566+
567+
// Give the server time to start
568+
tokio::time::sleep(Duration::from_millis(1000)).await;
569+
570+
// Check if the process is still running
571+
if let Ok(Some(status)) = child.try_wait() {
572+
let mut stderr_output = String::new();
573+
let _ = stderr.read_line(&mut stderr_output).await;
574+
return Err(anyhow::anyhow!(
575+
"Server process exited with status: {:?}, stderr: {}",
576+
status,
577+
stderr_output
578+
));
579+
}
580+
581+
// Send MCP initialize request
582+
let initialize_request = r#"{"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test-client", "version": "1.0.0"}}, "id": 1}
583+
"#;
584+
585+
stdin.write_all(initialize_request.as_bytes()).await?;
586+
stdin.flush().await?;
587+
588+
// Read and verify initialize response
589+
let mut response_line = String::new();
590+
match tokio::time::timeout(
591+
Duration::from_secs(10),
592+
stdout.read_line(&mut response_line),
593+
)
594+
.await
595+
{
596+
Ok(Ok(_)) => {}
597+
Ok(Err(e)) => {
598+
return Err(anyhow::anyhow!("Failed to read initialize response: {}", e));
599+
}
600+
Err(_) => {
601+
let mut stderr_output = String::new();
602+
let _ =
603+
tokio::time::timeout(Duration::from_secs(1), stderr.read_line(&mut stderr_output))
604+
.await;
605+
return Err(anyhow::anyhow!(
606+
"Timeout waiting for initialize response. Stderr: {}",
607+
stderr_output
608+
));
609+
}
610+
}
611+
612+
let response: serde_json::Value =
613+
serde_json::from_str(&response_line).context("Failed to parse initialize response")?;
614+
615+
assert_eq!(response["jsonrpc"], "2.0");
616+
assert_eq!(response["id"], 1);
617+
assert!(response["result"].is_object());
618+
619+
// Send initialized notification (required by MCP protocol)
620+
let initialized_notification = r#"{"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}
621+
"#;
622+
623+
stdin.write_all(initialized_notification.as_bytes()).await?;
624+
stdin.flush().await?;
625+
626+
// Step 1: Send initial list_tools request to get baseline tool count
627+
let list_tools_request = r#"{"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2}
628+
"#;
629+
630+
stdin.write_all(list_tools_request.as_bytes()).await?;
631+
stdin.flush().await?;
632+
633+
// Read initial tools list response
634+
let mut tools_response_line = String::new();
635+
tokio::time::timeout(
636+
Duration::from_secs(10),
637+
stdout.read_line(&mut tools_response_line),
638+
)
639+
.await
640+
.context("Timeout waiting for initial tools/list response")?
641+
.context("Failed to read initial tools/list response")?;
642+
643+
let initial_tools_response: serde_json::Value = serde_json::from_str(&tools_response_line)
644+
.context("Failed to parse initial tools/list response")?;
645+
646+
assert_eq!(initial_tools_response["jsonrpc"], "2.0");
647+
assert_eq!(initial_tools_response["id"], 2);
648+
assert!(initial_tools_response["result"].is_object());
649+
assert!(initial_tools_response["result"]["tools"].is_array());
650+
651+
let initial_tools = &initial_tools_response["result"]["tools"]
652+
.as_array()
653+
.unwrap();
654+
let initial_tool_count = initial_tools.len();
655+
println!("Initial tool count: {}", initial_tool_count);
656+
657+
// Build a component to load
658+
let component_path = build_fetch_component().await?;
659+
660+
// Step 2: Load a component using the load-component tool
661+
let load_component_request = format!(
662+
r#"{{"jsonrpc": "2.0", "method": "tools/call", "params": {{"name": "load-component", "arguments": {{"path": "file://{}"}}}}, "id": 3}}
663+
"#,
664+
component_path.to_str().unwrap()
665+
);
666+
667+
stdin.write_all(load_component_request.as_bytes()).await?;
668+
stdin.flush().await?;
669+
670+
// Read the tool list change notification first (this is what we're testing!)
671+
let mut notification_line = String::new();
672+
tokio::time::timeout(
673+
Duration::from_secs(15),
674+
stdout.read_line(&mut notification_line),
675+
)
676+
.await
677+
.context("Timeout waiting for tool list change notification")?
678+
.context("Failed to read tool list change notification")?;
679+
680+
let notification: serde_json::Value = serde_json::from_str(&notification_line)
681+
.context("Failed to parse tool list change notification")?;
682+
683+
// Verify we received a tools/list_changed notification
684+
assert_eq!(notification["jsonrpc"], "2.0");
685+
assert_eq!(notification["method"], "notifications/tools/list_changed");
686+
println!("✓ Received tools/list_changed notification as expected");
687+
688+
// Read the actual load-component response
689+
let mut load_response_line = String::new();
690+
tokio::time::timeout(
691+
Duration::from_secs(15),
692+
stdout.read_line(&mut load_response_line),
693+
)
694+
.await
695+
.context("Timeout waiting for load-component response")?
696+
.context("Failed to read load-component response")?;
697+
698+
let load_response: serde_json::Value = serde_json::from_str(&load_response_line)
699+
.context("Failed to parse load-component response")?;
700+
701+
assert_eq!(load_response["jsonrpc"], "2.0");
702+
assert_eq!(load_response["id"], 3);
703+
704+
// Check if the load succeeded
705+
if load_response["error"].is_object() {
706+
panic!("Failed to load component: {}", load_response["error"]);
707+
}
708+
assert!(load_response["result"].is_object());
709+
println!("✓ Component loaded successfully");
710+
711+
// Step 3: Send another list_tools request to verify tools were added
712+
let list_tools_request_after = r#"{"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 4}
713+
"#;
714+
715+
stdin.write_all(list_tools_request_after.as_bytes()).await?;
716+
stdin.flush().await?;
717+
718+
// Read updated tools list response
719+
let mut updated_tools_response_line = String::new();
720+
tokio::time::timeout(
721+
Duration::from_secs(10),
722+
stdout.read_line(&mut updated_tools_response_line),
723+
)
724+
.await
725+
.context("Timeout waiting for updated tools/list response")?
726+
.context("Failed to read updated tools/list response")?;
727+
728+
let updated_tools_response: serde_json::Value =
729+
serde_json::from_str(&updated_tools_response_line)
730+
.context("Failed to parse updated tools/list response")?;
731+
732+
assert_eq!(updated_tools_response["jsonrpc"], "2.0");
733+
assert_eq!(updated_tools_response["id"], 4);
734+
assert!(updated_tools_response["result"].is_object());
735+
assert!(updated_tools_response["result"]["tools"].is_array());
736+
737+
let updated_tools = &updated_tools_response["result"]["tools"]
738+
.as_array()
739+
.unwrap();
740+
let updated_tool_count = updated_tools.len();
741+
println!("Updated tool count: {}", updated_tool_count);
742+
743+
// Verify that the tool count increased after loading the component
744+
assert!(
745+
updated_tool_count > initial_tool_count,
746+
"Tool count should have increased from {} to {}, but it didn't",
747+
initial_tool_count,
748+
updated_tool_count
749+
);
750+
println!("✓ Tool count increased as expected after loading component");
751+
752+
// Verify that the new tools from the component are present
753+
let updated_tool_names: Vec<String> = updated_tools
754+
.iter()
755+
.map(|tool| tool["name"].as_str().unwrap_or("").to_string())
756+
.collect();
757+
758+
// The fetch component should add a "fetch" tool
759+
assert!(
760+
updated_tool_names.contains(&"fetch".to_string()),
761+
"Expected 'fetch' tool from loaded component, but found tools: {:?}",
762+
updated_tool_names
763+
);
764+
println!("✓ New tools from loaded component are present in the list");
765+
766+
// Clean up
767+
child.kill().await.ok();
768+
769+
Ok(())
770+
}
771+
538772
#[test(tokio::test)]
539773
async fn test_http_transport() -> Result<()> {
540774
// Use a random available port to avoid conflicts

0 commit comments

Comments
 (0)