初始化提交
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
This commit is contained in:
19
crates/openfang-wire/src/lib.rs
Normal file
19
crates/openfang-wire/src/lib.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! OpenFang Wire Protocol (OFP) — agent-to-agent networking.
|
||||
//!
|
||||
//! Provides cross-machine agent discovery, authentication, and communication
|
||||
//! over TCP connections using a JSON-RPC framed protocol.
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! - **PeerNode**: Local network endpoint that listens for incoming connections
|
||||
//! - **PeerRegistry**: Tracks known peers and their agents
|
||||
//! - **WireMessage**: JSON-framed protocol messages
|
||||
//! - **PeerHandle**: Trait for routing remote messages through the kernel
|
||||
|
||||
pub mod message;
|
||||
pub mod peer;
|
||||
pub mod registry;
|
||||
|
||||
pub use message::{WireMessage, WireRequest, WireResponse};
|
||||
pub use peer::{PeerConfig, PeerNode};
|
||||
pub use registry::{PeerEntry, PeerRegistry, RemoteAgent};
|
||||
292
crates/openfang-wire/src/message.rs
Normal file
292
crates/openfang-wire/src/message.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
//! Wire protocol message types.
|
||||
//!
|
||||
//! All communication between OpenFang peers uses JSON-framed messages
|
||||
//! over TCP. Each message is prefixed with a 4-byte big-endian length header.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A wire protocol message (envelope).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WireMessage {
|
||||
/// Unique message ID.
|
||||
pub id: String,
|
||||
/// Message variant.
|
||||
#[serde(flatten)]
|
||||
pub kind: WireMessageKind,
|
||||
}
|
||||
|
||||
/// The different kinds of wire messages.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum WireMessageKind {
|
||||
/// Request from one peer to another.
|
||||
#[serde(rename = "request")]
|
||||
Request(WireRequest),
|
||||
/// Response to a request.
|
||||
#[serde(rename = "response")]
|
||||
Response(WireResponse),
|
||||
/// One-way notification (no response expected).
|
||||
#[serde(rename = "notification")]
|
||||
Notification(WireNotification),
|
||||
}
|
||||
|
||||
/// Request messages.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "method")]
|
||||
pub enum WireRequest {
|
||||
/// Handshake: exchange peer identity.
|
||||
#[serde(rename = "handshake")]
|
||||
Handshake {
|
||||
/// The peer's unique node ID.
|
||||
node_id: String,
|
||||
/// Human-readable node name.
|
||||
node_name: String,
|
||||
/// Protocol version.
|
||||
protocol_version: u32,
|
||||
/// List of agents available on this peer.
|
||||
agents: Vec<RemoteAgentInfo>,
|
||||
/// Random nonce for HMAC authentication.
|
||||
#[serde(default)]
|
||||
nonce: String,
|
||||
/// HMAC-SHA256(shared_secret, nonce + node_id).
|
||||
#[serde(default)]
|
||||
auth_hmac: String,
|
||||
},
|
||||
/// Discover agents matching a query on the remote peer.
|
||||
#[serde(rename = "discover")]
|
||||
Discover {
|
||||
/// Search query (matches name, tags, description).
|
||||
query: String,
|
||||
},
|
||||
/// Send a message to a specific agent on the remote peer.
|
||||
#[serde(rename = "agent_message")]
|
||||
AgentMessage {
|
||||
/// Target agent ID or name on the remote peer.
|
||||
agent: String,
|
||||
/// The message text.
|
||||
message: String,
|
||||
/// Optional sender identity.
|
||||
sender: Option<String>,
|
||||
},
|
||||
/// Ping to check if the peer is alive.
|
||||
#[serde(rename = "ping")]
|
||||
Ping,
|
||||
}
|
||||
|
||||
/// Response messages.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "method")]
|
||||
pub enum WireResponse {
|
||||
/// Handshake acknowledgement.
|
||||
#[serde(rename = "handshake_ack")]
|
||||
HandshakeAck {
|
||||
node_id: String,
|
||||
node_name: String,
|
||||
protocol_version: u32,
|
||||
agents: Vec<RemoteAgentInfo>,
|
||||
/// Random nonce for HMAC authentication.
|
||||
#[serde(default)]
|
||||
nonce: String,
|
||||
/// HMAC-SHA256(shared_secret, nonce + node_id).
|
||||
#[serde(default)]
|
||||
auth_hmac: String,
|
||||
},
|
||||
/// Discovery results.
|
||||
#[serde(rename = "discover_result")]
|
||||
DiscoverResult { agents: Vec<RemoteAgentInfo> },
|
||||
/// Agent message response.
|
||||
#[serde(rename = "agent_response")]
|
||||
AgentResponse {
|
||||
/// The agent's response text.
|
||||
text: String,
|
||||
},
|
||||
/// Pong response.
|
||||
#[serde(rename = "pong")]
|
||||
Pong {
|
||||
/// Uptime in seconds.
|
||||
uptime_secs: u64,
|
||||
},
|
||||
/// Error response.
|
||||
#[serde(rename = "error")]
|
||||
Error {
|
||||
/// Error code.
|
||||
code: i32,
|
||||
/// Error message.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Notification messages (one-way, no response).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "event")]
|
||||
pub enum WireNotification {
|
||||
/// An agent was spawned on the peer.
|
||||
#[serde(rename = "agent_spawned")]
|
||||
AgentSpawned { agent: RemoteAgentInfo },
|
||||
/// An agent was terminated on the peer.
|
||||
#[serde(rename = "agent_terminated")]
|
||||
AgentTerminated { agent_id: String },
|
||||
/// Peer is shutting down.
|
||||
#[serde(rename = "shutting_down")]
|
||||
ShuttingDown,
|
||||
}
|
||||
|
||||
/// Information about a remote agent.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RemoteAgentInfo {
|
||||
/// Agent ID (UUID string).
|
||||
pub id: String,
|
||||
/// Human-readable name.
|
||||
pub name: String,
|
||||
/// Description of what the agent does.
|
||||
pub description: String,
|
||||
/// Tags for categorization/discovery.
|
||||
pub tags: Vec<String>,
|
||||
/// Available tools.
|
||||
pub tools: Vec<String>,
|
||||
/// Current state.
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
/// Current protocol version.
|
||||
pub const PROTOCOL_VERSION: u32 = 1;
|
||||
|
||||
/// Encode a wire message to bytes (4-byte big-endian length + JSON).
|
||||
pub fn encode_message(msg: &WireMessage) -> Result<Vec<u8>, serde_json::Error> {
|
||||
let json = serde_json::to_vec(msg)?;
|
||||
let len = json.len() as u32;
|
||||
let mut bytes = Vec::with_capacity(4 + json.len());
|
||||
bytes.extend_from_slice(&len.to_be_bytes());
|
||||
bytes.extend_from_slice(&json);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Decode the length prefix from a 4-byte header.
|
||||
pub fn decode_length(header: &[u8; 4]) -> u32 {
|
||||
u32::from_be_bytes(*header)
|
||||
}
|
||||
|
||||
/// Parse a JSON body into a WireMessage.
|
||||
pub fn decode_message(body: &[u8]) -> Result<WireMessage, serde_json::Error> {
|
||||
serde_json::from_slice(body)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode_roundtrip() {
|
||||
let msg = WireMessage {
|
||||
id: "msg-1".to_string(),
|
||||
kind: WireMessageKind::Request(WireRequest::Ping),
|
||||
};
|
||||
let bytes = encode_message(&msg).unwrap();
|
||||
// First 4 bytes are length
|
||||
let len = decode_length(&[bytes[0], bytes[1], bytes[2], bytes[3]]);
|
||||
assert_eq!(len as usize, bytes.len() - 4);
|
||||
let decoded = decode_message(&bytes[4..]).unwrap();
|
||||
assert_eq!(decoded.id, "msg-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handshake_serialization() {
|
||||
let msg = WireMessage {
|
||||
id: "hs-1".to_string(),
|
||||
kind: WireMessageKind::Request(WireRequest::Handshake {
|
||||
node_id: "node-abc".to_string(),
|
||||
node_name: "my-kernel".to_string(),
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
agents: vec![RemoteAgentInfo {
|
||||
id: "agent-1".to_string(),
|
||||
name: "coder".to_string(),
|
||||
description: "A coding agent".to_string(),
|
||||
tags: vec!["code".to_string()],
|
||||
tools: vec!["file_read".to_string()],
|
||||
state: "running".to_string(),
|
||||
}],
|
||||
nonce: "test-nonce".to_string(),
|
||||
auth_hmac: "test-hmac".to_string(),
|
||||
}),
|
||||
};
|
||||
let json = serde_json::to_string_pretty(&msg).unwrap();
|
||||
assert!(json.contains("handshake"));
|
||||
assert!(json.contains("coder"));
|
||||
let decoded: WireMessage = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(decoded.id, "hs-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_message_serialization() {
|
||||
let msg = WireMessage {
|
||||
id: "am-1".to_string(),
|
||||
kind: WireMessageKind::Request(WireRequest::AgentMessage {
|
||||
agent: "coder".to_string(),
|
||||
message: "Write a hello world".to_string(),
|
||||
sender: Some("orchestrator".to_string()),
|
||||
}),
|
||||
};
|
||||
let bytes = encode_message(&msg).unwrap();
|
||||
let decoded = decode_message(&bytes[4..]).unwrap();
|
||||
match decoded.kind {
|
||||
WireMessageKind::Request(WireRequest::AgentMessage { agent, message, .. }) => {
|
||||
assert_eq!(agent, "coder");
|
||||
assert_eq!(message, "Write a hello world");
|
||||
}
|
||||
other => panic!("Expected AgentMessage, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_response() {
|
||||
let msg = WireMessage {
|
||||
id: "err-1".to_string(),
|
||||
kind: WireMessageKind::Response(WireResponse::Error {
|
||||
code: 404,
|
||||
message: "Agent not found".to_string(),
|
||||
}),
|
||||
};
|
||||
let json = serde_json::to_string(&msg).unwrap();
|
||||
let decoded: WireMessage = serde_json::from_str(&json).unwrap();
|
||||
match decoded.kind {
|
||||
WireMessageKind::Response(WireResponse::Error { code, message }) => {
|
||||
assert_eq!(code, 404);
|
||||
assert_eq!(message, "Agent not found");
|
||||
}
|
||||
other => panic!("Expected Error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notification_serialization() {
|
||||
let msg = WireMessage {
|
||||
id: "n-1".to_string(),
|
||||
kind: WireMessageKind::Notification(WireNotification::AgentSpawned {
|
||||
agent: RemoteAgentInfo {
|
||||
id: "a-1".to_string(),
|
||||
name: "researcher".to_string(),
|
||||
description: "Research agent".to_string(),
|
||||
tags: vec![],
|
||||
tools: vec![],
|
||||
state: "running".to_string(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
let json = serde_json::to_string(&msg).unwrap();
|
||||
assert!(json.contains("agent_spawned"));
|
||||
let _: WireMessage = serde_json::from_str(&json).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discover_request() {
|
||||
let msg = WireMessage {
|
||||
id: "d-1".to_string(),
|
||||
kind: WireMessageKind::Request(WireRequest::Discover {
|
||||
query: "security".to_string(),
|
||||
}),
|
||||
};
|
||||
let json = serde_json::to_string(&msg).unwrap();
|
||||
assert!(json.contains("discover"));
|
||||
assert!(json.contains("security"));
|
||||
}
|
||||
}
|
||||
1022
crates/openfang-wire/src/peer.rs
Normal file
1022
crates/openfang-wire/src/peer.rs
Normal file
File diff suppressed because it is too large
Load Diff
351
crates/openfang-wire/src/registry.rs
Normal file
351
crates/openfang-wire/src/registry.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
//! Peer registry — tracks connected peers and their agents.
|
||||
//!
|
||||
//! The [`PeerRegistry`] is a thread-safe, concurrent data structure that
|
||||
//! records all known remote peers, their connection state, and the agents
|
||||
//! they advertise.
|
||||
|
||||
use crate::message::RemoteAgentInfo;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
/// A tracked remote agent, enriched with the owning peer's identity.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoteAgent {
|
||||
/// The remote peer that hosts this agent.
|
||||
pub peer_node_id: String,
|
||||
/// Agent details from the wire protocol.
|
||||
pub info: RemoteAgentInfo,
|
||||
}
|
||||
|
||||
/// Connection state of a peer.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PeerState {
|
||||
/// Handshake completed, fully connected.
|
||||
Connected,
|
||||
/// Connection lost but not removed yet (eligible for reconnect).
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
/// An entry representing a single known peer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PeerEntry {
|
||||
/// Unique node ID of the peer.
|
||||
pub node_id: String,
|
||||
/// Human-readable node name.
|
||||
pub node_name: String,
|
||||
/// Socket address of the peer.
|
||||
pub address: SocketAddr,
|
||||
/// Agents advertised by this peer.
|
||||
pub agents: Vec<RemoteAgentInfo>,
|
||||
/// Connection state.
|
||||
pub state: PeerState,
|
||||
/// When the peer first connected.
|
||||
pub connected_at: DateTime<Utc>,
|
||||
/// Protocol version negotiated during handshake.
|
||||
pub protocol_version: u32,
|
||||
}
|
||||
|
||||
/// Thread-safe registry of all known peers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PeerRegistry {
|
||||
peers: Arc<RwLock<HashMap<String, PeerEntry>>>,
|
||||
}
|
||||
|
||||
impl PeerRegistry {
|
||||
/// Create a new empty registry.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
peers: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register or update a peer after a successful handshake.
|
||||
pub fn add_peer(&self, entry: PeerEntry) {
|
||||
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
|
||||
peers.insert(entry.node_id.clone(), entry);
|
||||
}
|
||||
|
||||
/// Remove a peer entirely.
|
||||
pub fn remove_peer(&self, node_id: &str) -> Option<PeerEntry> {
|
||||
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
|
||||
peers.remove(node_id)
|
||||
}
|
||||
|
||||
/// Mark a peer as disconnected (but keep its entry for possible reconnect).
|
||||
pub fn mark_disconnected(&self, node_id: &str) {
|
||||
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(entry) = peers.get_mut(node_id) {
|
||||
entry.state = PeerState::Disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a peer as connected again.
|
||||
pub fn mark_connected(&self, node_id: &str) {
|
||||
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(entry) = peers.get_mut(node_id) {
|
||||
entry.state = PeerState::Connected;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a snapshot of a specific peer.
|
||||
pub fn get_peer(&self, node_id: &str) -> Option<PeerEntry> {
|
||||
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
|
||||
peers.get(node_id).cloned()
|
||||
}
|
||||
|
||||
/// Get all connected peers.
|
||||
pub fn connected_peers(&self) -> Vec<PeerEntry> {
|
||||
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
|
||||
peers
|
||||
.values()
|
||||
.filter(|p| p.state == PeerState::Connected)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all peers (connected + disconnected).
|
||||
pub fn all_peers(&self) -> Vec<PeerEntry> {
|
||||
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
|
||||
peers.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Update the agent list for a peer (e.g., after an AgentSpawned notification).
|
||||
pub fn update_agents(&self, node_id: &str, agents: Vec<RemoteAgentInfo>) {
|
||||
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(entry) = peers.get_mut(node_id) {
|
||||
entry.agents = agents;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a single agent to a peer's advertised list.
|
||||
pub fn add_agent(&self, node_id: &str, agent: RemoteAgentInfo) {
|
||||
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(entry) = peers.get_mut(node_id) {
|
||||
// Replace if agent with same ID already exists, otherwise push
|
||||
if let Some(existing) = entry.agents.iter_mut().find(|a| a.id == agent.id) {
|
||||
*existing = agent;
|
||||
} else {
|
||||
entry.agents.push(agent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an agent from a peer's advertised list.
|
||||
pub fn remove_agent(&self, node_id: &str, agent_id: &str) {
|
||||
let mut peers = self.peers.write().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(entry) = peers.get_mut(node_id) {
|
||||
entry.agents.retain(|a| a.id != agent_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all remote agents matching a query (searches name, tags, description).
|
||||
pub fn find_agents(&self, query: &str) -> Vec<RemoteAgent> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
|
||||
let mut results = Vec::new();
|
||||
|
||||
for peer in peers.values() {
|
||||
if peer.state != PeerState::Connected {
|
||||
continue;
|
||||
}
|
||||
for agent in &peer.agents {
|
||||
let matches = agent.name.to_lowercase().contains(&query_lower)
|
||||
|| agent.description.to_lowercase().contains(&query_lower)
|
||||
|| agent
|
||||
.tags
|
||||
.iter()
|
||||
.any(|t| t.to_lowercase().contains(&query_lower));
|
||||
if matches {
|
||||
results.push(RemoteAgent {
|
||||
peer_node_id: peer.node_id.clone(),
|
||||
info: agent.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Get all remote agents across all connected peers.
|
||||
pub fn all_remote_agents(&self) -> Vec<RemoteAgent> {
|
||||
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
|
||||
let mut results = Vec::new();
|
||||
|
||||
for peer in peers.values() {
|
||||
if peer.state != PeerState::Connected {
|
||||
continue;
|
||||
}
|
||||
for agent in &peer.agents {
|
||||
results.push(RemoteAgent {
|
||||
peer_node_id: peer.node_id.clone(),
|
||||
info: agent.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Number of connected peers.
|
||||
pub fn connected_count(&self) -> usize {
|
||||
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
|
||||
peers
|
||||
.values()
|
||||
.filter(|p| p.state == PeerState::Connected)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Total number of peers (including disconnected).
|
||||
pub fn total_count(&self) -> usize {
|
||||
let peers = self.peers.read().unwrap_or_else(|e| e.into_inner());
|
||||
peers.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PeerRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_peer(node_id: &str, agents: Vec<RemoteAgentInfo>) -> PeerEntry {
|
||||
PeerEntry {
|
||||
node_id: node_id.to_string(),
|
||||
node_name: format!("{node_id}-name"),
|
||||
address: "127.0.0.1:9000".parse().unwrap(),
|
||||
agents,
|
||||
state: PeerState::Connected,
|
||||
connected_at: Utc::now(),
|
||||
protocol_version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_agent(id: &str, name: &str, tags: &[&str]) -> RemoteAgentInfo {
|
||||
RemoteAgentInfo {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: format!("{name} agent"),
|
||||
tags: tags.iter().map(|s| s.to_string()).collect(),
|
||||
tools: vec![],
|
||||
state: "running".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_and_get_peer() {
|
||||
let registry = PeerRegistry::new();
|
||||
let peer = make_peer("node-1", vec![make_agent("a1", "coder", &["code"])]);
|
||||
registry.add_peer(peer);
|
||||
|
||||
let retrieved = registry.get_peer("node-1").unwrap();
|
||||
assert_eq!(retrieved.node_id, "node-1");
|
||||
assert_eq!(retrieved.agents.len(), 1);
|
||||
assert_eq!(retrieved.agents[0].name, "coder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_peer() {
|
||||
let registry = PeerRegistry::new();
|
||||
registry.add_peer(make_peer("node-1", vec![]));
|
||||
assert_eq!(registry.total_count(), 1);
|
||||
|
||||
let removed = registry.remove_peer("node-1");
|
||||
assert!(removed.is_some());
|
||||
assert_eq!(registry.total_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disconnect_reconnect() {
|
||||
let registry = PeerRegistry::new();
|
||||
registry.add_peer(make_peer("node-1", vec![]));
|
||||
assert_eq!(registry.connected_count(), 1);
|
||||
|
||||
registry.mark_disconnected("node-1");
|
||||
assert_eq!(registry.connected_count(), 0);
|
||||
assert_eq!(registry.total_count(), 1);
|
||||
|
||||
registry.mark_connected("node-1");
|
||||
assert_eq!(registry.connected_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_agents_by_name() {
|
||||
let registry = PeerRegistry::new();
|
||||
registry.add_peer(make_peer(
|
||||
"node-1",
|
||||
vec![
|
||||
make_agent("a1", "coder", &["code"]),
|
||||
make_agent("a2", "researcher", &["research"]),
|
||||
],
|
||||
));
|
||||
registry.add_peer(make_peer(
|
||||
"node-2",
|
||||
vec![make_agent("a3", "code-reviewer", &["code", "review"])],
|
||||
));
|
||||
|
||||
let results = registry.find_agents("code");
|
||||
assert_eq!(results.len(), 2); // "coder" and "code-reviewer"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_agents_by_tag() {
|
||||
let registry = PeerRegistry::new();
|
||||
registry.add_peer(make_peer(
|
||||
"node-1",
|
||||
vec![make_agent("a1", "helper", &["security", "audit"])],
|
||||
));
|
||||
|
||||
let results = registry.find_agents("security");
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].info.name, "helper");
|
||||
assert_eq!(results[0].peer_node_id, "node-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_agents_skips_disconnected() {
|
||||
let registry = PeerRegistry::new();
|
||||
registry.add_peer(make_peer(
|
||||
"node-1",
|
||||
vec![make_agent("a1", "coder", &["code"])],
|
||||
));
|
||||
registry.mark_disconnected("node-1");
|
||||
|
||||
let results = registry.find_agents("coder");
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_remove_agent() {
|
||||
let registry = PeerRegistry::new();
|
||||
registry.add_peer(make_peer("node-1", vec![]));
|
||||
|
||||
registry.add_agent("node-1", make_agent("a1", "coder", &[]));
|
||||
assert_eq!(registry.get_peer("node-1").unwrap().agents.len(), 1);
|
||||
|
||||
registry.remove_agent("node-1", "a1");
|
||||
assert_eq!(registry.get_peer("node-1").unwrap().agents.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_remote_agents() {
|
||||
let registry = PeerRegistry::new();
|
||||
registry.add_peer(make_peer("node-1", vec![make_agent("a1", "coder", &[])]));
|
||||
registry.add_peer(make_peer(
|
||||
"node-2",
|
||||
vec![
|
||||
make_agent("a2", "writer", &[]),
|
||||
make_agent("a3", "tester", &[]),
|
||||
],
|
||||
));
|
||||
|
||||
let all = registry.all_remote_agents();
|
||||
assert_eq!(all.len(), 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user