use std::sync::Arc;
use std::time::Duration;
use mcpmesh::allowlist::{AllowlistGate, PeerEntry, PeerStore};
use mcpmesh::config::Config;
use mcpmesh::daemon::{MeshState, build_services, spawn_accept_loop};
use mcpmesh::pairing::{Invite, LiveInvites};
use mcpmesh::roster::gate::RosterGate;
use mcpmesh_net::framing::{FrameReader, Inbound, write_frame};
use mcpmesh_net::registry::ConnRegistry;
use mcpmesh_net::{ALPN_MCP, ALPN_PAIR, TrustGate, connect};
use serde_json::json;
use tokio::io::BufReader;
use tokio::time::timeout;
const STUB: &str = env!("CARGO_BIN_EXE_echo_mcp_stub");
async fn dual_alpn_endpoint() -> iroh::Endpoint {
iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(iroh::RelayMode::Disabled)
.alpns(vec![ALPN_MCP.to_vec(), ALPN_PAIR.to_vec()])
.bind()
.await
.expect("bind dual-ALPN endpoint")
}
async fn client_endpoint() -> iroh::Endpoint {
iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(iroh::RelayMode::Disabled)
.alpns(vec![ALPN_MCP.to_vec()])
.bind()
.await
.expect("bind client endpoint")
}
#[tokio::test]
async fn accept_loop_routes_mesh_alpn_to_a_gated_session() {
timeout(Duration::from_secs(60), async {
let dir = tempfile::tempdir().unwrap();
let client = client_endpoint().await;
let cfg = Config::from_toml_str(&format!(
"[services.echo]\nrun = ['{STUB}']\nallow = [\"eid:{}\"]\n",
client.id()
))
.expect("parse config");
let store = Arc::new(PeerStore::open(&dir.path().join("state.redb")).unwrap());
store
.add(PeerEntry {
endpoint_id: *client.id().as_bytes(),
nickname: "tester".into(),
services: vec!["echo".into()],
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
let gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(store.clone()));
let server = dual_alpn_endpoint().await;
let addr = server.addr();
let mesh = MeshState::new(
server,
gate,
store,
Arc::new(LiveInvites::new()),
"server".into(),
dir.path().join("config.toml"),
Arc::new(RosterGate::empty()),
Arc::new(ConnRegistry::new()),
None,
None,
None,
None,
);
let _task = spawn_accept_loop(mesh.clone(), Arc::new(build_services(&cfg)));
let mut transport = connect(&client, addr, "echo").await.unwrap().0;
transport
.send_value(json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"_meta": {"mcpmesh/service": "echo"},
"capabilities": {}, "clientInfo": {"name": "tester", "version": "0"}
}
}))
.await
.unwrap();
let init = transport.recv_value().await.unwrap().unwrap();
assert_eq!(
init["result"]["serverInfo"]["name"], "echo-stub",
"mcp/1 must route to a gated mesh session under the daemon's accept loop: {init}"
);
})
.await
.expect("mesh-dispatch test timed out");
}
const FUTURE: u64 = 4_000_000_000;
fn decoy_invite(secret: [u8; 32]) -> Invite {
Invite {
secret,
inviter_id: [0xEEu8; 32],
inviter_addr_json: "{}".into(),
nickname: "server".into(),
services: vec!["x".into()],
expires_at_epoch: FUTURE,
app_label: None,
}
}
#[tokio::test]
async fn accept_loop_routes_pair_alpn_to_the_gate_exempt_rendezvous() {
timeout(Duration::from_secs(60), async {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(PeerStore::open(&dir.path().join("state.redb")).unwrap());
let gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(store.clone()));
let invites = Arc::new(LiveInvites::new());
invites.mint(decoy_invite([1u8; 32]));
let server = dual_alpn_endpoint().await;
let addr = server.addr();
let mesh = MeshState::new(
server,
gate,
store,
invites,
"server".into(),
dir.path().join("config.toml"),
Arc::new(RosterGate::empty()),
Arc::new(ConnRegistry::new()),
None,
None,
None,
None,
);
let _task = spawn_accept_loop(
mesh.clone(),
Arc::new(build_services(&Config::from_toml_str("").unwrap())),
);
let client = client_endpoint().await;
let conn = client
.connect(addr, ALPN_PAIR)
.await
.expect("pair/1 dial is accepted (gate-exempt)");
let (mut send, recv) = conn.open_bi().await.expect("open bi-stream");
let hello = json!({
"secret": vec![0u8; 32],
"redeemer_id": client.id().as_bytes().to_vec(),
"redeemer_nickname": "stranger",
});
write_frame(&mut send, &hello).await.expect("send hello");
let _ = send.finish();
let mut reader = FrameReader::new(BufReader::new(recv), 64 * 1024);
let reply = match reader.next().await.expect("read reply frame") {
Some(Inbound::Frame(v)) => v,
other => panic!("pair/1 must reply with a refusal frame, got: {other:?}"),
};
assert_eq!(
reply["result"], "refused",
"pair/1 must reach the gate-exempt rendezvous and refuse by invite, got: {reply}"
);
assert_eq!(
reply["reason"], "pairing refused",
"an unknown secret gets the generic refusal reason, got: {reply}"
);
})
.await
.expect("pair-dispatch test timed out");
}
#[tokio::test]
async fn accept_loop_pair_alpn_with_no_live_invite_is_closed_early() {
timeout(Duration::from_secs(60), async {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(PeerStore::open(&dir.path().join("state.redb")).unwrap());
let gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(store.clone()));
let server = dual_alpn_endpoint().await;
let addr = server.addr();
let mesh = MeshState::new(
server,
gate,
store.clone(),
Arc::new(LiveInvites::new()), "server".into(),
dir.path().join("config.toml"),
Arc::new(RosterGate::empty()),
Arc::new(ConnRegistry::new()),
None,
None,
None,
None,
);
let _task = spawn_accept_loop(
mesh.clone(),
Arc::new(build_services(&Config::from_toml_str("").unwrap())),
);
let client = client_endpoint().await;
let client_id = *client.id().as_bytes();
let got_reply: Option<serde_json::Value> = match client.connect(addr, ALPN_PAIR).await {
Err(_) => None, Ok(conn) => match conn.open_bi().await {
Err(_) => None, Ok((mut send, recv)) => {
let hello = json!({
"secret": vec![0u8; 32],
"redeemer_id": client_id.to_vec(),
"redeemer_nickname": "stranger",
});
let _ = write_frame(&mut send, &hello).await;
let _ = send.finish();
let mut reader = FrameReader::new(BufReader::new(recv), 64 * 1024);
match reader.next().await {
Ok(Some(Inbound::Frame(v))) => Some(v),
_ => None, }
}
},
};
assert!(
got_reply.is_none(),
"a pair dial with no live invite must be closed early (no rendezvous reply), got: {got_reply:?}"
);
assert!(
store.resolve(&client_id).unwrap().is_none(),
"the accept-gate must not let any PeerEntry be written when no invite is live"
);
})
.await
.expect("no-live-invite accept-gate test timed out");
}
#[tokio::test]
async fn trust_mutations_emit_audit_events() {
use mcpmesh::audit::{AuditLog, AuditSink};
use mcpmesh::daemon::grant_service_access;
timeout(Duration::from_secs(30), async {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
format!("[services.notes]\nrun = ['{STUB}']\nallow = []\n"),
)
.unwrap();
let server = dual_alpn_endpoint().await;
let store = Arc::new(PeerStore::open(&dir.path().join("state.redb")).unwrap());
let gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(store.clone()));
let mesh = MeshState::new(
server,
gate,
store.clone(),
Arc::new(LiveInvites::new()),
"server".into(),
config_path.clone(),
Arc::new(RosterGate::empty()),
Arc::new(ConnRegistry::new()),
None,
None,
None,
None,
);
let audit_dir = dir.path().join("audit");
mesh.set_audit(AuditSink::new(AuditLog::spawn(audit_dir.clone())));
grant_service_access(&mesh, "b64u:BOB", "bob", &["notes".to_string()])
.await
.unwrap();
let month = &mcpmesh::audit::now_ts()[..7];
let file = audit_dir.join(format!("{month}.jsonl"));
let mut pair = 0;
for _ in 0..50 {
if let Ok(b) = std::fs::read_to_string(&file) {
pair = b.matches("\"event\":\"pair\"").count();
if pair >= 1 {
break;
}
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert_eq!(pair, 1, "the pairing grant recorded one trust(pair) event");
let body = std::fs::read_to_string(&file).unwrap();
assert!(body.contains("\"kind\":\"trust\""));
assert!(
body.contains("\"target\":\"bob\""),
"the audit record targets the DISPLAY nickname, got: {body}"
);
assert!(
body.contains("\"principal\":\"b64u:BOB\""),
"the pair record carries the stable principal (#57): {body}"
);
let cfg_body = std::fs::read_to_string(&config_path).unwrap();
assert!(
cfg_body.contains("b64u:BOB"),
"the grant appends the stable principal to the service allow, got: {cfg_body}"
);
assert!(
!cfg_body.contains("\"bob\""),
"the display nickname must never land in the allow, got: {cfg_body}"
);
store
.add(mcpmesh::allowlist::PeerEntry {
endpoint_id: [0xB0u8; 32],
nickname: "bob".into(),
services: vec![],
paired_at: None,
user_id: Some("b64u:BOB".into()),
last_addr: None,
})
.unwrap();
let state = mcpmesh::control::DaemonState::with_mesh("test", mesh.clone());
mcpmesh::daemon::remove_peer(
&state,
mcpmesh_local_api::PeerRemoveParams {
nickname: "bob".into(),
},
)
.await
.unwrap();
let mut unpair = 0;
for _ in 0..50 {
if let Ok(b) = std::fs::read_to_string(&file) {
unpair = b.matches("\"event\":\"unpair\"").count();
if unpair >= 1 {
break;
}
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert_eq!(unpair, 1, "the unpair recorded one trust event");
let body = std::fs::read_to_string(&file).unwrap();
let unpair_line = body
.lines()
.find(|l| l.contains("\"event\":\"unpair\""))
.expect("unpair line present");
assert!(
!unpair_line.contains("principal"),
"unpair has no single subject — no principal (#57): {unpair_line}"
);
})
.await
.expect("trust audit test timed out");
}