1use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct RemoteResult {
18 pub peer_id: String,
20 pub bindings: HashMap<String, String>,
22 pub proof_depth: u32,
24 pub latency_ms: u64,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct InferenceRequest {
31 pub request_id: String,
33 pub goal: String,
35 pub max_depth: u32,
37 pub requester_peer_id: String,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct InferenceResponse {
44 pub request_id: String,
46 pub bindings: Vec<HashMap<String, String>>,
49 pub proof_found: bool,
51 pub error: Option<String>,
53}
54
55#[derive(Debug)]
59pub struct DistributedInferenceSession {
60 pub session_id: String,
62 pub goal: String,
64 pub local_results: Vec<String>,
66 pub remote_results: Vec<RemoteResult>,
68 pub pending_peers: HashSet<String>,
70 pub completed_peers: HashSet<String>,
72 pub started_at: std::time::Instant,
74 pub timeout: std::time::Duration,
76}
77
78impl DistributedInferenceSession {
79 pub fn new(goal: &str, timeout: std::time::Duration) -> Self {
81 Self {
82 session_id: uuid::Uuid::new_v4().to_string(),
83 goal: goal.to_string(),
84 local_results: Vec::new(),
85 remote_results: Vec::new(),
86 pending_peers: HashSet::new(),
87 completed_peers: HashSet::new(),
88 started_at: std::time::Instant::now(),
89 timeout,
90 }
91 }
92
93 pub fn is_complete(&self) -> bool {
101 let any_peers_registered =
102 !self.pending_peers.is_empty() || !self.completed_peers.is_empty();
103
104 if self.started_at.elapsed() >= self.timeout {
105 return true; }
107
108 if !any_peers_registered {
110 return false;
111 }
112
113 self.pending_peers.is_empty()
115 }
116
117 pub fn is_expired(&self) -> bool {
119 self.started_at.elapsed() >= self.timeout
120 }
121}
122
123#[derive(Debug, thiserror::Error)]
127pub enum ReasoningError {
128 #[error("Session not found: {0}")]
129 SessionNotFound(String),
130
131 #[error("Peer already registered: {0}")]
132 PeerAlreadyRegistered(String),
133
134 #[error("Peer not registered in session: {0}")]
135 PeerNotRegistered(String),
136
137 #[error("Session has already expired")]
138 SessionExpired,
139}
140
141#[derive(Debug, Clone)]
145pub struct DistributedReasonerConfig {
146 pub max_depth: usize,
148 pub timeout: std::time::Duration,
150 pub max_peers: usize,
152 pub cache_ttl: std::time::Duration,
154 pub parallel_queries: usize,
156}
157
158impl Default for DistributedReasonerConfig {
159 fn default() -> Self {
160 Self {
161 max_depth: 10,
162 timeout: std::time::Duration::from_secs(30),
163 max_peers: 5,
164 cache_ttl: std::time::Duration::from_secs(300),
165 parallel_queries: 3,
166 }
167 }
168}
169
170#[derive(Debug, Default)]
175pub struct SessionStats {
176 pub active_sessions: usize,
178 pub total_results: usize,
180 pub avg_latency_ms: f64,
182 pub cache_hit_rate: f64,
185}
186
187pub struct DistributedReasonerV2 {
201 #[allow(dead_code)]
204 local_reasoner: crate::reasoning::MemoizedInferenceEngine,
205 pub(super) sessions: HashMap<String, DistributedInferenceSession>,
208 cache: HashMap<String, (std::time::Instant, Vec<RemoteResult>)>,
210 config: DistributedReasonerConfig,
212 cache_lookups: u64,
214 cache_hits: u64,
216}
217
218impl DistributedReasonerV2 {
219 pub fn new(config: DistributedReasonerConfig) -> Self {
221 let cache_mgr = std::sync::Arc::new(crate::cache::CacheManager::new());
222 Self {
223 local_reasoner: crate::reasoning::MemoizedInferenceEngine::new(cache_mgr),
224 sessions: HashMap::new(),
225 cache: HashMap::new(),
226 config,
227 cache_lookups: 0,
228 cache_hits: 0,
229 }
230 }
231
232 pub fn start_session(&mut self, goal: &str) -> String {
238 let session = DistributedInferenceSession::new(goal, self.config.timeout);
239 let id = session.session_id.clone();
240 self.sessions.insert(id.clone(), session);
241 id
242 }
243
244 pub fn start_session_with_id(&mut self, goal: &str, session_id: &str) {
251 let mut session = DistributedInferenceSession::new(goal, self.config.timeout);
252 session.session_id = session_id.to_string();
253 self.sessions.insert(session_id.to_string(), session);
254 }
255
256 pub fn add_session_peer(
263 &mut self,
264 session_id: &str,
265 peer_id: &str,
266 ) -> Result<(), ReasoningError> {
267 let session = self
268 .sessions
269 .get_mut(session_id)
270 .ok_or_else(|| ReasoningError::SessionNotFound(session_id.to_string()))?;
271
272 if session.pending_peers.contains(peer_id) || session.completed_peers.contains(peer_id) {
273 return Err(ReasoningError::PeerAlreadyRegistered(peer_id.to_string()));
274 }
275 session.pending_peers.insert(peer_id.to_string());
276 Ok(())
277 }
278
279 pub fn record_remote_result(
285 &mut self,
286 session_id: &str,
287 result: RemoteResult,
288 ) -> Result<(), ReasoningError> {
289 let session = self
290 .sessions
291 .get_mut(session_id)
292 .ok_or_else(|| ReasoningError::SessionNotFound(session_id.to_string()))?;
293
294 let peer = result.peer_id.clone();
297 session.pending_peers.remove(&peer);
298 session.completed_peers.insert(peer);
299 session.remote_results.push(result);
300 Ok(())
301 }
302
303 pub fn mark_peer_responded(
307 &mut self,
308 session_id: &str,
309 peer_id: &str,
310 ) -> Result<(), ReasoningError> {
311 let session = self
312 .sessions
313 .get_mut(session_id)
314 .ok_or_else(|| ReasoningError::SessionNotFound(session_id.to_string()))?;
315
316 if !session.pending_peers.contains(peer_id) && !session.completed_peers.contains(peer_id) {
317 return Err(ReasoningError::PeerNotRegistered(peer_id.to_string()));
318 }
319 session.pending_peers.remove(peer_id);
320 session.completed_peers.insert(peer_id.to_string());
321 Ok(())
322 }
323
324 pub fn is_session_complete(&self, session_id: &str) -> bool {
329 self.sessions
330 .get(session_id)
331 .map(|s| s.is_complete())
332 .unwrap_or(false)
333 }
334
335 pub fn get_session_results(&mut self, session_id: &str) -> Option<Vec<RemoteResult>> {
341 self.cache_lookups += 1;
342
343 let session = self.sessions.get(session_id)?;
344 let goal = session.goal.clone();
345 let ttl = self.config.cache_ttl;
346
347 if let Some((cached_at, cached)) = self.cache.get(&goal) {
349 if cached_at.elapsed() < ttl {
350 self.cache_hits += 1;
351 return Some(cached.clone());
352 }
353 }
354
355 let results = session.remote_results.clone();
357 if !results.is_empty() {
358 self.cache
359 .insert(goal, (std::time::Instant::now(), results.clone()));
360 }
361 Some(results)
362 }
363
364 pub fn cleanup_expired_sessions(&mut self) -> usize {
370 let expired: Vec<String> = self
371 .sessions
372 .iter()
373 .filter(|(_, s)| s.is_expired())
374 .map(|(id, _)| id.clone())
375 .collect();
376
377 let count = expired.len();
378 for id in expired {
379 self.sessions.remove(&id);
380 }
381 count
382 }
383
384 pub fn evict_stale_cache(&mut self) -> usize {
389 let ttl = self.config.cache_ttl;
390 let before = self.cache.len();
391 self.cache
392 .retain(|_, (cached_at, _)| cached_at.elapsed() < ttl);
393 before - self.cache.len()
394 }
395
396 pub fn session_stats(&self) -> SessionStats {
400 let active_sessions = self.sessions.values().filter(|s| !s.is_complete()).count();
401
402 let all_results: Vec<&RemoteResult> = self
403 .sessions
404 .values()
405 .flat_map(|s| s.remote_results.iter())
406 .collect();
407
408 let total_results = all_results.len();
409
410 let avg_latency_ms = if total_results == 0 {
411 0.0
412 } else {
413 let sum: u64 = all_results.iter().map(|r| r.latency_ms).sum();
414 sum as f64 / total_results as f64
415 };
416
417 let cache_hit_rate = if self.cache_lookups == 0 {
418 0.0
419 } else {
420 self.cache_hits as f64 / self.cache_lookups as f64
421 };
422
423 SessionStats {
424 active_sessions,
425 total_results,
426 avg_latency_ms,
427 cache_hit_rate,
428 }
429 }
430
431 #[inline]
433 pub fn session_count(&self) -> usize {
434 self.sessions.len()
435 }
436
437 pub fn gc_sessions(&mut self, max_age_secs: u64) -> usize {
445 let max_age = std::time::Duration::from_secs(max_age_secs);
446 let expired: Vec<String> = self
447 .sessions
448 .iter()
449 .filter(|(_, s)| s.started_at.elapsed() >= max_age)
450 .map(|(id, _)| id.clone())
451 .collect();
452
453 let count = expired.len();
454 for id in expired {
455 self.sessions.remove(&id);
456 }
457 count
458 }
459
460 pub fn session_metrics(&self) -> SessionMetrics {
468 let mut active_sessions: usize = 0;
469 let mut completed_sessions: usize = 0;
470 let mut expired_sessions: usize = 0;
471 let mut total_peers: usize = 0;
472 let mut latency_sum: u64 = 0;
473 let mut result_count: usize = 0;
474
475 for session in self.sessions.values() {
476 let is_expired = session.is_expired();
477 let is_complete = session.is_complete();
478
479 if is_expired {
480 expired_sessions += 1;
481 }
482 if is_complete {
483 completed_sessions += 1;
484 } else {
485 active_sessions += 1;
486 }
487
488 total_peers += session.pending_peers.len() + session.completed_peers.len();
489
490 for result in &session.remote_results {
491 latency_sum += result.latency_ms;
492 result_count += 1;
493 }
494 }
495
496 let session_count = self.sessions.len();
497 let avg_peers_per_session = if session_count == 0 {
498 0.0
499 } else {
500 total_peers as f64 / session_count as f64
501 };
502
503 let avg_latency_ms = if result_count == 0 {
504 0.0
505 } else {
506 latency_sum as f64 / result_count as f64
507 };
508
509 SessionMetrics {
510 active_sessions,
511 completed_sessions,
512 expired_sessions,
513 avg_peers_per_session,
514 avg_latency_ms,
515 }
516 }
517}
518
519#[derive(Debug, Clone)]
521pub struct SessionMetrics {
522 pub active_sessions: usize,
524 pub completed_sessions: usize,
526 pub expired_sessions: usize,
528 pub avg_peers_per_session: f64,
530 pub avg_latency_ms: f64,
532}
533
534#[derive(Debug, Clone)]
540pub struct PartialResult {
541 pub peer_id: String,
543 pub new_bindings: Vec<HashMap<String, String>>,
545 pub total_so_far: usize,
547 pub is_final: bool,
550}
551
552pub struct InferenceResultStream {
573 session_id: String,
575 rx: tokio::sync::mpsc::Receiver<InferenceResponse>,
577 accumulated: Vec<HashMap<String, String>>,
579 deadline: tokio::time::Instant,
581}
582
583impl InferenceResultStream {
584 pub fn new(
589 session_id: String,
590 rx: tokio::sync::mpsc::Receiver<InferenceResponse>,
591 deadline: tokio::time::Instant,
592 ) -> Self {
593 Self {
594 session_id,
595 rx,
596 accumulated: Vec::new(),
597 deadline,
598 }
599 }
600
601 pub fn session_id(&self) -> &str {
603 &self.session_id
604 }
605
606 pub fn result_count(&self) -> usize {
608 self.accumulated.len()
609 }
610
611 pub async fn next_partial(&mut self) -> Option<PartialResult> {
616 let remaining = self
617 .deadline
618 .saturating_duration_since(tokio::time::Instant::now());
619 if remaining.is_zero() {
620 return None;
621 }
622
623 match tokio::time::timeout(remaining, self.rx.recv()).await {
624 Ok(Some(resp)) => {
625 let new_bindings: Vec<HashMap<String, String>> = resp.bindings;
626 self.accumulated.extend(new_bindings.clone());
627 let total_so_far = self.accumulated.len();
628
629 let is_final = self
632 .deadline
633 .saturating_duration_since(tokio::time::Instant::now())
634 .is_zero();
635
636 Some(PartialResult {
637 peer_id: resp.request_id,
638 new_bindings,
639 total_so_far,
640 is_final,
641 })
642 }
643 Ok(None) | Err(_) => None,
645 }
646 }
647}
648
649#[cfg(test)]
654mod distributed_v2_tests {
655 use super::*;
656
657 fn make_reasoner() -> DistributedReasonerV2 {
659 DistributedReasonerV2::new(DistributedReasonerConfig {
660 max_depth: 5,
661 timeout: std::time::Duration::from_secs(10),
662 max_peers: 3,
663 cache_ttl: std::time::Duration::from_secs(60),
664 parallel_queries: 2,
665 })
666 }
667
668 #[test]
671 fn test_session_lifecycle() {
672 let mut reasoner = make_reasoner();
673
674 let session_id = reasoner.start_session("parent(alice, bob)");
675 assert!(!reasoner.is_session_complete(&session_id));
676
677 reasoner
678 .add_session_peer(&session_id, "peer1")
679 .expect("add peer1");
680 reasoner
681 .mark_peer_responded(&session_id, "peer1")
682 .expect("peer1 responded");
683
684 assert!(reasoner.is_session_complete(&session_id));
686 }
687
688 #[test]
689 fn test_multiple_peers_session() {
690 let mut reasoner = make_reasoner();
691 let sid = reasoner.start_session("ancestor(alice, Z)");
692
693 reasoner.add_session_peer(&sid, "peer1").expect("add peer1");
694 reasoner.add_session_peer(&sid, "peer2").expect("add peer2");
695
696 reasoner
698 .mark_peer_responded(&sid, "peer1")
699 .expect("peer1 responded");
700 assert!(!reasoner.is_session_complete(&sid));
701
702 reasoner
703 .mark_peer_responded(&sid, "peer2")
704 .expect("peer2 responded");
705 assert!(reasoner.is_session_complete(&sid));
706 }
707
708 #[test]
711 fn test_record_remote_result() {
712 let mut reasoner = make_reasoner();
713 let sid = reasoner.start_session("parent(alice, X)");
714
715 reasoner
716 .add_session_peer(&sid, "peer-alpha")
717 .expect("add peer");
718
719 let result = RemoteResult {
720 peer_id: "peer-alpha".to_string(),
721 bindings: [("X".to_string(), "bob".to_string())].into_iter().collect(),
722 proof_depth: 1,
723 latency_ms: 42,
724 };
725 reasoner
726 .record_remote_result(&sid, result)
727 .expect("record result");
728
729 let session = reasoner.sessions.get(&sid).expect("session exists");
731 assert!(!session.pending_peers.contains("peer-alpha"));
732 assert!(session.completed_peers.contains("peer-alpha"));
733 assert_eq!(session.remote_results.len(), 1);
734
735 let results = reasoner.get_session_results(&sid).expect("get results");
736 assert_eq!(results.len(), 1);
737 assert_eq!(results[0].latency_ms, 42);
738 }
739
740 #[test]
741 fn test_record_result_unknown_session() {
742 let mut reasoner = make_reasoner();
743 let result = RemoteResult {
744 peer_id: "ghost".to_string(),
745 bindings: HashMap::new(),
746 proof_depth: 0,
747 latency_ms: 0,
748 };
749 let err = reasoner.record_remote_result("no-such-id", result);
750 assert!(matches!(err, Err(ReasoningError::SessionNotFound(_))));
751 }
752
753 #[test]
756 fn test_session_timeout_detection() {
757 let mut reasoner = DistributedReasonerV2::new(DistributedReasonerConfig {
758 max_depth: 3,
759 timeout: std::time::Duration::from_nanos(1),
761 max_peers: 2,
762 cache_ttl: std::time::Duration::from_secs(60),
763 parallel_queries: 1,
764 });
765
766 let sid = reasoner.start_session("slow_predicate(X)");
767 reasoner
768 .add_session_peer(&sid, "slow-peer")
769 .expect("add peer");
770
771 assert!(reasoner.is_session_complete(&sid));
774
775 let session = reasoner.sessions.get(&sid).expect("session present");
776 assert!(session.is_expired());
777 }
778
779 #[test]
782 fn test_cleanup_expired_sessions() {
783 let mut reasoner = DistributedReasonerV2::new(DistributedReasonerConfig {
784 max_depth: 3,
785 timeout: std::time::Duration::from_nanos(1), max_peers: 2,
787 cache_ttl: std::time::Duration::from_secs(60),
788 parallel_queries: 1,
789 });
790
791 let _s1 = reasoner.start_session("foo(X)");
792 let _s2 = reasoner.start_session("bar(Y)");
793 assert_eq!(reasoner.session_count(), 2);
794
795 let cleaned = reasoner.cleanup_expired_sessions();
796 assert_eq!(cleaned, 2);
797 assert_eq!(reasoner.session_count(), 0);
798 }
799
800 #[test]
801 fn test_cleanup_keeps_active_sessions() {
802 let mut reasoner = make_reasoner(); let _sid = reasoner.start_session("live_goal(X)");
805 assert_eq!(reasoner.session_count(), 1);
806
807 let cleaned = reasoner.cleanup_expired_sessions();
808 assert_eq!(cleaned, 0);
809 assert_eq!(reasoner.session_count(), 1);
810 }
811
812 #[test]
815 fn test_session_stats_empty() {
816 let reasoner = make_reasoner();
817 let stats = reasoner.session_stats();
818 assert_eq!(stats.active_sessions, 0);
819 assert_eq!(stats.total_results, 0);
820 assert_eq!(stats.avg_latency_ms, 0.0);
821 assert_eq!(stats.cache_hit_rate, 0.0);
822 }
823
824 #[test]
825 fn test_session_stats() {
826 let mut reasoner = make_reasoner();
827 let sid = reasoner.start_session("grandparent(alice, Z)");
828
829 reasoner.add_session_peer(&sid, "p1").expect("add p1");
830 reasoner.add_session_peer(&sid, "p2").expect("add p2");
831
832 reasoner
833 .record_remote_result(
834 &sid,
835 RemoteResult {
836 peer_id: "p1".to_string(),
837 bindings: HashMap::new(),
838 proof_depth: 2,
839 latency_ms: 100,
840 },
841 )
842 .expect("record p1 result");
843
844 reasoner
845 .record_remote_result(
846 &sid,
847 RemoteResult {
848 peer_id: "p2".to_string(),
849 bindings: HashMap::new(),
850 proof_depth: 1,
851 latency_ms: 200,
852 },
853 )
854 .expect("record p2 result");
855
856 let stats = reasoner.session_stats();
857 assert_eq!(stats.active_sessions, 0);
859 assert_eq!(stats.total_results, 2);
860 assert!((stats.avg_latency_ms - 150.0).abs() < f64::EPSILON);
862 }
863
864 #[test]
865 fn test_session_stats_active_count() {
866 let mut reasoner = make_reasoner();
867 let sid = reasoner.start_session("pending_goal(X)");
868 reasoner
869 .add_session_peer(&sid, "waiting-peer")
870 .expect("add peer");
871 let stats = reasoner.session_stats();
873 assert_eq!(stats.active_sessions, 1);
874 }
875
876 #[test]
879 fn test_inference_request_serde() {
880 let req = InferenceRequest {
881 request_id: "req-001".to_string(),
882 goal: "ancestor(alice, X)".to_string(),
883 max_depth: 7,
884 requester_peer_id: "12D3KooW...".to_string(),
885 };
886
887 let json = serde_json::to_string(&req).expect("serialize InferenceRequest");
888 let decoded: InferenceRequest =
889 serde_json::from_str(&json).expect("deserialize InferenceRequest");
890
891 assert_eq!(req.request_id, decoded.request_id);
892 assert_eq!(req.goal, decoded.goal);
893 assert_eq!(req.max_depth, decoded.max_depth);
894 assert_eq!(req.requester_peer_id, decoded.requester_peer_id);
895 }
896
897 #[test]
898 fn test_inference_response_serde() {
899 let mut bindings = HashMap::new();
900 bindings.insert("X".to_string(), "charlie".to_string());
901
902 let resp = InferenceResponse {
903 request_id: "req-001".to_string(),
904 bindings: vec![bindings],
905 proof_found: true,
906 error: None,
907 };
908
909 let json = serde_json::to_string(&resp).expect("serialize InferenceResponse");
910 let decoded: InferenceResponse =
911 serde_json::from_str(&json).expect("deserialize InferenceResponse");
912
913 assert_eq!(resp.request_id, decoded.request_id);
914 assert!(decoded.proof_found);
915 assert!(decoded.error.is_none());
916 assert_eq!(decoded.bindings.len(), 1);
917 assert_eq!(
918 decoded.bindings[0].get("X").map(String::as_str),
919 Some("charlie")
920 );
921 }
922
923 #[test]
924 fn test_inference_response_with_error_serde() {
925 let resp = InferenceResponse {
926 request_id: "req-err".to_string(),
927 bindings: vec![],
928 proof_found: false,
929 error: Some("depth limit exceeded".to_string()),
930 };
931
932 let json = serde_json::to_string(&resp).expect("serialize");
933 let decoded: InferenceResponse = serde_json::from_str(&json).expect("deserialize");
934 assert_eq!(decoded.error.as_deref(), Some("depth limit exceeded"));
935 }
936
937 #[test]
938 fn test_remote_result_serde() {
939 let mut bindings = HashMap::new();
940 bindings.insert("Y".to_string(), "dave".to_string());
941
942 let result = RemoteResult {
943 peer_id: "QmPeer123".to_string(),
944 bindings,
945 proof_depth: 3,
946 latency_ms: 55,
947 };
948
949 let json = serde_json::to_string(&result).expect("serialize RemoteResult");
950 let decoded: RemoteResult = serde_json::from_str(&json).expect("deserialize RemoteResult");
951
952 assert_eq!(decoded.peer_id, "QmPeer123");
953 assert_eq!(decoded.proof_depth, 3);
954 assert_eq!(decoded.latency_ms, 55);
955 assert_eq!(decoded.bindings.get("Y").map(String::as_str), Some("dave"));
956 }
957
958 #[test]
961 fn test_add_peer_unknown_session() {
962 let mut reasoner = make_reasoner();
963 let err = reasoner.add_session_peer("ghost-session", "peer1");
964 assert!(matches!(err, Err(ReasoningError::SessionNotFound(_))));
965 }
966
967 #[test]
968 fn test_add_duplicate_peer() {
969 let mut reasoner = make_reasoner();
970 let sid = reasoner.start_session("dup_peer_test(X)");
971 reasoner
972 .add_session_peer(&sid, "peer1")
973 .expect("first add ok");
974 let err = reasoner.add_session_peer(&sid, "peer1");
975 assert!(matches!(err, Err(ReasoningError::PeerAlreadyRegistered(_))));
976 }
977
978 #[test]
979 fn test_mark_unregistered_peer_responded() {
980 let mut reasoner = make_reasoner();
981 let sid = reasoner.start_session("unregistered(X)");
982 let err = reasoner.mark_peer_responded(&sid, "ghost-peer");
983 assert!(matches!(err, Err(ReasoningError::PeerNotRegistered(_))));
984 }
985
986 #[test]
989 fn test_cache_hit_rate() {
990 let mut reasoner = make_reasoner();
991 let sid = reasoner.start_session("cached_goal(X)");
992
993 let result = RemoteResult {
994 peer_id: "cache-peer".to_string(),
995 bindings: HashMap::new(),
996 proof_depth: 1,
997 latency_ms: 10,
998 };
999 reasoner.record_remote_result(&sid, result).expect("record");
1000
1001 let _ = reasoner.get_session_results(&sid);
1003
1004 let sid2 = reasoner.start_session("cached_goal(X)");
1006 let _ = reasoner.get_session_results(&sid2);
1007
1008 let stats = reasoner.session_stats();
1009 assert!((stats.cache_hit_rate - 0.5).abs() < f64::EPSILON);
1011 }
1012
1013 #[test]
1018 fn test_session_gc() {
1019 let mut reasoner = DistributedReasonerV2::new(DistributedReasonerConfig {
1021 max_depth: 3,
1022 timeout: std::time::Duration::from_nanos(1),
1023 max_peers: 2,
1024 cache_ttl: std::time::Duration::from_secs(60),
1025 parallel_queries: 1,
1026 });
1027
1028 for i in 0..5 {
1030 reasoner.start_session(&format!("goal_{i}(X)"));
1031 }
1032 assert_eq!(reasoner.session_count(), 5);
1033
1034 let removed = reasoner.gc_sessions(0);
1036 assert_eq!(removed, 5, "all 5 sessions should have been gc'd");
1037 assert_eq!(reasoner.session_count(), 0);
1038 }
1039
1040 #[test]
1042 fn test_session_gc_keeps_recent() {
1043 let mut reasoner = make_reasoner(); for i in 0..5 {
1046 reasoner.start_session(&format!("fresh_{i}(X)"));
1047 }
1048 assert_eq!(reasoner.session_count(), 5);
1049
1050 let removed = reasoner.gc_sessions(3600);
1052 assert_eq!(removed, 0, "no session should have been gc'd");
1053 assert_eq!(reasoner.session_count(), 5);
1054 }
1055
1056 #[test]
1058 fn test_session_metrics() {
1059 let mut reasoner = make_reasoner(); let sid_a = reasoner.start_session("active_goal(X)");
1063 reasoner
1064 .add_session_peer(&sid_a, "peer-a")
1065 .expect("add peer");
1066
1067 let sid_b = reasoner.start_session("done_goal(Y)");
1069 reasoner
1070 .add_session_peer(&sid_b, "peer-b")
1071 .expect("add peer-b");
1072 reasoner
1073 .record_remote_result(
1074 &sid_b,
1075 RemoteResult {
1076 peer_id: "peer-b".to_string(),
1077 bindings: HashMap::new(),
1078 proof_depth: 1,
1079 latency_ms: 80,
1080 },
1081 )
1082 .expect("record peer-b");
1083
1084 let metrics = reasoner.session_metrics();
1085
1086 assert_eq!(metrics.active_sessions, 1, "one session should be active");
1088 assert_eq!(
1089 metrics.completed_sessions, 1,
1090 "one session should be completed"
1091 );
1092 assert!(
1095 (metrics.avg_peers_per_session - 1.0).abs() < f64::EPSILON,
1096 "avg_peers_per_session should be 1.0, got {}",
1097 metrics.avg_peers_per_session
1098 );
1099 assert!(
1101 (metrics.avg_latency_ms - 80.0).abs() < f64::EPSILON,
1102 "avg_latency_ms should be 80.0, got {}",
1103 metrics.avg_latency_ms
1104 );
1105 }
1106
1107 #[tokio::test]
1112 async fn test_inference_result_stream_collects() {
1113 let (tx, rx) = tokio::sync::mpsc::channel::<InferenceResponse>(16);
1114
1115 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
1117 let mut stream = InferenceResultStream::new("session-123".to_string(), rx, deadline);
1118
1119 for i in 0u32..3 {
1121 let mut b = HashMap::new();
1122 b.insert("X".to_string(), format!("val{i}"));
1123 tx.send(InferenceResponse {
1124 request_id: format!("peer-{i}"),
1125 bindings: vec![b],
1126 proof_found: true,
1127 error: None,
1128 })
1129 .await
1130 .expect("send");
1131 }
1132 drop(tx);
1134
1135 assert_eq!(stream.session_id(), "session-123");
1136
1137 let mut received_count = 0usize;
1138 while let Some(partial) = stream.next_partial().await {
1139 received_count += 1;
1140 assert_eq!(
1141 partial.new_bindings.len(),
1142 1,
1143 "each peer sends exactly one binding set"
1144 );
1145 assert_eq!(
1146 partial.total_so_far, received_count,
1147 "accumulated count must grow monotonically"
1148 );
1149 }
1150
1151 assert_eq!(
1152 received_count, 3,
1153 "must have received exactly 3 partial results"
1154 );
1155 assert_eq!(stream.result_count(), 3);
1156 }
1157}