//! Test E2E de Fase 2: discovery remoto vía DHT. //! //! Pipeline: //! 1. **Provider node (A)**: arma server con `BrahmanNet` configurado; //! listen TCP; un cliente local registra una Card con un output //! flow. El server llama `announce_outputs` automáticamente, lo //! que hace `start_providing` en el DHT bajo la key derivada del //! flow. //! 2. **Consumer node (B)**: arma su propio `BrahmanNet`; dial-ea al //! multiaddr del provider para que ambos se conozcan vía Identify //! (esto popula sus respectivos routing tables de Kademlia). //! 3. **B llama `find_remote_providers(flow_name, type)`**: la query //! DHT propaga vía Kad, y eventually el provider responde con su //! `PeerId`. //! 4. **Verificación**: el `PeerId` que B descubre coincide con el //! de A. //! //! Notas: //! - Kademlia replication factor por defecto es 20; con 2 nodos no //! hay propagación material — A es el único provider, B llega a A //! vía la conexión directa establecida en step 2 y obtiene el record //! del store local de A. //! - El test usa flow `monad-list:json` por familiaridad (es el flow //! real que `akasha daemon` declara). Sirve también como prueba de //! que el sistema completo (daemon + DHT) funcionaría con cero //! cambios en la Card. use std::collections::BTreeSet; use std::sync::Arc; use std::time::Duration; use brahman_broker::{Broker, BrokerConfig}; use brahman_card::{ ulid::Ulid, Card, CardKind, Flow, Flows, Lifecycle, Payload, Priority, Supervision, TypeRef, CARD_SCHEMA_VERSION, }; use brahman_handshake::network::{find_remote_providers, run_libp2p_accept_loop}; use brahman_handshake::server::{Server, ServerConfig}; use brahman_net::{BrahmanNet, Multiaddr, Protocol}; use tempfile::TempDir; use tokio::sync::Mutex; fn provider_card(label: &str, flow_name: &str, type_name: &str) -> Card { Card { schema_version: CARD_SCHEMA_VERSION, id: Ulid::new(), label: label.into(), provides: BTreeSet::new(), requires: BTreeSet::new(), permissions: Default::default(), soma: Default::default(), payload: Payload::Virtual, supervision: Supervision::Delegate, lifecycle: Lifecycle::Daemon, priority: Priority::Normal, kind: CardKind::Ente, flow: Flows { input: vec![], output: vec![Flow { name: flow_name.into(), ty: TypeRef::Primitive { name: type_name.into(), }, pin_to: None, }], }, ..Default::default() } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn dht_discovery_finds_remote_provider() { // ---- Node A (provider): server + libp2p net + Card con output ---- let tmp = TempDir::new().unwrap(); let a_unix = tmp.path().join("a.sock"); let a_broker = Arc::new(Mutex::new(Broker::new(BrokerConfig::default()))); let a_net = Arc::new(BrahmanNet::new().unwrap()); let a_peer = a_net.peer_id; let a_server = Arc::new( Server::bind( &a_unix, ServerConfig { init_attached: true, broker: Some(a_broker.clone()), net: Some(a_net.clone()), // ← clave Fase 2: anuncia al DHT policy: None, }, ) .unwrap(), ); let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/0".parse().unwrap(); let a_addr = a_net.listen(listen_addr).await; let mut a_full_addr = a_addr.clone(); a_full_addr.push(Protocol::P2p(a_peer)); tokio::spawn(run_libp2p_accept_loop(a_server.clone(), a_net.clone())); // Unix accept loop: necesario para que Client::connect al socket // local no cuelgue (Server no se auto-accepta; el caller arma el // loop). Cada session entrante corre en su propia task. { let s = a_server.clone(); tokio::spawn(async move { loop { match s.accept_one().await { Ok(session) => { tokio::spawn(async move { let _ = session.handle().await; }); } Err(_) => break, } } }); } // Registrar la Card local en A con un flow output. let card = provider_card("test.engine_remote", "monad-list", "json"); let mut local_client = brahman_handshake::client::Client::connect(&a_unix, card) .await .expect("registro local en A"); // ---- Node B (consumer): otro net que dial-a a A ---- let b_net = BrahmanNet::new().unwrap(); b_net.dial(a_full_addr.clone()); // Esperar a que la conexión se establezca y Identify popule el // routing table de Kad. En localhost con 2 peers, ~250ms es de // sobra; sumamos margen para CI. tokio::time::sleep(Duration::from_millis(500)).await; // ---- Discovery: B busca providers de "monad-list:json" ---- let providers = find_remote_providers( &b_net, "monad-list", &TypeRef::Primitive { name: "json".into(), }, ) .await; assert!( providers.contains(&a_peer), "B debería descubrir a A vía DHT. Encontrados: {:?}, esperado: {}", providers, a_peer ); // Sanidad: el cliente local sigue vivo durante todo el test (lo // que mantiene la Card registrada y por tanto el record DHT vivo). local_client.farewell().await.ok(); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn dht_discovery_negative_unknown_flow() { // Mismo setup que el test happy-path, pero B busca un flow que A // NO ofrece. Debe devolver lista vacía dentro del timeout // razonable (no colgarse). let tmp = TempDir::new().unwrap(); let a_unix = tmp.path().join("a.sock"); let a_broker = Arc::new(Mutex::new(Broker::new(BrokerConfig::default()))); let a_net = Arc::new(BrahmanNet::new().unwrap()); let a_peer = a_net.peer_id; let a_server = Arc::new( Server::bind( &a_unix, ServerConfig { init_attached: true, broker: Some(a_broker), net: Some(a_net.clone()), policy: None, }, ) .unwrap(), ); let a_addr = a_net.listen("/ip4/127.0.0.1/tcp/0".parse().unwrap()).await; let mut a_full = a_addr.clone(); a_full.push(Protocol::P2p(a_peer)); tokio::spawn(run_libp2p_accept_loop(a_server.clone(), a_net.clone())); // Unix accept loop: necesario para que Client::connect al socket // local no cuelgue (Server no se auto-accepta; el caller arma el // loop). Cada session entrante corre en su propia task. { let s = a_server.clone(); tokio::spawn(async move { loop { match s.accept_one().await { Ok(session) => { tokio::spawn(async move { let _ = session.handle().await; }); } Err(_) => break, } } }); } let card = provider_card("test.engine_other", "monad-list", "json"); let mut local = brahman_handshake::client::Client::connect(&a_unix, card) .await .unwrap(); let b_net = BrahmanNet::new().unwrap(); b_net.dial(a_full); tokio::time::sleep(Duration::from_millis(500)).await; // Buscamos un flow que NADIE anunció. let providers = find_remote_providers( &b_net, "flow-que-no-existe", &TypeRef::Primitive { name: "json".into(), }, ) .await; assert!( providers.is_empty(), "no debería haber providers para un flow inexistente, got: {:?}", providers ); local.farewell().await.ok(); } /// stop_providing test: A registra Card con flow X, B descubre a A. /// El cliente local de A hace farewell → cleanup llama /// withdraw_outputs → A se quita del provider local store. Una nueva /// query desde B (que rutea por A, único peer en el DHT) ya no debe /// listarlo. #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn dht_discovery_withdraws_on_session_cleanup() { let tmp = TempDir::new().unwrap(); let a_unix = tmp.path().join("a.sock"); let a_broker = Arc::new(Mutex::new(Broker::new(BrokerConfig::default()))); let a_net = Arc::new(BrahmanNet::new().unwrap()); let a_peer = a_net.peer_id; let a_server = Arc::new( Server::bind( &a_unix, ServerConfig { init_attached: true, broker: Some(a_broker), net: Some(a_net.clone()), policy: None, }, ) .unwrap(), ); let sessions = a_server.sessions(); let a_addr = a_net.listen("/ip4/127.0.0.1/tcp/0".parse().unwrap()).await; let mut a_full = a_addr.clone(); a_full.push(Protocol::P2p(a_peer)); tokio::spawn(run_libp2p_accept_loop(a_server.clone(), a_net.clone())); { let s = a_server.clone(); tokio::spawn(async move { loop { match s.accept_one().await { Ok(session) => { tokio::spawn(async move { let _ = session.handle().await; }); } Err(_) => break, } } }); } // Card con un flow output anunciable. let card = provider_card("test.withdraws", "monad-list", "json"); let local = brahman_handshake::client::Client::connect(&a_unix, card) .await .expect("registro local en A"); let b_net = BrahmanNet::new().unwrap(); b_net.dial(a_full); tokio::time::sleep(Duration::from_millis(500)).await; // Confirmación previa: A es discoverable. let before = find_remote_providers( &b_net, "monad-list", &TypeRef::Primitive { name: "json".into(), }, ) .await; assert!( before.contains(&a_peer), "antes del farewell A debería ser discoverable. got: {:?}", before ); // Farewell del cliente local → server.cleanup → withdraw_outputs. local.farewell().await.ok(); // Esperamos a que la sesión salga del registro de A (señal de // que cleanup completó). let mut waited = 0; while !sessions.lock().await.is_empty() && waited < 50 { tokio::time::sleep(Duration::from_millis(20)).await; waited += 1; } assert!( sessions.lock().await.is_empty(), "sesión debería estar removida tras farewell" ); // Pequeño margen extra para que el Command::StopProviding lo // procese el swarm task (no es await-able desde fuera). tokio::time::sleep(Duration::from_millis(100)).await; // Nueva query: A ya no debería listarse como provider. let after = find_remote_providers( &b_net, "monad-list", &TypeRef::Primitive { name: "json".into(), }, ) .await; assert!( !after.contains(&a_peer), "tras farewell + withdraw_outputs, A NO debería ser discoverable. got: {:?}", after ); }