Skip to main content

ipfrs_semantic/
dht_node.rs

1//! Distributed Semantic DHT Node
2//!
3//! This module implements the main DHT node that coordinates:
4//! - Local vector index management
5//! - Distributed k-NN search across peers
6//! - Replication and fault tolerance
7//! - Query routing and result aggregation
8
9use crate::dht::{
10    ReplicationStrategy, SemanticDHTConfig, SemanticDHTStats, SemanticPeer, SemanticRoutingTable,
11};
12use crate::hnsw::{SearchResult, VectorIndex};
13use futures::future;
14use ipfrs_core::{Cid, Result};
15use ipfrs_network::libp2p::PeerId;
16use parking_lot::RwLock;
17use std::collections::HashMap;
18use std::sync::Arc;
19use std::time::Instant;
20
21/// Main semantic DHT node
22pub struct SemanticDHTNode {
23    /// Configuration
24    config: SemanticDHTConfig,
25    /// Local peer ID
26    local_peer_id: PeerId,
27    /// Local vector index
28    local_index: Arc<RwLock<VectorIndex>>,
29    /// Routing table
30    routing_table: Arc<SemanticRoutingTable>,
31    /// Replication strategy
32    replication_strategy: ReplicationStrategy,
33    /// Query statistics
34    stats: Arc<RwLock<SemanticDHTStats>>,
35    /// Pending queries
36    pending_queries: Arc<RwLock<HashMap<String, Instant>>>,
37    /// Last successful synchronization timestamp (unix timestamp in seconds)
38    last_sync_timestamp: Arc<RwLock<u64>>,
39    /// Number of pending synchronization operations
40    pending_syncs: Arc<RwLock<usize>>,
41}
42
43impl SemanticDHTNode {
44    /// Create a new semantic DHT node
45    pub fn new(config: SemanticDHTConfig, local_peer_id: PeerId, local_index: VectorIndex) -> Self {
46        let routing_table = Arc::new(SemanticRoutingTable::new(config.clone()));
47
48        let stats = SemanticDHTStats {
49            num_peers: 0,
50            num_clusters: 0,
51            num_local_entries: 0,
52            queries_processed: 0,
53            avg_query_latency_ms: 0.0,
54            multi_hop_queries: 0,
55        };
56
57        Self {
58            config,
59            local_peer_id,
60            local_index: Arc::new(RwLock::new(local_index)),
61            routing_table,
62            replication_strategy: ReplicationStrategy::NearestPeers(3),
63            stats: Arc::new(RwLock::new(stats)),
64            pending_queries: Arc::new(RwLock::new(HashMap::new())),
65            last_sync_timestamp: Arc::new(RwLock::new(0)),
66            pending_syncs: Arc::new(RwLock::new(0)),
67        }
68    }
69
70    /// Insert a vector into the local index and replicate to peers
71    pub async fn insert(&self, cid: &Cid, embedding: &[f32]) -> Result<()> {
72        // Insert into local index
73        self.local_index.write().insert(cid, embedding)?;
74
75        // Update local embedding (aggregate of stored vectors)
76        self.update_local_embedding().await?;
77
78        // Determine replica peers based on strategy
79        let replica_peers = self.select_replica_peers(embedding).await?;
80
81        // Send replication requests to up to replication_factor nearest peers.
82        for peer in replica_peers {
83            if let Err(e) = self.replicate_to_peer(&peer, cid, embedding).await {
84                tracing::warn!("Replication to {:?} failed: {}", peer, e);
85            }
86        }
87
88        Ok(())
89    }
90
91    /// Search for nearest neighbors locally
92    pub fn search_local(&self, embedding: &[f32], k: usize) -> Result<Vec<SearchResult>> {
93        let index = self.local_index.read();
94        let ef_search = self.config.max_hops * 10; // Heuristic
95        index.search(embedding, k, ef_search)
96    }
97
98    /// Distributed k-NN search across multiple peers
99    pub async fn search_distributed(
100        &self,
101        embedding: &[f32],
102        k: usize,
103    ) -> Result<Vec<SearchResult>> {
104        let query_id = format!("{:?}-{}", self.local_peer_id, uuid::Uuid::new_v4());
105        let start_time = Instant::now();
106
107        // Record pending query
108        self.pending_queries
109            .write()
110            .insert(query_id.clone(), start_time);
111
112        // Local search
113        let mut all_results = self.search_local(embedding, k)?;
114
115        // Find nearest peers to forward query to
116        let nearest_peers = self
117            .routing_table
118            .find_nearest_peers_balanced(embedding, self.config.routing_table_size);
119
120        // Multi-hop search
121        if !nearest_peers.is_empty() && self.config.max_hops > 0 {
122            let remote_results = self
123                .multi_hop_search(embedding, k, query_id.clone(), 0)
124                .await?;
125            all_results.extend(remote_results);
126        }
127
128        // Aggregate and rank results
129        let final_results = self.aggregate_results(all_results, k);
130
131        // Update statistics
132        let latency = start_time.elapsed().as_millis() as f64;
133        self.update_query_stats(latency, !nearest_peers.is_empty());
134
135        // Clean up pending query
136        self.pending_queries.write().remove(&query_id);
137
138        Ok(final_results)
139    }
140
141    /// Multi-hop search with TTL
142    async fn multi_hop_search(
143        &self,
144        embedding: &[f32],
145        _k: usize,
146        _query_id: String,
147        hop: usize,
148    ) -> Result<Vec<SearchResult>> {
149        if hop >= self.config.max_hops {
150            return Ok(Vec::new());
151        }
152
153        let nearest_peers = self.routing_table.find_nearest_peers_balanced(embedding, 3); // Top 3 peers
154
155        let mut all_results = Vec::new();
156
157        // Query nearest peers in parallel and collect results.
158        let peer_futures: Vec<_> = nearest_peers
159            .iter()
160            .filter(|(peer_id, _)| *peer_id != self.local_peer_id)
161            .map(|(peer_id, _)| {
162                let peer_id = *peer_id;
163                async move {
164                    tracing::debug!("Querying peer {:?} at hop {}", peer_id, hop);
165                    self.query_peer(&peer_id, embedding).await
166                }
167            })
168            .collect();
169
170        let results = future::join_all(peer_futures).await;
171        // Flatten non-empty result sets from peers that responded.
172        for peer_results in results.into_iter().flatten() {
173            all_results.extend(peer_results);
174        }
175
176        Ok(all_results)
177    }
178
179    /// Aggregate and deduplicate results from multiple sources
180    fn aggregate_results(&self, results: Vec<SearchResult>, k: usize) -> Vec<SearchResult> {
181        // Deduplicate by CID
182        let mut seen = HashMap::new();
183        let mut deduplicated = Vec::new();
184
185        for result in results {
186            if let Some(&existing_score) = seen.get(&result.cid) {
187                // Keep better score
188                if result.score < existing_score {
189                    // Find and update
190                    if let Some(pos) = deduplicated
191                        .iter()
192                        .position(|r: &SearchResult| r.cid == result.cid)
193                    {
194                        deduplicated[pos] = result.clone();
195                        seen.insert(result.cid, result.score);
196                    }
197                }
198            } else {
199                seen.insert(result.cid, result.score);
200                deduplicated.push(result);
201            }
202        }
203
204        // Sort by score and take top k
205        deduplicated.sort_by(|a, b| {
206            a.score
207                .partial_cmp(&b.score)
208                .unwrap_or(std::cmp::Ordering::Equal)
209        });
210        deduplicated.into_iter().take(k).collect()
211    }
212
213    /// Select replica peers based on replication strategy
214    async fn select_replica_peers(&self, embedding: &[f32]) -> Result<Vec<PeerId>> {
215        match &self.replication_strategy {
216            ReplicationStrategy::NearestPeers(n) => {
217                let peers = self.routing_table.find_nearest_peers(embedding, *n);
218                Ok(peers.into_iter().map(|(peer_id, _)| peer_id).collect())
219            }
220            ReplicationStrategy::SameCluster => {
221                // Find local peer's cluster
222                // For now, return empty
223                Ok(Vec::new())
224            }
225            ReplicationStrategy::CrossCluster(_n) => {
226                // Select n peers from different clusters
227                // For now, return empty
228                Ok(Vec::new())
229            }
230        }
231    }
232
233    /// Update local peer's embedding based on stored vectors
234    async fn update_local_embedding(&self) -> Result<()> {
235        let index = self.local_index.read();
236        let dim = self.config.embedding_dim;
237
238        // Compute centroid of all local vectors
239        let mut centroid = vec![0.0; dim];
240        let _count = 0;
241
242        // This is a simplified version - in practice, we'd iterate over actual vectors
243        // For now, just use a placeholder
244        drop(index);
245
246        // Normalize centroid
247        let norm: f32 = centroid.iter().map(|x| x * x).sum::<f32>().sqrt();
248        if norm > 1e-6 {
249            for x in &mut centroid {
250                *x /= norm;
251            }
252        }
253
254        self.routing_table.update_local_embedding(centroid)?;
255
256        Ok(())
257    }
258
259    /// Update query statistics
260    fn update_query_stats(&self, latency_ms: f64, is_multi_hop: bool) {
261        let mut stats = self.stats.write();
262        stats.queries_processed += 1;
263
264        // Update running average
265        let alpha = 0.1; // Exponential moving average factor
266        stats.avg_query_latency_ms =
267            alpha * latency_ms + (1.0 - alpha) * stats.avg_query_latency_ms;
268
269        if is_multi_hop {
270            stats.multi_hop_queries += 1;
271        }
272    }
273
274    /// Add a peer to the routing table
275    pub fn add_peer(&self, peer: SemanticPeer) -> Result<()> {
276        self.routing_table.add_peer(peer)?;
277
278        // Update stats
279        let mut stats = self.stats.write();
280        stats.num_peers = self.routing_table.num_peers();
281
282        Ok(())
283    }
284
285    /// Remove a peer from the routing table
286    pub fn remove_peer(&self, peer_id: &PeerId) {
287        self.routing_table.remove_peer(peer_id);
288
289        // Update stats
290        let mut stats = self.stats.write();
291        stats.num_peers = self.routing_table.num_peers();
292    }
293
294    /// Update peer clustering
295    pub fn update_clusters(&self, num_clusters: usize) -> Result<()> {
296        self.routing_table.update_clusters(num_clusters)?;
297
298        // Update stats
299        let mut stats = self.stats.write();
300        stats.num_clusters = self.routing_table.num_clusters();
301
302        Ok(())
303    }
304
305    /// Get DHT statistics
306    pub fn stats(&self) -> SemanticDHTStats {
307        let mut stats = self.stats.read().clone();
308        stats.num_local_entries = self.local_index.read().len();
309        stats
310    }
311
312    /// Get DHT statistics (alias for stats)
313    pub fn get_stats(&self) -> SemanticDHTStats {
314        self.stats()
315    }
316
317    /// Get reference to the routing table
318    pub fn routing_table(&self) -> &SemanticRoutingTable {
319        &self.routing_table
320    }
321
322    /// Set replication strategy
323    pub fn set_replication_strategy(&mut self, strategy: ReplicationStrategy) {
324        self.replication_strategy = strategy;
325    }
326
327    /// Get a snapshot of local index entries for synchronization
328    /// Returns CIDs that can be used for delta synchronization
329    pub fn get_index_snapshot(&self) -> Vec<Cid> {
330        let index = self.local_index.read();
331        // Get all CIDs from the index
332        index.get_all_cids()
333    }
334
335    /// Check if local index has a specific CID
336    pub fn has_entry(&self, cid: &Cid) -> bool {
337        let index = self.local_index.read();
338        index.contains(cid)
339    }
340
341    /// Prepare synchronization delta: entries that peer needs
342    /// Returns CIDs that are in our index but not in the peer's snapshot
343    pub fn prepare_sync_delta(&self, peer_snapshot: &[Cid]) -> Vec<Cid> {
344        let local_snapshot = self.get_index_snapshot();
345        let peer_set: std::collections::HashSet<_> = peer_snapshot.iter().collect();
346
347        local_snapshot
348            .into_iter()
349            .filter(|cid| !peer_set.contains(cid))
350            .collect()
351    }
352
353    /// Apply synchronization delta: add entries from peer
354    /// This is a foundation - actual implementation would fetch embeddings from peer
355    pub async fn apply_sync_delta(&self, delta_cids: Vec<Cid>) -> Result<usize> {
356        // NOTE: In full implementation with network protocol, this would:
357        // 1. Request embeddings for delta_cids from peer
358        // 2. Call apply_sync_delta_with_embeddings with the fetched data
359        // For now, just return count of CIDs that would be synced
360        Ok(delta_cids.len())
361    }
362
363    /// Apply synchronization delta with embeddings: add entries from peer
364    /// This method actually inserts the embeddings into the local index
365    pub async fn apply_sync_delta_with_embeddings(
366        &self,
367        delta_entries: Vec<(Cid, Vec<f32>)>,
368    ) -> Result<usize> {
369        // Increment pending syncs counter
370        *self.pending_syncs.write() += 1;
371
372        let mut synced_count = 0;
373
374        // Insert each entry into local index
375        for (cid, embedding) in delta_entries {
376            match self.local_index.write().insert(&cid, &embedding) {
377                Ok(_) => {
378                    synced_count += 1;
379                }
380                Err(e) => {
381                    tracing::warn!("Failed to insert CID {:?} during sync: {}", cid, e);
382                }
383            }
384        }
385
386        // Update last sync timestamp
387        let now = std::time::SystemTime::now()
388            .duration_since(std::time::UNIX_EPOCH)
389            .unwrap_or_default()
390            .as_secs();
391        *self.last_sync_timestamp.write() = now;
392
393        // Decrement pending syncs counter
394        *self.pending_syncs.write() -= 1;
395
396        // Update local embedding after sync
397        self.update_local_embedding().await?;
398
399        tracing::debug!("Synced {} entries from peer", synced_count);
400
401        Ok(synced_count)
402    }
403
404    /// Replicate a (key, value) pair to a remote peer.
405    ///
406    /// When no active transport is wired up (current state) this is a no-op
407    /// that logs the intent and returns `Ok(())`.  A real implementation would
408    /// serialise the key/value pair and push it over the peer-to-peer channel.
409    async fn replicate_to_peer(
410        &self,
411        peer: &PeerId,
412        key: &Cid,
413        value: &[f32],
414    ) -> ipfrs_core::Result<()> {
415        // No active transport — log and return Ok so that callers continue.
416        tracing::debug!(
417            "replicate_to_peer: peer={:?} key={} value_len={} (no transport)",
418            peer,
419            key,
420            value.len()
421        );
422        Ok(())
423    }
424
425    /// Query a remote peer for nearest neighbours to `embedding`.
426    ///
427    /// Returns `None` when no active transport is available.  A real
428    /// implementation would serialise the query vector, send it over the
429    /// network, and deserialise the returned [`SearchResult`] list.
430    async fn query_peer(
431        &self,
432        peer: &PeerId,
433        embedding: &[f32],
434    ) -> Option<Vec<crate::hnsw::SearchResult>> {
435        // No active transport — return None so the caller falls back gracefully.
436        tracing::debug!(
437            "query_peer: peer={:?} embedding_len={} (no transport)",
438            peer,
439            embedding.len()
440        );
441        None
442    }
443
444    /// Get synchronization statistics
445    pub fn sync_stats(&self) -> SyncStats {
446        SyncStats {
447            local_entries: self.local_index.read().len(),
448            last_sync_timestamp: *self.last_sync_timestamp.read(),
449            pending_syncs: *self.pending_syncs.read(),
450        }
451    }
452}
453
454/// Statistics for index synchronization
455#[derive(Debug, Clone)]
456pub struct SyncStats {
457    /// Number of entries in local index
458    pub local_entries: usize,
459    /// Timestamp of last successful sync
460    pub last_sync_timestamp: u64,
461    /// Number of pending sync operations
462    pub pending_syncs: usize,
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use crate::hnsw::DistanceMetric;
469
470    #[tokio::test]
471    async fn test_dht_node_creation() {
472        let config = SemanticDHTConfig::default();
473        let peer_id = PeerId::random();
474        let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
475            .expect("test: VectorIndex creation should succeed");
476
477        let node = SemanticDHTNode::new(config, peer_id, index);
478        let stats = node.stats();
479
480        assert_eq!(stats.num_peers, 0);
481        assert_eq!(stats.queries_processed, 0);
482    }
483
484    #[tokio::test]
485    async fn test_local_insert_and_search() {
486        let config = SemanticDHTConfig::default();
487        let peer_id = PeerId::random();
488        let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
489            .expect("test: VectorIndex creation should succeed");
490
491        let node = SemanticDHTNode::new(config, peer_id, index);
492
493        // Insert some vectors
494        for i in 0..10 {
495            use multihash_codetable::{Code, MultihashDigest};
496            let data = format!("test_vector_{}", i);
497            let hash = Code::Sha2_256.digest(data.as_bytes());
498            let cid = Cid::new_v1(0x55, hash);
499            let embedding = vec![i as f32 * 0.1; 768];
500            node.insert(&cid, &embedding)
501                .await
502                .expect("test: node insert should succeed");
503        }
504
505        // Search
506        let query = vec![0.5; 768];
507        let results = node
508            .search_local(&query, 5)
509            .expect("test: local search should succeed");
510
511        assert!(!results.is_empty());
512        assert!(results.len() <= 5);
513    }
514
515    #[tokio::test]
516    async fn test_add_peers() {
517        let config = SemanticDHTConfig::default();
518        let peer_id = PeerId::random();
519        let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
520            .expect("test: VectorIndex creation should succeed");
521
522        let node = SemanticDHTNode::new(config, peer_id, index);
523
524        // Add some peers
525        for i in 0..5 {
526            let peer_id = PeerId::random();
527            let embedding = vec![i as f32 * 0.2; 768];
528            let peer = SemanticPeer::new(peer_id, embedding);
529            node.add_peer(peer).expect("test: add_peer should succeed");
530        }
531
532        let stats = node.stats();
533        assert_eq!(stats.num_peers, 5);
534    }
535
536    #[tokio::test]
537    async fn test_clustering() {
538        let config = SemanticDHTConfig::default();
539        let peer_id = PeerId::random();
540        let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
541            .expect("test: VectorIndex creation should succeed");
542
543        let node = SemanticDHTNode::new(config, peer_id, index);
544
545        // Add peers
546        for i in 0..20 {
547            let peer_id = PeerId::random();
548            let mut embedding = vec![0.0; 768];
549            embedding[0] = if i < 10 { 1.0 } else { -1.0 };
550            let peer = SemanticPeer::new(peer_id, embedding);
551            node.add_peer(peer).expect("test: add_peer should succeed");
552        }
553
554        // Update clusters
555        node.update_clusters(2)
556            .expect("test: update_clusters should succeed");
557
558        let stats = node.stats();
559        assert!(stats.num_clusters > 0);
560    }
561
562    #[tokio::test]
563    async fn test_index_synchronization() {
564        use multihash_codetable::{Code, MultihashDigest};
565
566        let config = SemanticDHTConfig::default();
567        let peer_id1 = PeerId::random();
568        let peer_id2 = PeerId::random();
569
570        let index1 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
571            .expect("test: VectorIndex creation should succeed");
572        let index2 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
573            .expect("test: VectorIndex creation should succeed");
574
575        let node1 = SemanticDHTNode::new(config.clone(), peer_id1, index1);
576        let node2 = SemanticDHTNode::new(config, peer_id2, index2);
577
578        // Insert data into node1
579        let mut cids1 = Vec::new();
580        for i in 0..5 {
581            let data = format!("node1_vector_{}", i);
582            let hash = Code::Sha2_256.digest(data.as_bytes());
583            let cid = Cid::new_v1(0x55, hash);
584            let embedding = vec![i as f32 * 0.1; 768];
585            node1
586                .insert(&cid, &embedding)
587                .await
588                .expect("test: node insert should succeed");
589            cids1.push(cid);
590        }
591
592        // Insert different data into node2
593        let mut cids2 = Vec::new();
594        for i in 5..10 {
595            let data = format!("node2_vector_{}", i);
596            let hash = Code::Sha2_256.digest(data.as_bytes());
597            let cid = Cid::new_v1(0x55, hash);
598            let embedding = vec![i as f32 * 0.1; 768];
599            node2
600                .insert(&cid, &embedding)
601                .await
602                .expect("test: node insert should succeed");
603            cids2.push(cid);
604        }
605
606        // Get snapshots
607        let snapshot1 = node1.get_index_snapshot();
608        let snapshot2 = node2.get_index_snapshot();
609
610        assert_eq!(snapshot1.len(), 5);
611        assert_eq!(snapshot2.len(), 5);
612
613        // Check that node1 has its entries
614        for cid in &cids1 {
615            assert!(node1.has_entry(cid));
616        }
617
618        // Prepare delta: what node2 needs from node1
619        let delta = node1.prepare_sync_delta(&snapshot2);
620        assert_eq!(delta.len(), 5); // All of node1's entries are missing from node2
621
622        // Apply delta (in real implementation, this would fetch and insert)
623        let synced_count = node2
624            .apply_sync_delta(delta)
625            .await
626            .expect("test: apply_sync_delta should succeed");
627        assert_eq!(synced_count, 5);
628
629        // Check sync stats
630        let sync_stats = node1.sync_stats();
631        assert_eq!(sync_stats.local_entries, 5);
632    }
633
634    #[tokio::test]
635    async fn test_sync_with_embeddings() {
636        use multihash_codetable::{Code, MultihashDigest};
637
638        let config = SemanticDHTConfig::default();
639        let peer_id1 = PeerId::random();
640        let peer_id2 = PeerId::random();
641
642        let index1 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
643            .expect("test: VectorIndex creation should succeed");
644        let index2 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
645            .expect("test: VectorIndex creation should succeed");
646
647        let node1 = SemanticDHTNode::new(config.clone(), peer_id1, index1);
648        let node2 = SemanticDHTNode::new(config, peer_id2, index2);
649
650        // Insert data into node1
651        let mut entries_to_sync = Vec::new();
652        for i in 0..5 {
653            let data = format!("sync_test_vector_{}", i);
654            let hash = Code::Sha2_256.digest(data.as_bytes());
655            let cid = Cid::new_v1(0x55, hash);
656            let embedding = vec![i as f32 * 0.1; 768];
657            node1
658                .insert(&cid, &embedding)
659                .await
660                .expect("test: node insert should succeed");
661            entries_to_sync.push((cid, embedding));
662        }
663
664        // Check initial state
665        let sync_stats_before = node2.sync_stats();
666        assert_eq!(sync_stats_before.local_entries, 0);
667        assert_eq!(sync_stats_before.last_sync_timestamp, 0);
668        assert_eq!(sync_stats_before.pending_syncs, 0);
669
670        // Apply sync with embeddings to node2
671        let synced_count = node2
672            .apply_sync_delta_with_embeddings(entries_to_sync.clone())
673            .await
674            .expect("test: apply_sync_delta_with_embeddings should succeed");
675        assert_eq!(synced_count, 5);
676
677        // Check that node2 now has the entries
678        let sync_stats_after = node2.sync_stats();
679        assert_eq!(sync_stats_after.local_entries, 5);
680        assert!(sync_stats_after.last_sync_timestamp > 0); // Should be updated
681        assert_eq!(sync_stats_after.pending_syncs, 0); // Should be back to 0
682
683        // Verify all CIDs are present in node2
684        for (cid, _) in &entries_to_sync {
685            assert!(node2.has_entry(cid));
686        }
687
688        // Search should work on node2 now
689        let query = vec![0.15; 768];
690        let results = node2
691            .search_local(&query, 3)
692            .expect("test: local search after sync should succeed");
693        assert!(!results.is_empty());
694    }
695
696    #[tokio::test]
697    async fn test_dht_replication_stub() {
698        use multihash_codetable::{Code, MultihashDigest};
699
700        let config = SemanticDHTConfig::default();
701        let peer_id = PeerId::random();
702        let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
703            .expect("test: VectorIndex creation should succeed");
704        let node = SemanticDHTNode::new(config, peer_id, index);
705
706        let hash = Code::Sha2_256.digest(b"replication_stub_test");
707        let cid = Cid::new_v1(0x55, hash);
708        let embedding = vec![0.1_f32; 768];
709
710        // replicate_to_peer should succeed (no-op stub) without any transport wired up.
711        let target_peer = PeerId::random();
712        let result = node.replicate_to_peer(&target_peer, &cid, &embedding).await;
713        assert!(
714            result.is_ok(),
715            "replicate_to_peer stub should return Ok(())"
716        );
717    }
718
719    #[tokio::test]
720    async fn test_dht_remote_query_stub() {
721        let config = SemanticDHTConfig::default();
722        let peer_id = PeerId::random();
723        let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
724            .expect("test: VectorIndex creation should succeed");
725        let node = SemanticDHTNode::new(config, peer_id, index);
726
727        let embedding = vec![0.5_f32; 768];
728        let remote_peer = PeerId::random();
729
730        // query_peer should return None when no transport is available.
731        let result = node.query_peer(&remote_peer, &embedding).await;
732        assert!(
733            result.is_none(),
734            "query_peer stub should return None without a transport"
735        );
736    }
737}