#![cfg(unix)]
mod harness;
use std::process::Stdio;
use std::time::Duration;
use harness::{MCPMESH, STUB, run_cmd, shutdown_daemon, world};
use mcpmesh_net::framing::write_frame;
use serde_json::{Value, json};
use tokio::time::timeout;
#[tokio::test(flavor = "multi_thread")]
async fn cold_daemon_dials_a_paired_peer_from_the_persisted_address() {
timeout(Duration::from_secs(120), async {
let alice_dir = tempfile::tempdir().unwrap();
let bob_dir = tempfile::tempdir().unwrap();
let (alice_socket, alice_env) = world(
alice_dir.path(),
&format!(
"[identity]\nnickname = \"alice\"\n\n[network]\nrelay_mode = \"disabled\"\n\n\
[services.echo]\nrun = ['{STUB}']\nallow = []\n"
),
);
let (bob_socket, bob_env) = world(
bob_dir.path(),
"[identity]\nnickname = \"bob\"\n\n[network]\nrelay_mode = \"disabled\"\n",
);
let out = run_cmd(&alice_env, &["invite", "echo"]);
assert!(
out.status.success(),
"`mcpmesh invite echo` exit 0; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let invite_line = stdout
.split_whitespace()
.find(|t| t.starts_with("mcpmesh-invite:"))
.unwrap_or_else(|| panic!("no invite line in:\n{stdout}"))
.to_string();
let out = run_cmd(&bob_env, &["pair", &invite_line]);
assert!(
out.status.success(),
"`mcpmesh pair <invite>` exit 0; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
shutdown_daemon(&bob_socket).await;
let mut child = tokio::process::Command::new(MCPMESH)
.arg("connect")
.arg("alice/echo")
.envs(bob_env.iter().map(|(k, v)| (k.clone(), v.clone())))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn one-shot mcpmesh connect");
let mut child_in = child.stdin.take().expect("piped stdin");
write_frame(
&mut child_in,
&json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {},
"clientInfo": {"name": "cold-dial", "version": "0"}}
}),
)
.await
.unwrap();
drop(child_in);
let out = timeout(Duration::from_secs(60), child.wait_with_output())
.await
.expect("cold-dial connect did not finish within 60s")
.expect("collect cold-dial connect output");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
out.status.success(),
"cold-dial connect exit 0; stdout: {stdout}; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let frames: Vec<Value> = stdout
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| serde_json::from_str(l).unwrap_or_else(|e| panic!("non-JSON line {l:?}: {e}")))
.collect();
assert_eq!(
frames.len(),
1,
"the one-shot pipe carries exactly the one response frame:\n{stdout}"
);
assert_eq!(
frames[0]["id"], 1,
"the response answers our request id: {}",
frames[0]
);
assert_eq!(
frames[0]["result"]["serverInfo"]["name"], "echo-stub",
"the COLD daemon's dial reached Alice's real backend from the persisted \
address alone (not a -32055 refusal): {}",
frames[0]
);
shutdown_daemon(&alice_socket).await;
shutdown_daemon(&bob_socket).await;
})
.await
.expect("cold-dial e2e timed out");
}