1use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use std::collections::{HashMap, HashSet};
22use std::path::PathBuf;
23use std::sync::Arc;
24use tokio::sync::RwLock;
25use uuid::Uuid;
26use zeroize::Zeroize;
27
28use crate::error::{Result, VaultError};
29
30pub type NodeId = String;
32
33#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
35pub struct VectorClock {
36 pub timestamps: HashMap<NodeId, u64>,
38}
39
40impl VectorClock {
41 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn increment(&mut self, node_id: &str) {
48 *self.timestamps.entry(node_id.to_string()).or_insert(0) += 1;
49 }
50
51 pub fn merge(&mut self, other: &VectorClock) {
53 for (node, ts) in &other.timestamps {
54 let entry = self.timestamps.entry(node.clone()).or_insert(0);
55 *entry = (*entry).max(*ts);
56 }
57 }
58
59 pub fn compare(&self, other: &VectorClock) -> ClockComparison {
61 let mut dominated = true;
62 let mut dominates = true;
63
64 for (node, ts) in &self.timestamps {
65 let other_ts = other.timestamps.get(node).copied().unwrap_or(0);
66 if *ts > other_ts {
67 dominated = false;
68 }
69 if *ts < other_ts {
70 dominates = false;
71 }
72 }
73
74 for (node, ts) in &other.timestamps {
75 if !self.timestamps.contains_key(node) && *ts > 0 {
76 dominates = false;
77 }
78 }
79
80 match (dominates, dominated) {
81 (true, true) => ClockComparison::Equal,
82 (true, false) => ClockComparison::After,
83 (false, true) => ClockComparison::Before,
84 (false, false) => ClockComparison::Concurrent,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum ClockComparison {
92 Equal,
94 Before,
96 After,
98 Concurrent,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct FederationConfig {
105 pub node_id: NodeId,
107 pub node_name: String,
109 pub peers: Vec<PeerConfig>,
111 pub sync_interval_secs: u64,
113 pub auto_resolve_conflicts: bool,
115 pub max_concurrent_syncs: usize,
117}
118
119impl Default for FederationConfig {
120 fn default() -> Self {
121 Self {
122 node_id: Uuid::new_v4().to_string(),
123 node_name: hostname::get()
124 .map(|h| h.to_string_lossy().into_owned())
125 .unwrap_or_else(|_| "unknown".into()),
126 peers: Vec::new(),
127 sync_interval_secs: 300, auto_resolve_conflicts: true,
129 max_concurrent_syncs: 4,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct PeerConfig {
137 pub node_id: NodeId,
139 pub name: String,
141 pub endpoint: String,
143 pub api_key: Option<String>,
145 pub enabled: bool,
147}
148
149impl Drop for PeerConfig {
150 fn drop(&mut self) {
151 if let Some(ref mut key) = self.api_key {
152 key.zeroize();
153 }
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct ModelSyncState {
160 pub name: String,
162 pub clock: VectorClock,
164 pub last_sync: HashMap<NodeId, DateTime<Utc>>,
166 pub known_versions: HashSet<String>, pub pending_upload: HashSet<String>,
170 pub pending_download: HashSet<(NodeId, String)>, }
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct SyncManifest {
177 pub source_node: NodeId,
179 pub timestamp: DateTime<Utc>,
181 pub models: Vec<ModelManifestEntry>,
183 pub clock: VectorClock,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct ModelManifestEntry {
190 pub name: String,
192 pub versions: Vec<VersionManifestEntry>,
194 pub clock: VectorClock,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct VersionManifestEntry {
201 pub version: u32,
203 pub checkpoint_id: String,
205 pub created_at: DateTime<Utc>,
207 pub checksum: String,
209 pub size_bytes: u64,
211 pub parent_id: Option<String>,
213 pub origin_node: NodeId,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SyncResult {
220 pub peer_id: NodeId,
222 pub timestamp: DateTime<Utc>,
224 pub duration_ms: u64,
226 pub models_synced: u32,
228 pub versions_uploaded: u32,
230 pub versions_downloaded: u32,
232 pub conflicts: Vec<SyncConflict>,
234 pub errors: Vec<String>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct SyncConflict {
241 pub model: String,
243 pub local_version: String,
245 pub remote_version: String,
247 pub remote_node: NodeId,
249 pub resolution: Option<ConflictResolution>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub enum ConflictResolution {
256 KeepLocal,
258 UseRemote,
260 Branch {
262 local_name: String,
263 remote_name: String,
264 },
265 Manual,
267}
268
269pub struct FederationManager {
271 config: FederationConfig,
272 state: Arc<RwLock<FederationState>>,
273 http_client: reqwest::Client,
274}
275
276struct FederationState {
278 models: HashMap<String, ModelSyncState>,
280 clock: VectorClock,
282 history: Vec<SyncResult>,
284 state_file: PathBuf,
286}
287
288impl FederationManager {
289 pub fn new(config: FederationConfig, state_dir: PathBuf) -> Result<Self> {
291 let state_file = state_dir.join("federation_state.json");
292
293 let federation_state = if state_file.exists() {
295 let contents = std::fs::read_to_string(&state_file)?;
296 let loaded: SavedFederationState = serde_json::from_str(&contents)?;
297 FederationState {
298 models: loaded.models,
299 clock: loaded.clock,
300 history: loaded.history,
301 state_file,
302 }
303 } else {
304 FederationState {
305 models: HashMap::new(),
306 clock: VectorClock::new(),
307 history: Vec::new(),
308 state_file,
309 }
310 };
311
312 let http_client = reqwest::Client::builder()
313 .timeout(std::time::Duration::from_secs(300))
314 .build()
315 .map_err(|e| {
316 VaultError::IoError(std::io::Error::other(format!("HTTP client error: {e}")))
317 })?;
318
319 Ok(Self {
320 config,
321 state: Arc::new(RwLock::new(federation_state)),
322 http_client,
323 })
324 }
325
326 pub fn node_id(&self) -> &str {
328 &self.config.node_id
329 }
330
331 pub fn peers(&self) -> &[PeerConfig] {
333 &self.config.peers
334 }
335
336 pub fn add_peer(&mut self, peer: PeerConfig) {
338 self.config.peers.push(peer);
339 }
340
341 pub fn remove_peer(&mut self, node_id: &str) {
343 self.config.peers.retain(|p| p.node_id != node_id);
344 }
345
346 pub async fn generate_manifest(
348 &self,
349 vault_models: Vec<(String, Vec<crate::version::ModelVersion>)>,
350 ) -> SyncManifest {
351 let state = self.state.read().await;
352
353 let models = vault_models
354 .into_iter()
355 .map(
356 |(name, versions): (String, Vec<crate::version::ModelVersion>)| {
357 let model_clock = state
358 .models
359 .get(&name)
360 .map(|s| s.clock.clone())
361 .unwrap_or_default();
362
363 let version_entries = versions
364 .into_iter()
365 .map(|v| VersionManifestEntry {
366 version: v.version,
367 checkpoint_id: crate::federation_transport::federation_checkpoint_id(
373 &v,
374 ),
375 created_at: v.timestamp,
376 checksum: v.checksum_sha256,
377 size_bytes: v.size_bytes,
378 parent_id: v.parent_version.map(|p| p.to_string()),
379 origin_node: self.config.node_id.clone(),
380 })
381 .collect();
382
383 ModelManifestEntry {
384 name,
385 versions: version_entries,
386 clock: model_clock,
387 }
388 },
389 )
390 .collect();
391
392 SyncManifest {
393 source_node: self.config.node_id.clone(),
394 timestamp: Utc::now(),
395 models,
396 clock: state.clock.clone(),
397 }
398 }
399
400 pub fn compute_delta(&self, local: &SyncManifest, remote: &SyncManifest) -> SyncDelta {
402 let mut to_upload = Vec::new();
403 let mut to_download = Vec::new();
404 let mut conflicts = Vec::new();
405
406 let remote_models: HashMap<&str, &ModelManifestEntry> =
408 remote.models.iter().map(|m| (m.name.as_str(), m)).collect();
409
410 for local_model in &local.models {
412 if let Some(remote_model) = remote_models.get(local_model.name.as_str()) {
413 let local_checkpoints: HashSet<&str> = local_model
415 .versions
416 .iter()
417 .map(|v| v.checkpoint_id.as_str())
418 .collect();
419 let remote_checkpoints: HashSet<&str> = remote_model
420 .versions
421 .iter()
422 .map(|v| v.checkpoint_id.as_str())
423 .collect();
424
425 for version in &local_model.versions {
427 if !remote_checkpoints.contains(version.checkpoint_id.as_str()) {
428 to_upload.push(SyncItem {
429 model: local_model.name.clone(),
430 checkpoint_id: version.checkpoint_id.clone(),
431 size_bytes: version.size_bytes,
432 });
433 }
434 }
435
436 for version in &remote_model.versions {
438 if !local_checkpoints.contains(version.checkpoint_id.as_str()) {
439 to_download.push(SyncItem {
440 model: remote_model.name.clone(),
441 checkpoint_id: version.checkpoint_id.clone(),
442 size_bytes: version.size_bytes,
443 });
444 }
445 }
446
447 let local_by_version: HashMap<u32, &VersionManifestEntry> = local_model
449 .versions
450 .iter()
451 .map(|v| (v.version, v))
452 .collect();
453 let remote_by_version: HashMap<u32, &VersionManifestEntry> = remote_model
454 .versions
455 .iter()
456 .map(|v| (v.version, v))
457 .collect();
458
459 for (version, local_v) in &local_by_version {
460 if let Some(remote_v) = remote_by_version.get(version) {
461 if local_v.checkpoint_id != remote_v.checkpoint_id {
462 conflicts.push(SyncConflict {
463 model: local_model.name.clone(),
464 local_version: local_v.checkpoint_id.clone(),
465 remote_version: remote_v.checkpoint_id.clone(),
466 remote_node: remote.source_node.clone(),
467 resolution: None,
468 });
469 }
470 }
471 }
472 } else {
473 for version in &local_model.versions {
475 to_upload.push(SyncItem {
476 model: local_model.name.clone(),
477 checkpoint_id: version.checkpoint_id.clone(),
478 size_bytes: version.size_bytes,
479 });
480 }
481 }
482 }
483
484 let local_model_names: HashSet<&str> =
486 local.models.iter().map(|m| m.name.as_str()).collect();
487 for remote_model in &remote.models {
488 if !local_model_names.contains(remote_model.name.as_str()) {
489 for version in &remote_model.versions {
490 to_download.push(SyncItem {
491 model: remote_model.name.clone(),
492 checkpoint_id: version.checkpoint_id.clone(),
493 size_bytes: version.size_bytes,
494 });
495 }
496 }
497 }
498
499 SyncDelta {
500 to_upload,
501 to_download,
502 conflicts,
503 }
504 }
505
506 pub async fn sync_with_peer(
508 &self,
509 peer: &PeerConfig,
510 local_manifest: &SyncManifest,
511 download_fn: impl Fn(&str, &str) -> Result<Vec<u8>>,
512 upload_fn: impl Fn(&str, &str, &[u8]) -> Result<()>,
513 ) -> Result<SyncResult> {
514 let start = std::time::Instant::now();
515 let mut errors = Vec::new();
516
517 let remote_manifest = self.fetch_manifest(peer).await?;
519
520 let delta = self.compute_delta(local_manifest, &remote_manifest);
522
523 let mut resolved_conflicts = Vec::new();
525 for conflict in delta.conflicts {
526 let resolution = if self.config.auto_resolve_conflicts {
527 Some(ConflictResolution::KeepLocal)
530 } else {
531 Some(ConflictResolution::Manual)
532 };
533 resolved_conflicts.push(SyncConflict {
534 resolution,
535 ..conflict
536 });
537 }
538
539 let mut downloaded = 0;
541 for item in &delta.to_download {
542 match self
543 .download_version(peer, &item.model, &item.checkpoint_id)
544 .await
545 {
546 Ok(data) => {
547 if let Err(e) = upload_fn(&item.model, &item.checkpoint_id, &data) {
548 errors.push(format!(
549 "Failed to store {}/{}: {e}",
550 item.model, item.checkpoint_id
551 ));
552 } else {
553 downloaded += 1;
554 }
555 }
556 Err(e) => {
557 errors.push(format!(
558 "Failed to download {}/{}: {e}",
559 item.model, item.checkpoint_id
560 ));
561 }
562 }
563 }
564
565 let mut uploaded = 0;
567 for item in &delta.to_upload {
568 match download_fn(&item.model, &item.checkpoint_id) {
569 Ok(data) => {
570 if let Err(e) = self
571 .upload_version(peer, &item.model, &item.checkpoint_id, &data)
572 .await
573 {
574 errors.push(format!(
575 "Failed to upload {}/{}: {e}",
576 item.model, item.checkpoint_id
577 ));
578 } else {
579 uploaded += 1;
580 }
581 }
582 Err(e) => {
583 errors.push(format!(
584 "Failed to read {}/{}: {e}",
585 item.model, item.checkpoint_id
586 ));
587 }
588 }
589 }
590
591 {
593 let mut state = self.state.write().await;
594 state.clock.increment(&self.config.node_id);
595 state.clock.merge(&remote_manifest.clock);
596 }
597
598 let result = SyncResult {
599 peer_id: peer.node_id.clone(),
600 timestamp: Utc::now(),
601 duration_ms: start.elapsed().as_millis() as u64,
602 models_synced: (delta.to_upload.len() + delta.to_download.len()) as u32,
603 versions_uploaded: uploaded,
604 versions_downloaded: downloaded,
605 conflicts: resolved_conflicts,
606 errors,
607 };
608
609 {
611 let mut state = self.state.write().await;
612 state.history.push(result.clone());
613 if state.history.len() > 1000 {
614 state.history.remove(0);
615 }
616 let _ = self.save_state(&state);
617 }
618
619 Ok(result)
620 }
621
622 pub async fn fetch_peer_manifest(&self, peer: &PeerConfig) -> Result<SyncManifest> {
628 self.fetch_manifest(peer).await
629 }
630
631 async fn fetch_manifest(&self, peer: &PeerConfig) -> Result<SyncManifest> {
632 let url = format!("{}/api/v1/federation/manifest", peer.endpoint);
633 let mut req = self.http_client.get(&url);
634
635 if let Some(api_key) = &peer.api_key {
636 req = req.header("X-API-Key", api_key);
637 }
638
639 let response = req
640 .send()
641 .await
642 .map_err(|e| VaultError::IoError(std::io::Error::other(format!("HTTP error: {e}"))))?;
643
644 if !response.status().is_success() {
645 return Err(VaultError::IoError(std::io::Error::other(format!(
646 "Peer returned error: {}",
647 response.status()
648 ))));
649 }
650
651 let manifest = response
652 .json::<SyncManifest>()
653 .await
654 .map_err(|e| VaultError::IoError(std::io::Error::other(format!("JSON error: {e}"))))?;
655
656 Ok(manifest)
657 }
658
659 async fn download_version(
661 &self,
662 peer: &PeerConfig,
663 model: &str,
664 checkpoint_id: &str,
665 ) -> Result<Vec<u8>> {
666 let url = format!(
667 "{}/api/v1/federation/models/{}/versions/{}",
668 peer.endpoint, model, checkpoint_id
669 );
670 let mut req = self.http_client.get(&url);
671
672 if let Some(api_key) = &peer.api_key {
673 req = req.header("X-API-Key", api_key);
674 }
675
676 let response = req
677 .send()
678 .await
679 .map_err(|e| VaultError::IoError(std::io::Error::other(format!("HTTP error: {e}"))))?;
680
681 if !response.status().is_success() {
682 return Err(VaultError::IoError(std::io::Error::other(format!(
683 "Download failed: {}",
684 response.status()
685 ))));
686 }
687
688 let data = response
689 .bytes()
690 .await
691 .map_err(|e| VaultError::IoError(std::io::Error::other(format!("Read error: {e}"))))?;
692
693 Ok(data.to_vec())
694 }
695
696 async fn upload_version(
698 &self,
699 peer: &PeerConfig,
700 model: &str,
701 checkpoint_id: &str,
702 data: &[u8],
703 ) -> Result<()> {
704 let url = format!(
705 "{}/api/v1/federation/models/{}/versions/{}",
706 peer.endpoint, model, checkpoint_id
707 );
708 let mut req = self.http_client.put(&url).body(data.to_vec());
709
710 if let Some(api_key) = &peer.api_key {
711 req = req.header("X-API-Key", api_key);
712 }
713
714 let response = req
715 .send()
716 .await
717 .map_err(|e| VaultError::IoError(std::io::Error::other(format!("HTTP error: {e}"))))?;
718
719 if !response.status().is_success() {
720 return Err(VaultError::IoError(std::io::Error::other(format!(
721 "Upload failed: {}",
722 response.status()
723 ))));
724 }
725
726 Ok(())
727 }
728
729 fn save_state(&self, state: &FederationState) -> Result<()> {
731 let saved = SavedFederationState {
732 models: state.models.clone(),
733 clock: state.clock.clone(),
734 history: state.history.clone(),
735 };
736
737 let json = serde_json::to_string_pretty(&saved)?;
738
739 {
741 use std::io::Write;
742 let mut opts = std::fs::OpenOptions::new();
743 opts.write(true).create(true).truncate(true);
744 crate::permissions::set_create_mode(&mut opts);
745 let mut f = opts.open(&state.state_file)?;
746 f.write_all(json.as_bytes())?;
747 }
748 crate::permissions::restrict_file(&state.state_file)?;
749
750 Ok(())
751 }
752
753 pub async fn get_history(&self, limit: Option<usize>) -> Vec<SyncResult> {
755 let state = self.state.read().await;
756 let history = &state.history;
757 if let Some(n) = limit {
758 history.iter().rev().take(n).cloned().collect()
759 } else {
760 history.clone()
761 }
762 }
763
764 pub async fn status(&self) -> FederationStatus {
766 let state = self.state.read().await;
767 FederationStatus {
768 node_id: self.config.node_id.clone(),
769 node_name: self.config.node_name.clone(),
770 peer_count: self.config.peers.len(),
771 model_count: state.models.len(),
772 last_sync: state.history.last().map(|r| r.timestamp),
773 clock: state.clock.clone(),
774 }
775 }
776}
777
778#[derive(Debug, Clone)]
780pub struct SyncDelta {
781 pub to_upload: Vec<SyncItem>,
783 pub to_download: Vec<SyncItem>,
785 pub conflicts: Vec<SyncConflict>,
787}
788
789#[derive(Debug, Clone)]
791pub struct SyncItem {
792 pub model: String,
794 pub checkpoint_id: String,
796 pub size_bytes: u64,
798}
799
800#[derive(Debug, Clone, Serialize, Deserialize)]
802struct SavedFederationState {
803 models: HashMap<String, ModelSyncState>,
804 clock: VectorClock,
805 history: Vec<SyncResult>,
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize)]
810pub struct FederationStatus {
811 pub node_id: NodeId,
813 pub node_name: String,
815 pub peer_count: usize,
817 pub model_count: usize,
819 pub last_sync: Option<DateTime<Utc>>,
821 pub clock: VectorClock,
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828
829 #[test]
830 fn test_vector_clock_basic() {
831 let mut clock1 = VectorClock::new();
832 let mut clock2 = VectorClock::new();
833
834 clock1.increment("node1");
835 assert_eq!(clock1.timestamps.get("node1"), Some(&1));
836
837 clock2.increment("node2");
838 clock1.merge(&clock2);
839
840 assert_eq!(clock1.timestamps.get("node1"), Some(&1));
841 assert_eq!(clock1.timestamps.get("node2"), Some(&1));
842 }
843
844 #[test]
845 fn test_vector_clock_comparison() {
846 let mut clock1 = VectorClock::new();
847 let mut clock2 = VectorClock::new();
848
849 assert_eq!(clock1.compare(&clock2), ClockComparison::Equal);
851
852 clock1.increment("node1");
854 assert_eq!(clock1.compare(&clock2), ClockComparison::After);
855 assert_eq!(clock2.compare(&clock1), ClockComparison::Before);
856
857 clock2.increment("node2");
859 assert_eq!(clock1.compare(&clock2), ClockComparison::Concurrent);
860 }
861
862 #[test]
863 fn test_vector_clock_multiple_increments() {
864 let mut clock = VectorClock::new();
865 clock.increment("a");
866 clock.increment("a");
867 clock.increment("a");
868 assert_eq!(clock.timestamps.get("a"), Some(&3));
869 }
870
871 #[test]
872 fn test_vector_clock_merge_takes_max() {
873 let mut c1 = VectorClock::new();
874 let mut c2 = VectorClock::new();
875
876 c1.increment("a");
877 c1.increment("a"); c2.increment("a"); c2.increment("b"); c1.merge(&c2);
882 assert_eq!(c1.timestamps.get("a"), Some(&2)); assert_eq!(c1.timestamps.get("b"), Some(&1)); }
885
886 #[test]
887 fn test_vector_clock_equal_after_merge() {
888 let mut c1 = VectorClock::new();
889 c1.increment("a");
890 let c2 = c1.clone();
891 assert_eq!(c1.compare(&c2), ClockComparison::Equal);
892 }
893
894 #[test]
895 fn test_vector_clock_serialization() {
896 let mut clock = VectorClock::new();
897 clock.increment("node1");
898 let json = serde_json::to_string(&clock).unwrap();
899 let deserialized: VectorClock = serde_json::from_str(&json).unwrap();
900 assert_eq!(clock, deserialized);
901 }
902
903 #[test]
904 fn test_clock_comparison_debug() {
905 assert_eq!(format!("{:?}", ClockComparison::Equal), "Equal");
906 assert_eq!(format!("{:?}", ClockComparison::Before), "Before");
907 assert_eq!(format!("{:?}", ClockComparison::After), "After");
908 assert_eq!(format!("{:?}", ClockComparison::Concurrent), "Concurrent");
909 }
910
911 #[test]
912 fn test_federation_config_default() {
913 let config = FederationConfig::default();
914 assert!(!config.node_id.is_empty());
915 assert_eq!(config.sync_interval_secs, 300);
916 assert!(config.auto_resolve_conflicts);
917 assert_eq!(config.max_concurrent_syncs, 4);
918 assert!(config.peers.is_empty());
919 assert!(!config.node_name.is_empty());
920 }
921
922 #[test]
923 fn test_federation_config_serialization() {
924 let config = FederationConfig::default();
925 let json = serde_json::to_string(&config).unwrap();
926 let deserialized: FederationConfig = serde_json::from_str(&json).unwrap();
927 assert_eq!(deserialized.sync_interval_secs, 300);
928 assert!(deserialized.auto_resolve_conflicts);
929 }
930
931 #[test]
932 fn test_peer_config_zeroize_on_drop() {
933 let peer = PeerConfig {
934 node_id: "peer1".to_string(),
935 name: "Test Peer".to_string(),
936 endpoint: "https://example.com".to_string(),
937 api_key: Some("secret-key-12345".to_string()),
938 enabled: true,
939 };
940 drop(peer);
942 }
943
944 #[test]
945 fn test_peer_config_without_api_key() {
946 let peer = PeerConfig {
947 node_id: "peer1".to_string(),
948 name: "Test Peer".to_string(),
949 endpoint: "https://example.com".to_string(),
950 api_key: None,
951 enabled: false,
952 };
953 assert!(!peer.enabled);
954 drop(peer);
955 }
956
957 #[test]
958 fn test_peer_config_serialization() {
959 let peer = PeerConfig {
960 node_id: "p1".to_string(),
961 name: "Peer 1".to_string(),
962 endpoint: "https://peer1.example.com".to_string(),
963 api_key: Some("key".to_string()),
964 enabled: true,
965 };
966 let json = serde_json::to_string(&peer).unwrap();
967 let deserialized: PeerConfig = serde_json::from_str(&json).unwrap();
968 assert_eq!(deserialized.node_id, "p1");
969 assert_eq!(deserialized.api_key.as_deref(), Some("key"));
970 }
971
972 #[test]
973 fn test_model_sync_state_default_fields() {
974 let state = ModelSyncState {
975 name: "test-model".to_string(),
976 clock: VectorClock::new(),
977 last_sync: HashMap::new(),
978 known_versions: HashSet::new(),
979 pending_upload: HashSet::new(),
980 pending_download: HashSet::new(),
981 };
982 assert_eq!(state.name, "test-model");
983 assert!(state.known_versions.is_empty());
984 }
985
986 #[test]
987 fn test_sync_manifest_serialization() {
988 let manifest = SyncManifest {
989 source_node: "node-a".to_string(),
990 timestamp: Utc::now(),
991 models: vec![],
992 clock: VectorClock::new(),
993 };
994 let json = serde_json::to_string(&manifest).unwrap();
995 let deserialized: SyncManifest = serde_json::from_str(&json).unwrap();
996 assert_eq!(deserialized.source_node, "node-a");
997 }
998
999 #[test]
1000 fn test_sync_result_serialization() {
1001 let result = SyncResult {
1002 peer_id: "peer-1".to_string(),
1003 timestamp: Utc::now(),
1004 duration_ms: 1500,
1005 models_synced: 3,
1006 versions_uploaded: 2,
1007 versions_downloaded: 1,
1008 conflicts: vec![],
1009 errors: vec![],
1010 };
1011 let json = serde_json::to_string(&result).unwrap();
1012 assert!(json.contains("peer-1"));
1013 }
1014
1015 #[test]
1016 fn test_sync_conflict_serialization() {
1017 let conflict = SyncConflict {
1018 model: "llama".to_string(),
1019 local_version: "abc".to_string(),
1020 remote_version: "def".to_string(),
1021 remote_node: "node-b".to_string(),
1022 resolution: Some(ConflictResolution::KeepLocal),
1023 };
1024 let json = serde_json::to_string(&conflict).unwrap();
1025 assert!(json.contains("KeepLocal"));
1026 }
1027
1028 #[test]
1029 fn test_conflict_resolution_variants() {
1030 let keep = ConflictResolution::KeepLocal;
1031 let remote = ConflictResolution::UseRemote;
1032 let branch = ConflictResolution::Branch {
1033 local_name: "local-v2".to_string(),
1034 remote_name: "remote-v2".to_string(),
1035 };
1036 let manual = ConflictResolution::Manual;
1037
1038 for r in [keep, remote, manual] {
1040 let json = serde_json::to_string(&r).unwrap();
1041 let _: ConflictResolution = serde_json::from_str(&json).unwrap();
1042 }
1043 let json = serde_json::to_string(&branch).unwrap();
1044 assert!(json.contains("local-v2"));
1045 }
1046
1047 #[test]
1048 fn test_version_manifest_entry() {
1049 let entry = VersionManifestEntry {
1050 version: 3,
1051 checkpoint_id: "cp-xyz".to_string(),
1052 created_at: Utc::now(),
1053 checksum: "sha256-abc".to_string(),
1054 size_bytes: 1024,
1055 parent_id: Some("cp-prev".to_string()),
1056 origin_node: "node-a".to_string(),
1057 };
1058 let json = serde_json::to_string(&entry).unwrap();
1059 let deserialized: VersionManifestEntry = serde_json::from_str(&json).unwrap();
1060 assert_eq!(deserialized.version, 3);
1061 assert_eq!(deserialized.checkpoint_id, "cp-xyz");
1062 }
1063
1064 #[test]
1065 fn test_federation_status_serialization() {
1066 let status = FederationStatus {
1067 node_id: "n1".to_string(),
1068 node_name: "Node One".to_string(),
1069 peer_count: 2,
1070 model_count: 5,
1071 last_sync: Some(Utc::now()),
1072 clock: VectorClock::new(),
1073 };
1074 let json = serde_json::to_string(&status).unwrap();
1075 let d: FederationStatus = serde_json::from_str(&json).unwrap();
1076 assert_eq!(d.peer_count, 2);
1077 }
1078
1079 #[tokio::test]
1080 async fn test_federation_manager_new() {
1081 let dir = tempfile::tempdir().unwrap();
1082 let config = FederationConfig::default();
1083 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1084 assert!(!mgr.node_id().is_empty());
1085 assert!(mgr.peers().is_empty());
1086 }
1087
1088 #[tokio::test]
1089 async fn test_federation_manager_add_remove_peer() {
1090 let dir = tempfile::tempdir().unwrap();
1091 let config = FederationConfig::default();
1092 let mut mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1093
1094 let peer = PeerConfig {
1095 node_id: "peer-1".to_string(),
1096 name: "Peer 1".to_string(),
1097 endpoint: "https://peer1.example.com".to_string(),
1098 api_key: None,
1099 enabled: true,
1100 };
1101 mgr.add_peer(peer);
1102 assert_eq!(mgr.peers().len(), 1);
1103
1104 mgr.remove_peer("peer-1");
1105 assert!(mgr.peers().is_empty());
1106 }
1107
1108 #[tokio::test]
1109 async fn test_federation_manager_status() {
1110 let dir = tempfile::tempdir().unwrap();
1111 let config = FederationConfig {
1112 node_name: "test-node".to_string(),
1113 ..Default::default()
1114 };
1115 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1116
1117 let status = mgr.status().await;
1118 assert_eq!(status.node_name, "test-node");
1119 assert_eq!(status.peer_count, 0);
1120 assert_eq!(status.model_count, 0);
1121 assert!(status.last_sync.is_none());
1122 }
1123
1124 #[tokio::test]
1125 async fn test_federation_manager_get_history_empty() {
1126 let dir = tempfile::tempdir().unwrap();
1127 let config = FederationConfig::default();
1128 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1129
1130 let history = mgr.get_history(None).await;
1131 assert!(history.is_empty());
1132
1133 let limited = mgr.get_history(Some(5)).await;
1134 assert!(limited.is_empty());
1135 }
1136
1137 #[tokio::test]
1138 async fn test_federation_manager_generate_manifest_empty() {
1139 let dir = tempfile::tempdir().unwrap();
1140 let config = FederationConfig::default();
1141 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1142
1143 let manifest = mgr.generate_manifest(vec![]).await;
1144 assert!(manifest.models.is_empty());
1145 assert_eq!(manifest.source_node, mgr.node_id());
1146 }
1147
1148 #[tokio::test]
1149 async fn test_federation_manager_generate_manifest_with_models() {
1150 let dir = tempfile::tempdir().unwrap();
1151 let config = FederationConfig::default();
1152 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1153
1154 let versions = vec![crate::version::ModelVersion {
1155 version: 1,
1156 checkpoint_id: "cp-1".to_string(),
1157 timestamp: Utc::now(),
1158 checksum_sha256: "abc123".to_string(),
1159 size_bytes: 1024,
1160 compressed_size_bytes: 800,
1161 parent_version: None,
1162 format: "safetensors".to_string(),
1163 metadata: HashMap::new(),
1164 file_path: "models/cp-1.enc".to_string(),
1165 }];
1166
1167 let manifest = mgr
1168 .generate_manifest(vec![("my-model".to_string(), versions)])
1169 .await;
1170 assert_eq!(manifest.models.len(), 1);
1171 assert_eq!(manifest.models[0].name, "my-model");
1172 assert_eq!(manifest.models[0].versions.len(), 1);
1173 }
1174
1175 #[test]
1176 fn test_compute_delta_empty_manifests() {
1177 let dir = tempfile::tempdir().unwrap();
1178 let config = FederationConfig::default();
1179 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1180
1181 let local = SyncManifest {
1182 source_node: "a".to_string(),
1183 timestamp: Utc::now(),
1184 models: vec![],
1185 clock: VectorClock::new(),
1186 };
1187 let remote = SyncManifest {
1188 source_node: "b".to_string(),
1189 timestamp: Utc::now(),
1190 models: vec![],
1191 clock: VectorClock::new(),
1192 };
1193
1194 let delta = mgr.compute_delta(&local, &remote);
1195 assert!(delta.to_upload.is_empty());
1196 assert!(delta.to_download.is_empty());
1197 assert!(delta.conflicts.is_empty());
1198 }
1199
1200 #[test]
1201 fn test_compute_delta_local_only_model() {
1202 let dir = tempfile::tempdir().unwrap();
1203 let config = FederationConfig::default();
1204 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1205
1206 let local = SyncManifest {
1207 source_node: "a".to_string(),
1208 timestamp: Utc::now(),
1209 models: vec![ModelManifestEntry {
1210 name: "model-x".to_string(),
1211 versions: vec![VersionManifestEntry {
1212 version: 1,
1213 checkpoint_id: "cp-1".to_string(),
1214 created_at: Utc::now(),
1215 checksum: "sha".to_string(),
1216 size_bytes: 100,
1217 parent_id: None,
1218 origin_node: "a".to_string(),
1219 }],
1220 clock: VectorClock::new(),
1221 }],
1222 clock: VectorClock::new(),
1223 };
1224 let remote = SyncManifest {
1225 source_node: "b".to_string(),
1226 timestamp: Utc::now(),
1227 models: vec![],
1228 clock: VectorClock::new(),
1229 };
1230
1231 let delta = mgr.compute_delta(&local, &remote);
1232 assert_eq!(delta.to_upload.len(), 1);
1233 assert_eq!(delta.to_upload[0].model, "model-x");
1234 assert!(delta.to_download.is_empty());
1235 }
1236
1237 #[test]
1238 fn test_compute_delta_remote_only_model() {
1239 let dir = tempfile::tempdir().unwrap();
1240 let config = FederationConfig::default();
1241 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1242
1243 let local = SyncManifest {
1244 source_node: "a".to_string(),
1245 timestamp: Utc::now(),
1246 models: vec![],
1247 clock: VectorClock::new(),
1248 };
1249 let remote = SyncManifest {
1250 source_node: "b".to_string(),
1251 timestamp: Utc::now(),
1252 models: vec![ModelManifestEntry {
1253 name: "model-y".to_string(),
1254 versions: vec![VersionManifestEntry {
1255 version: 1,
1256 checkpoint_id: "cp-2".to_string(),
1257 created_at: Utc::now(),
1258 checksum: "sha2".to_string(),
1259 size_bytes: 200,
1260 parent_id: None,
1261 origin_node: "b".to_string(),
1262 }],
1263 clock: VectorClock::new(),
1264 }],
1265 clock: VectorClock::new(),
1266 };
1267
1268 let delta = mgr.compute_delta(&local, &remote);
1269 assert!(delta.to_upload.is_empty());
1270 assert_eq!(delta.to_download.len(), 1);
1271 assert_eq!(delta.to_download[0].model, "model-y");
1272 }
1273
1274 #[test]
1275 fn test_compute_delta_conflict() {
1276 let dir = tempfile::tempdir().unwrap();
1277 let config = FederationConfig::default();
1278 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1279
1280 let local = SyncManifest {
1281 source_node: "a".to_string(),
1282 timestamp: Utc::now(),
1283 models: vec![ModelManifestEntry {
1284 name: "model-z".to_string(),
1285 versions: vec![VersionManifestEntry {
1286 version: 1,
1287 checkpoint_id: "cp-local".to_string(),
1288 created_at: Utc::now(),
1289 checksum: "sha-l".to_string(),
1290 size_bytes: 100,
1291 parent_id: None,
1292 origin_node: "a".to_string(),
1293 }],
1294 clock: VectorClock::new(),
1295 }],
1296 clock: VectorClock::new(),
1297 };
1298 let remote = SyncManifest {
1299 source_node: "b".to_string(),
1300 timestamp: Utc::now(),
1301 models: vec![ModelManifestEntry {
1302 name: "model-z".to_string(),
1303 versions: vec![VersionManifestEntry {
1304 version: 1,
1305 checkpoint_id: "cp-remote".to_string(),
1306 created_at: Utc::now(),
1307 checksum: "sha-r".to_string(),
1308 size_bytes: 100,
1309 parent_id: None,
1310 origin_node: "b".to_string(),
1311 }],
1312 clock: VectorClock::new(),
1313 }],
1314 clock: VectorClock::new(),
1315 };
1316
1317 let delta = mgr.compute_delta(&local, &remote);
1318 assert_eq!(delta.conflicts.len(), 1);
1319 assert_eq!(delta.conflicts[0].model, "model-z");
1320 }
1321
1322 #[test]
1323 fn test_compute_delta_shared_versions() {
1324 let dir = tempfile::tempdir().unwrap();
1325 let config = FederationConfig::default();
1326 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1327
1328 let entry = VersionManifestEntry {
1330 version: 1,
1331 checkpoint_id: "cp-same".to_string(),
1332 created_at: Utc::now(),
1333 checksum: "sha".to_string(),
1334 size_bytes: 100,
1335 parent_id: None,
1336 origin_node: "a".to_string(),
1337 };
1338 let local = SyncManifest {
1339 source_node: "a".to_string(),
1340 timestamp: Utc::now(),
1341 models: vec![ModelManifestEntry {
1342 name: "m".to_string(),
1343 versions: vec![entry.clone()],
1344 clock: VectorClock::new(),
1345 }],
1346 clock: VectorClock::new(),
1347 };
1348 let remote = SyncManifest {
1349 source_node: "b".to_string(),
1350 timestamp: Utc::now(),
1351 models: vec![ModelManifestEntry {
1352 name: "m".to_string(),
1353 versions: vec![entry],
1354 clock: VectorClock::new(),
1355 }],
1356 clock: VectorClock::new(),
1357 };
1358
1359 let delta = mgr.compute_delta(&local, &remote);
1360 assert!(delta.to_upload.is_empty());
1361 assert!(delta.to_download.is_empty());
1362 assert!(delta.conflicts.is_empty());
1363 }
1364
1365 #[tokio::test]
1366 async fn test_federation_manager_load_existing_state() {
1367 let dir = tempfile::tempdir().unwrap();
1368 let state_file = dir.path().join("federation_state.json");
1369 let saved = SavedFederationState {
1370 models: HashMap::new(),
1371 clock: {
1372 let mut c = VectorClock::new();
1373 c.increment("existing");
1374 c
1375 },
1376 history: vec![],
1377 };
1378 std::fs::write(&state_file, serde_json::to_string(&saved).unwrap()).unwrap();
1379
1380 let config = FederationConfig::default();
1381 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1382
1383 let status = mgr.status().await;
1384 assert_eq!(status.clock.timestamps.get("existing"), Some(&1));
1385 }
1386
1387 #[test]
1388 fn test_sync_item_debug() {
1389 let item = SyncItem {
1390 model: "m".to_string(),
1391 checkpoint_id: "cp".to_string(),
1392 size_bytes: 42,
1393 };
1394 let dbg = format!("{:?}", item);
1395 assert!(dbg.contains("42"));
1396 }
1397
1398 #[test]
1399 fn test_model_manifest_entry_serialization() {
1400 let entry = ModelManifestEntry {
1401 name: "llm".to_string(),
1402 versions: vec![],
1403 clock: VectorClock::new(),
1404 };
1405 let json = serde_json::to_string(&entry).unwrap();
1406 let d: ModelManifestEntry = serde_json::from_str(&json).unwrap();
1407 assert_eq!(d.name, "llm");
1408 }
1409
1410 #[tokio::test]
1411 async fn test_federation_manager_save_and_reload_state() {
1412 let dir = tempfile::tempdir().unwrap();
1413 let config = FederationConfig::default();
1414 let node_id = config.node_id.clone();
1415 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1416
1417 {
1420 let mut state = mgr.state.write().await;
1421 state.clock.increment(&node_id);
1422 state.models.insert(
1423 "saved-model".to_string(),
1424 ModelSyncState {
1425 name: "saved-model".to_string(),
1426 clock: VectorClock::new(),
1427 last_sync: HashMap::new(),
1428 known_versions: HashSet::new(),
1429 pending_upload: HashSet::new(),
1430 pending_download: HashSet::new(),
1431 },
1432 );
1433 mgr.save_state(&state).unwrap();
1434 }
1435
1436 let config2 = FederationConfig::default();
1438 let mgr2 = FederationManager::new(config2, dir.path().to_path_buf()).unwrap();
1439 let status2 = mgr2.status().await;
1440 assert_eq!(status2.model_count, 1);
1441 assert_eq!(status2.clock.timestamps.get(&node_id), Some(&1));
1442 }
1443
1444 #[test]
1445 fn test_compute_delta_mixed_versions() {
1446 let dir = tempfile::tempdir().unwrap();
1448 let config = FederationConfig::default();
1449 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1450
1451 let shared = VersionManifestEntry {
1452 version: 1,
1453 checkpoint_id: "cp-shared".to_string(),
1454 created_at: Utc::now(),
1455 checksum: "sha".to_string(),
1456 size_bytes: 100,
1457 parent_id: None,
1458 origin_node: "a".to_string(),
1459 };
1460 let local_only = VersionManifestEntry {
1461 version: 2,
1462 checkpoint_id: "cp-local-v2".to_string(),
1463 created_at: Utc::now(),
1464 checksum: "sha2".to_string(),
1465 size_bytes: 200,
1466 parent_id: Some("cp-shared".to_string()),
1467 origin_node: "a".to_string(),
1468 };
1469 let remote_only = VersionManifestEntry {
1470 version: 3,
1471 checkpoint_id: "cp-remote-v3".to_string(),
1472 created_at: Utc::now(),
1473 checksum: "sha3".to_string(),
1474 size_bytes: 300,
1475 parent_id: Some("cp-shared".to_string()),
1476 origin_node: "b".to_string(),
1477 };
1478
1479 let local = SyncManifest {
1480 source_node: "a".to_string(),
1481 timestamp: Utc::now(),
1482 models: vec![ModelManifestEntry {
1483 name: "m".to_string(),
1484 versions: vec![shared.clone(), local_only],
1485 clock: VectorClock::new(),
1486 }],
1487 clock: VectorClock::new(),
1488 };
1489 let remote = SyncManifest {
1490 source_node: "b".to_string(),
1491 timestamp: Utc::now(),
1492 models: vec![ModelManifestEntry {
1493 name: "m".to_string(),
1494 versions: vec![shared, remote_only],
1495 clock: VectorClock::new(),
1496 }],
1497 clock: VectorClock::new(),
1498 };
1499
1500 let delta = mgr.compute_delta(&local, &remote);
1501 assert_eq!(delta.to_upload.len(), 1);
1502 assert_eq!(delta.to_upload[0].checkpoint_id, "cp-local-v2");
1503 assert_eq!(delta.to_download.len(), 1);
1504 assert_eq!(delta.to_download[0].checkpoint_id, "cp-remote-v3");
1505 assert!(delta.conflicts.is_empty()); }
1507
1508 #[tokio::test]
1509 async fn test_federation_manager_generate_manifest_multi_version() {
1510 let dir = tempfile::tempdir().unwrap();
1511 let config = FederationConfig::default();
1512 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1513
1514 let versions = vec![
1515 crate::version::ModelVersion {
1516 version: 1,
1517 checkpoint_id: "cp-1".to_string(),
1518 timestamp: Utc::now(),
1519 checksum_sha256: "abc".to_string(),
1520 size_bytes: 512,
1521 compressed_size_bytes: 400,
1522 parent_version: None,
1523 format: "safetensors".to_string(),
1524 metadata: HashMap::new(),
1525 file_path: "models/cp-1.enc".to_string(),
1526 },
1527 crate::version::ModelVersion {
1528 version: 2,
1529 checkpoint_id: "cp-2".to_string(),
1530 timestamp: Utc::now(),
1531 checksum_sha256: "def".to_string(),
1532 size_bytes: 1024,
1533 compressed_size_bytes: 900,
1534 parent_version: Some(1),
1535 format: "gguf".to_string(),
1536 metadata: HashMap::new(),
1537 file_path: "models/cp-2.enc".to_string(),
1538 },
1539 ];
1540
1541 let manifest = mgr
1542 .generate_manifest(vec![("multi-v".to_string(), versions)])
1543 .await;
1544 assert_eq!(manifest.models.len(), 1);
1545 assert_eq!(manifest.models[0].versions.len(), 2);
1546 assert_eq!(manifest.models[0].versions[0].version, 1);
1547 assert_eq!(manifest.models[0].versions[1].version, 2);
1548 assert_eq!(
1549 manifest.models[0].versions[1].parent_id.as_deref(),
1550 Some("1")
1551 );
1552 }
1553
1554 #[test]
1555 fn test_saved_federation_state_roundtrip() {
1556 let mut clock = VectorClock::new();
1557 clock.increment("n1");
1558 clock.increment("n2");
1559
1560 let saved = SavedFederationState {
1561 models: {
1562 let mut m = HashMap::new();
1563 m.insert(
1564 "model-a".to_string(),
1565 ModelSyncState {
1566 name: "model-a".to_string(),
1567 clock: clock.clone(),
1568 last_sync: HashMap::new(),
1569 known_versions: {
1570 let mut s = HashSet::new();
1571 s.insert("cp-1".to_string());
1572 s
1573 },
1574 pending_upload: HashSet::new(),
1575 pending_download: HashSet::new(),
1576 },
1577 );
1578 m
1579 },
1580 clock,
1581 history: vec![SyncResult {
1582 peer_id: "p1".to_string(),
1583 timestamp: Utc::now(),
1584 duration_ms: 500,
1585 models_synced: 1,
1586 versions_uploaded: 0,
1587 versions_downloaded: 1,
1588 conflicts: vec![],
1589 errors: vec![],
1590 }],
1591 };
1592
1593 let json = serde_json::to_string(&saved).unwrap();
1594 let loaded: SavedFederationState = serde_json::from_str(&json).unwrap();
1595 assert_eq!(loaded.models.len(), 1);
1596 assert!(loaded.models.contains_key("model-a"));
1597 assert_eq!(loaded.history.len(), 1);
1598 assert_eq!(loaded.clock.timestamps.get("n1"), Some(&1));
1599 }
1600
1601 #[test]
1602 fn test_sync_delta_debug() {
1603 let delta = SyncDelta {
1604 to_upload: vec![],
1605 to_download: vec![],
1606 conflicts: vec![],
1607 };
1608 let dbg = format!("{:?}", delta);
1609 assert!(dbg.contains("SyncDelta"));
1610 }
1611
1612 #[test]
1613 fn test_model_sync_state_with_pending() {
1614 let mut state = ModelSyncState {
1615 name: "pending-model".to_string(),
1616 clock: VectorClock::new(),
1617 last_sync: HashMap::new(),
1618 known_versions: HashSet::new(),
1619 pending_upload: HashSet::new(),
1620 pending_download: HashSet::new(),
1621 };
1622 state.pending_upload.insert("cp-up-1".to_string());
1623 state
1624 .pending_download
1625 .insert(("node-b".to_string(), "cp-down-1".to_string()));
1626 state.known_versions.insert("cp-1".to_string());
1627 state.last_sync.insert("node-b".to_string(), Utc::now());
1628
1629 let json = serde_json::to_string(&state).unwrap();
1630 let d: ModelSyncState = serde_json::from_str(&json).unwrap();
1631 assert_eq!(d.pending_upload.len(), 1);
1632 assert_eq!(d.pending_download.len(), 1);
1633 assert_eq!(d.known_versions.len(), 1);
1634 assert_eq!(d.last_sync.len(), 1);
1635 }
1636
1637 #[tokio::test]
1638 async fn test_federation_manager_history_with_limit() {
1639 let dir = tempfile::tempdir().unwrap();
1640 let state_file = dir.path().join("federation_state.json");
1641
1642 let history: Vec<SyncResult> = (0..5)
1644 .map(|i| SyncResult {
1645 peer_id: format!("peer-{}", i),
1646 timestamp: Utc::now(),
1647 duration_ms: 100 * (i as u64 + 1),
1648 models_synced: 1,
1649 versions_uploaded: 0,
1650 versions_downloaded: 1,
1651 conflicts: vec![],
1652 errors: vec![],
1653 })
1654 .collect();
1655
1656 let saved = SavedFederationState {
1657 models: HashMap::new(),
1658 clock: VectorClock::new(),
1659 history,
1660 };
1661 std::fs::write(&state_file, serde_json::to_string(&saved).unwrap()).unwrap();
1662
1663 let config = FederationConfig::default();
1664 let mgr = FederationManager::new(config, dir.path().to_path_buf()).unwrap();
1665
1666 let all = mgr.get_history(None).await;
1667 assert_eq!(all.len(), 5);
1668
1669 let limited = mgr.get_history(Some(3)).await;
1670 assert_eq!(limited.len(), 3);
1671 assert_eq!(limited[0].peer_id, "peer-4");
1673 }
1674
1675 #[test]
1676 fn test_conflict_resolution_branch_serialization() {
1677 let res = ConflictResolution::Branch {
1678 local_name: "main-v2".to_string(),
1679 remote_name: "fork-v2".to_string(),
1680 };
1681 let json = serde_json::to_string(&res).unwrap();
1682 let d: ConflictResolution = serde_json::from_str(&json).unwrap();
1683 match d {
1684 ConflictResolution::Branch {
1685 local_name,
1686 remote_name,
1687 } => {
1688 assert_eq!(local_name, "main-v2");
1689 assert_eq!(remote_name, "fork-v2");
1690 }
1691 _ => panic!("Expected Branch variant"),
1692 }
1693 }
1694
1695 #[test]
1696 fn test_federation_status_without_last_sync() {
1697 let status = FederationStatus {
1698 node_id: "n1".to_string(),
1699 node_name: "node1".to_string(),
1700 peer_count: 0,
1701 model_count: 0,
1702 last_sync: None,
1703 clock: VectorClock::new(),
1704 };
1705 let json = serde_json::to_string(&status).unwrap();
1706 let d: FederationStatus = serde_json::from_str(&json).unwrap();
1707 assert!(d.last_sync.is_none());
1708 }
1709}